Add an optional activationKey to the feature role of capability.json — the dotted config key that gates the whole capability (e.g. graphify.enabled). gen-capability-registry validates it (non-empty string, reserved-name guard, must be declared in the capability's own config slice, feature-only) and emits it per-capability in the generated registry. Declared on graphify + intel. No runtime consumption yet (resolver wiring lands in #1305). Part of #1302. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -155,13 +155,13 @@ Tiny read-only helper Module owning discovery of canonical `commands/gsd/*.md` c
|
||||
Projects a pure, typed install plan for a given runtime by composing artifact placements (Runtime Artifact Layout Module), command text (Shell Command Projection Module), and per-runtime config intentions — with no filesystem IO or format-specific serialization. Runtime-specific adapters consume the plan and execute concrete file mutations and config rendering. See ADR-58.
|
||||
|
||||
### Capability [Planned]
|
||||
A bundle delivering one optional GSD feature, toggled as a unit at install or after install. Owns its skills, agents, hooks, federated config-key schema (keys + defaults + validation), and loop extension-point registrations, plus a `requires` list of other Capabilities. Declared co-located in the Capability's own folder and compiled into a generated central Capability Registry at build time. The five-step loop (Discuss → Plan → Execute → Verify → Ship) and shared-infrastructure skills (phase, config, help, update, surface, progress) are the privileged host, not Capabilities, in v1 — but host extension points are data so a loop step can become a Capability under a future uniform kernel. Supersedes the implicit feature-scattering across clusters, install-profiles, and config-schema. Generalizes the Skill Surface Budget Module and Runtime Install Policy Module.
|
||||
A bundle delivering one optional GSD feature, toggled as a unit at install or after install. Owns its skills, agents, hooks, federated config-key schema (keys + defaults + validation), and loop extension-point registrations, plus a `requires` list of other Capabilities, plus an optional `activationKey` (a dotted config key, e.g. `graphify.enabled`) naming the config toggle that gates the whole capability — consumed by the Capability State Resolver's per-capability `active` (absent → no config gate; see Capability State Resolver tri-state deepening). Declared co-located in the Capability's own folder and compiled into a generated central Capability Registry at build time. The five-step loop (Discuss → Plan → Execute → Verify → Ship) and shared-infrastructure skills (phase, config, help, update, surface, progress) are the privileged host, not Capabilities, in v1 — but host extension points are data so a loop step can become a Capability under a future uniform kernel. Supersedes the implicit feature-scattering across clusters, install-profiles, and config-schema. Generalizes the Skill Surface Budget Module and Runtime Install Policy Module.
|
||||
|
||||
### Loop Host Contract
|
||||
Generated description of what the five-step loop (Discuss → Plan → Execute → Verify → Ship) exposes as extension points: per-step loop points, agent roles, and core artifacts. Sourced from structured `<!-- gsd:loop-host ... -->` HTML-comment markers embedded near the top of each of the five step workflow files (`discuss-phase.md`, `plan-phase.md`, `execute-phase.md`, `verify-work.md`, `ship.md`). Generated by `scripts/gen-loop-host-contract.cjs` → `gsd-core/bin/lib/loop-host-contract.cjs` (ADR-894 §3 phase 3a-impl-2). Covers exactly the 12 canonical points (discuss:pre/post, plan:pre/post, execute:pre/wave:pre/wave:post/post, verify:pre/post, ship:pre/post). The generator enforces a drift guard: every declared non-orchestrator agent role must correspond to an actual agent reference in the workflow file. Consumed by `gen-capability-registry.cjs` (replaces the former inline `LOOP_HOST_CONTRACT` constant). Run `node scripts/gen-loop-host-contract.cjs --write` after editing a workflow step marker.
|
||||
|
||||
### Capability Registry
|
||||
Generated central manifest projecting all co-located Capability declarations into one validated artifact for runtime resolution and for the install, surface, config, and loop-extension adapters. Mirrors the research-profiles / package-identity generation pattern (co-located source → generated central file). Generated by `scripts/gen-capability-registry.cjs` → `gsd-core/bin/lib/capability-registry.cjs` (ADR-894 §5 phase 3a-impl). Role-partitioned indexes: `bySkill`, `byAgent`, `byLoopPoint` (hook ordering materialized), `configKeys` (ownership map: key→capId), `configSchema` (full per-key schema: key→{ owner, type, default, description }), `runtimes`, `requiresClosure(id)`. ADR-857 phase 3b adds `configSchema` with validated type/default/description per key, sourced from each capability's `.config` slice. ADR-857 phase 4a adds two derived views: `capabilityClusters` (`{ <capId>: [<skill stems>] }` — each cap's skills array, sorted, derived from the capability's `skills` declaration; consistency-gated against the hand-authored `CLUSTERS`) and `profileMembership` (`{ <capId>: { tier, profiles: [...] } }` — the tier-derived index: suffix of `PROFILE_RANK` starting at the capability's tier). Both views cover the same capability set: only capabilities that own skills (non-empty `skills` array). The generator enforces a HARD gate (throws) if a capId matching a `CLUSTERS` key has a mismatched skill set, and emits SOFT `⚠ pending-reconciliation` warnings to stderr (never to the file) for skills not yet in the hand-authored profile at the capability's tier. `install` and `surface` are UNTOUCHED (still read hand-authored constants; derived views are emitted and tested but unconsumed until cutover). Validated against the Loop Host Contract (12 points; generated by `gen-loop-host-contract.cjs` from workflow markers, phase 3a-impl-2). Run `node scripts/gen-capability-registry.cjs --write` after editing any `capabilities/<id>/capability.json`.
|
||||
Generated central manifest projecting all co-located Capability declarations into one validated artifact for runtime resolution and for the install, surface, config, and loop-extension adapters. Mirrors the research-profiles / package-identity generation pattern (co-located source → generated central file). Generated by `scripts/gen-capability-registry.cjs` → `gsd-core/bin/lib/capability-registry.cjs` (ADR-894 §5 phase 3a-impl). Role-partitioned indexes: `bySkill`, `byAgent`, `byLoopPoint` (hook ordering materialized), `configKeys` (ownership map: key→capId), `configSchema` (full per-key schema: key→{ owner, type, default, description }), `runtimes`, `requiresClosure(id)`. Each feature capability's entry in `capabilities` now includes the optional `activationKey` field (the dotted config key that gates the whole capability, e.g. `"graphify.enabled"`; absent means no config gate). ADR-857 phase 3b adds `configSchema` with validated type/default/description per key, sourced from each capability's `.config` slice. ADR-857 phase 4a adds two derived views: `capabilityClusters` (`{ <capId>: [<skill stems>] }` — each cap's skills array, sorted, derived from the capability's `skills` declaration; consistency-gated against the hand-authored `CLUSTERS`) and `profileMembership` (`{ <capId>: { tier, profiles: [...] } }` — the tier-derived index: suffix of `PROFILE_RANK` starting at the capability's tier). Both views cover the same capability set: only capabilities that own skills (non-empty `skills` array). The generator enforces a HARD gate (throws) if a capId matching a `CLUSTERS` key has a mismatched skill set, and emits SOFT `⚠ pending-reconciliation` warnings to stderr (never to the file) for skills not yet in the hand-authored profile at the capability's tier. `install` and `surface` are UNTOUCHED (still read hand-authored constants; derived views are emitted and tested but unconsumed until cutover). Validated against the Loop Host Contract (12 points; generated by `gen-loop-host-contract.cjs` from workflow markers, phase 3a-impl-2). Run `node scripts/gen-capability-registry.cjs --write` after editing any `capabilities/<id>/capability.json`.
|
||||
|
||||
### Federated Config
|
||||
ADR-857 phase 3b seam that merges capability-declared config slices into the `loadConfig` return value. Implemented in `src/federated-config.cts` → `gsd-core/bin/lib/federated-config.cjs`. Exports `mergeFederatedConfig({ configSchema, isCentralKey, userConfig }) → { values, validKeys, warnings }`. Rules: central-schema keys are skipped with a `pending-migration` warning; malformed slices are skipped with a warning (never throws); valid federated keys (absent from the central schema) resolve to the user-supplied value (if type-matches) or the slice default. Object writes are guarded against prototype pollution with inline literal `__proto__`/`constructor`/`prototype` key checks. ADR-857 phase 6 made the channel live for migrated Capability keys: `config-schema.cjs` exposes `isCentralConfigKey()` for central ownership and `isValidConfigKey()` accepts central + runtime + dynamic + Capability-owned registry keys. `loadConfig` exposes `_setFederatedRegistryForTests`/`_resetFederatedRegistryForTests` seams for injecting a synthetic registry in tests.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"runtimeCompat": { "supported": ["*"], "unsupported": [] },
|
||||
"skills": ["graphify"],
|
||||
"agents": [],
|
||||
"activationKey": "graphify.enabled",
|
||||
"config": {
|
||||
"graphify.enabled": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"runtimeCompat": { "supported": ["*"], "unsupported": [] },
|
||||
"skills": [],
|
||||
"agents": [],
|
||||
"activationKey": "intel.enabled",
|
||||
"config": {
|
||||
"intel.enabled": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -762,6 +762,7 @@ const capabilities = {
|
||||
"graphify"
|
||||
],
|
||||
"agents": [],
|
||||
"activationKey": "graphify.enabled",
|
||||
"config": {
|
||||
"graphify.enabled": {
|
||||
"type": "boolean",
|
||||
@@ -845,6 +846,7 @@ const capabilities = {
|
||||
},
|
||||
"skills": [],
|
||||
"agents": [],
|
||||
"activationKey": "intel.enabled",
|
||||
"config": {
|
||||
"intel.enabled": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -542,6 +542,32 @@ function validateFeatureBody(cap) {
|
||||
}
|
||||
}
|
||||
|
||||
// activationKey: optional string naming the dotted config key that gates this capability.
|
||||
// If present: must be a non-empty string that is declared in this capability's own config slice.
|
||||
if (cap.activationKey !== undefined) {
|
||||
if (typeof cap.activationKey !== 'string' || cap.activationKey.length === 0) {
|
||||
errors.push(
|
||||
'capability "' + (cap.id || '(unknown)') + '" activationKey must be a non-empty string (got: ' +
|
||||
JSON.stringify(cap.activationKey) + ')',
|
||||
);
|
||||
} else if (cap.activationKey === '__proto__' || cap.activationKey === 'constructor' || cap.activationKey === 'prototype') {
|
||||
// Prototype-pollution guard (inline literal, CodeQL barrier)
|
||||
errors.push(
|
||||
'capability "' + (cap.id || '(unknown)') + '" activationKey "' + cap.activationKey +
|
||||
'" is a reserved JavaScript property name and cannot be used as an activationKey',
|
||||
);
|
||||
} else if (
|
||||
typeof cap.config !== 'object' ||
|
||||
cap.config === null ||
|
||||
!Object.prototype.hasOwnProperty.call(cap.config, cap.activationKey)
|
||||
) {
|
||||
errors.push(
|
||||
'capability "' + (cap.id || '(unknown)') + '" activationKey "' + cap.activationKey +
|
||||
'" is not declared in this capability\'s config slice — add it to the "config" object or use a key that is declared there',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -586,7 +612,7 @@ const VALID_HOOK_EVENTS = new Set(['claude', 'gemini', 'opencode-subset']);
|
||||
const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
|
||||
const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills', 'kimi-agents']);
|
||||
const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']);
|
||||
const FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks'];
|
||||
const FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks', 'activationKey'];
|
||||
const VALID_INSTALL_SURFACES = new Set(['settings-json', 'codex-toml', 'copilot-instructions', 'cline-rules', 'cursor-hooks-json', 'profile-marker-only']);
|
||||
const VALID_PERMISSION_WRITERS = new Set(['opencode', 'kilo']);
|
||||
const VALID_EXTENDED_HOOK_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'BeforeAgent', 'AfterAgent', 'BeforeModel']);
|
||||
|
||||
@@ -4772,3 +4772,262 @@ describe('#1196 — discuss loop wiring + wired-point guard', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── activationKey validation (issue #1304 Phase 1) ─────────────────────────
|
||||
|
||||
describe('activationKey validation', () => {
|
||||
// Minimal valid feature capability fixture for activationKey tests.
|
||||
// Uses UI_CAP as a base so all required fields are satisfied.
|
||||
function makeCapWithActivationKey(activationKey) {
|
||||
const cap = { ...UI_CAP, activationKey };
|
||||
if (activationKey === undefined) delete cap.activationKey;
|
||||
return cap;
|
||||
}
|
||||
|
||||
// (a) valid activationKey referencing a key declared in the cap's own config slice
|
||||
test('(a) valid activationKey referencing own config key: no errors, emitted in registry', () => {
|
||||
// UI_CAP declares 'workflow.ui_phase' (boolean) in its config — use that as activationKey
|
||||
const cap = makeCapWithActivationKey('workflow.ui_phase');
|
||||
const errors = validateCapability(cap, 'ui');
|
||||
assert.deepEqual(
|
||||
errors,
|
||||
[],
|
||||
'Expected no validation errors for activationKey that matches own config key, got: ' +
|
||||
JSON.stringify(errors),
|
||||
);
|
||||
|
||||
// Confirm activationKey is emitted in the built registry
|
||||
const capDir = makeTempCapDir({ ui: cap });
|
||||
const { capMap, errors: loadErrors } = loadAndValidate(new Set(), capDir);
|
||||
assert.deepEqual(loadErrors, [], 'Expected no load errors: ' + JSON.stringify(loadErrors));
|
||||
const registry = buildRegistry(capMap);
|
||||
assert.strictEqual(
|
||||
registry.capabilities.ui.activationKey,
|
||||
'workflow.ui_phase',
|
||||
'registry.capabilities.ui.activationKey must equal the declared activationKey',
|
||||
);
|
||||
});
|
||||
|
||||
// (b) activationKey referencing an UNKNOWN config key → a specific error naming the cap + key
|
||||
test('(b) activationKey referencing unknown config key: specific error emitted', () => {
|
||||
const cap = makeCapWithActivationKey('no-such-key.enabled');
|
||||
const errors = validateCapability(cap, 'ui');
|
||||
assert.ok(
|
||||
errors.length > 0,
|
||||
'Expected at least one error when activationKey references an unknown config key',
|
||||
);
|
||||
const joined = errors.join('\n');
|
||||
assert.ok(
|
||||
joined.includes('no-such-key.enabled'),
|
||||
'Error must name the bad activationKey, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
assert.ok(
|
||||
joined.includes('ui') || joined.includes('(unknown)'),
|
||||
'Error must name the capability id, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
assert.ok(
|
||||
joined.includes('config'),
|
||||
'Error must reference the config slice, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
// (c) activationKey absent → valid (back-compat)
|
||||
test('(c) activationKey absent: valid (back-compat — no errors)', () => {
|
||||
const cap = makeCapWithActivationKey(undefined);
|
||||
assert.ok(
|
||||
!Object.prototype.hasOwnProperty.call(cap, 'activationKey'),
|
||||
'Fixture must not have activationKey when undefined is passed',
|
||||
);
|
||||
const errors = validateCapability(cap, 'ui');
|
||||
assert.deepEqual(
|
||||
errors,
|
||||
[],
|
||||
'Expected no validation errors when activationKey is absent, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
// (d) activationKey present but empty string → error
|
||||
test('(d) activationKey empty string: error', () => {
|
||||
const cap = makeCapWithActivationKey('');
|
||||
const errors = validateCapability(cap, 'ui');
|
||||
assert.ok(
|
||||
errors.length > 0,
|
||||
'Expected at least one error when activationKey is an empty string',
|
||||
);
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('activationKey') && e.includes('non-empty')),
|
||||
'Error must mention activationKey and non-empty, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
// (d-extra) activationKey non-string (number) → error
|
||||
test('(d-extra) activationKey non-string (number): error', () => {
|
||||
const cap = { ...UI_CAP, activationKey: 42 };
|
||||
const errors = validateCapability(cap, 'ui');
|
||||
assert.ok(
|
||||
errors.length > 0,
|
||||
'Expected at least one error when activationKey is a number',
|
||||
);
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('activationKey') && e.includes('non-empty')),
|
||||
'Error must mention activationKey and non-empty string requirement, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
// (e) reserved-name guard: activationKey === '__proto__' → reserved-name error, not not-declared error
|
||||
test('(e) activationKey "__proto__": reserved-name error (not not-declared error)', () => {
|
||||
const cap = makeCapWithActivationKey('__proto__');
|
||||
const errors = validateCapability(cap, 'ui');
|
||||
assert.ok(
|
||||
errors.length > 0,
|
||||
'Expected at least one error when activationKey is "__proto__"',
|
||||
);
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('__proto__') && e.includes('reserved')),
|
||||
'Error must mention "__proto__" and "reserved", got: ' + JSON.stringify(errors),
|
||||
);
|
||||
// Must NOT emit the not-declared error (the guard runs before hasOwnProperty.call)
|
||||
assert.ok(
|
||||
!errors.some((e) => e.includes('is not declared in this capability\'s config slice')),
|
||||
'Reserved-name guard must fire before the not-declared check; got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
// (f) reserved-name guard: activationKey === 'constructor' → reserved-name error
|
||||
test('(f) activationKey "constructor": reserved-name error', () => {
|
||||
const cap = makeCapWithActivationKey('constructor');
|
||||
const errors = validateCapability(cap, 'ui');
|
||||
assert.ok(
|
||||
errors.length > 0,
|
||||
'Expected at least one error when activationKey is "constructor"',
|
||||
);
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('constructor') && e.includes('reserved')),
|
||||
'Error must mention "constructor" and "reserved", got: ' + JSON.stringify(errors),
|
||||
);
|
||||
assert.ok(
|
||||
!errors.some((e) => e.includes('is not declared in this capability\'s config slice')),
|
||||
'Reserved-name guard must fire before the not-declared check; got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
// (g) reserved-name guard: activationKey === 'prototype' → reserved-name error
|
||||
test('(g) activationKey "prototype": reserved-name error', () => {
|
||||
const cap = makeCapWithActivationKey('prototype');
|
||||
const errors = validateCapability(cap, 'ui');
|
||||
assert.ok(
|
||||
errors.length > 0,
|
||||
'Expected at least one error when activationKey is "prototype"',
|
||||
);
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('prototype') && e.includes('reserved')),
|
||||
'Error must mention "prototype" and "reserved", got: ' + JSON.stringify(errors),
|
||||
);
|
||||
assert.ok(
|
||||
!errors.some((e) => e.includes('is not declared in this capability\'s config slice')),
|
||||
'Reserved-name guard must fire before the not-declared check; got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
// (h) regression guard: activationKey === null → non-empty-string error (typeof null === 'object' footgun)
|
||||
test('(h) activationKey null: non-empty-string error (typeof null footgun regression guard)', () => {
|
||||
const cap = { ...UI_CAP, activationKey: null };
|
||||
const errors = validateCapability(cap, 'ui');
|
||||
assert.ok(
|
||||
errors.length > 0,
|
||||
'Expected at least one error when activationKey is null',
|
||||
);
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('activationKey') && e.includes('non-empty')),
|
||||
'Error must mention activationKey and non-empty string requirement (typeof null === "object" must not bypass the check), got: ' + JSON.stringify(errors),
|
||||
);
|
||||
// Must NOT emit the reserved-name error
|
||||
assert.ok(
|
||||
!errors.some((e) => e.includes('reserved')),
|
||||
'null must not trigger the reserved-name guard, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
// Registry integration: activationKey absent → field absent in registry entry (omit semantics)
|
||||
test('activationKey absent: field omitted from registry capabilities entry', () => {
|
||||
const cap = makeCapWithActivationKey(undefined);
|
||||
const capDir = makeTempCapDir({ ui: cap });
|
||||
const { capMap, errors } = loadAndValidate(new Set(), capDir);
|
||||
assert.deepEqual(errors, [], 'Expected no load errors: ' + JSON.stringify(errors));
|
||||
const registry = buildRegistry(capMap);
|
||||
assert.ok(
|
||||
!Object.prototype.hasOwnProperty.call(registry.capabilities.ui, 'activationKey'),
|
||||
'activationKey must be absent from registry.capabilities.ui when not declared',
|
||||
);
|
||||
});
|
||||
|
||||
// Verify graphify capability.json declares correct activationKey
|
||||
test('graphify capability.json declares activationKey matching its own config key', () => {
|
||||
const graphifyCap = JSON.parse(
|
||||
require('node:fs').readFileSync(
|
||||
require('node:path').join(ROOT, 'capabilities', 'graphify', 'capability.json'),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
assert.strictEqual(
|
||||
graphifyCap.activationKey,
|
||||
'graphify.enabled',
|
||||
'graphify capability.json must declare activationKey: "graphify.enabled"',
|
||||
);
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(graphifyCap.config, 'graphify.enabled'),
|
||||
'graphify capability.json config must contain key "graphify.enabled"',
|
||||
);
|
||||
});
|
||||
|
||||
// Verify intel capability.json declares correct activationKey
|
||||
test('intel capability.json declares activationKey matching its own config key', () => {
|
||||
const intelCap = JSON.parse(
|
||||
require('node:fs').readFileSync(
|
||||
require('node:path').join(ROOT, 'capabilities', 'intel', 'capability.json'),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
assert.strictEqual(
|
||||
intelCap.activationKey,
|
||||
'intel.enabled',
|
||||
'intel capability.json must declare activationKey: "intel.enabled"',
|
||||
);
|
||||
assert.ok(
|
||||
Object.prototype.hasOwnProperty.call(intelCap.config, 'intel.enabled'),
|
||||
'intel capability.json config must contain key "intel.enabled"',
|
||||
);
|
||||
});
|
||||
|
||||
// (i) role:runtime capability with activationKey → feature-only field error
|
||||
test('(i) role:runtime with activationKey: feature-only field error', () => {
|
||||
const cap = {
|
||||
id: 'cursor', role: 'runtime', title: 'Cursor', description: 'Cursor IDE runtime',
|
||||
tier: 'standard', requires: [],
|
||||
activationKey: 'some.key',
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.cursor', env: ['CURSOR_CONFIG_DIR'] },
|
||||
configFormat: 'settings-json',
|
||||
artifactLayout: { global: [], local: [] },
|
||||
commandStyle: 'slash-hyphen',
|
||||
hooksSurface: 'cursor-hooks-json',
|
||||
hookEvents: 'claude',
|
||||
sandboxTier: 'none',
|
||||
supportTier: 2,
|
||||
installSurface: 'cursor-hooks-json',
|
||||
writesSharedSettings: false,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
},
|
||||
};
|
||||
const errors = validateCapability(cap, 'cursor');
|
||||
assert.ok(
|
||||
errors.length > 0,
|
||||
'Expected at least one error when role:runtime declares activationKey',
|
||||
);
|
||||
assert.ok(
|
||||
errors.some((e) => e.includes('activationKey') && e.includes('feature-only')),
|
||||
'Error must mention activationKey and feature-only, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user