diff --git a/capabilities/antigravity/capability.json b/capabilities/antigravity/capability.json index c8da3c395..a7627d316 100644 --- a/capabilities/antigravity/capability.json +++ b/capabilities/antigravity/capability.json @@ -25,7 +25,16 @@ "converter": "convertClaudeCommandToAntigravitySkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAntigravitySkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", diff --git a/capabilities/augment/capability.json b/capabilities/augment/capability.json index d1a986635..a43dc6380 100644 --- a/capabilities/augment/capability.json +++ b/capabilities/augment/capability.json @@ -31,7 +31,24 @@ "converter": "convertClaudeCommandToAugmentSkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAugmentSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", diff --git a/capabilities/codebuddy/capability.json b/capabilities/codebuddy/capability.json index 4707a112a..2e357ce97 100644 --- a/capabilities/codebuddy/capability.json +++ b/capabilities/codebuddy/capability.json @@ -31,7 +31,24 @@ "converter": "convertClaudeCommandToCodebuddySkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddyCommand" + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddySkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", diff --git a/capabilities/codex/capability.json b/capabilities/codex/capability.json index 02d79a6d7..42460508f 100644 --- a/capabilities/codex/capability.json +++ b/capabilities/codex/capability.json @@ -23,7 +23,16 @@ "converter": "convertClaudeCommandToCodexSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodexSkill" + } + ] }, "commandStyle": "shell-var", "hooksSurface": "codex-hooks-json", diff --git a/capabilities/copilot/capability.json b/capabilities/copilot/capability.json index 475764178..659b1dbd5 100644 --- a/capabilities/copilot/capability.json +++ b/capabilities/copilot/capability.json @@ -23,7 +23,16 @@ "converter": "convertClaudeCommandToCopilotSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCopilotSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "copilot-inline", diff --git a/capabilities/cursor/capability.json b/capabilities/cursor/capability.json index 359d2efef..8ac974b60 100644 --- a/capabilities/cursor/capability.json +++ b/capabilities/cursor/capability.json @@ -31,7 +31,24 @@ "converter": "convertClaudeCommandToCursorCommand" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToCursorSkill" + }, + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCursorCommand" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "cursor-hooks-json", diff --git a/capabilities/gemini/capability.json b/capabilities/gemini/capability.json index 624a820fa..dac2fb18b 100644 --- a/capabilities/gemini/capability.json +++ b/capabilities/gemini/capability.json @@ -23,7 +23,16 @@ "converter": null } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "commands/gsd", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", diff --git a/capabilities/hermes/capability.json b/capabilities/hermes/capability.json index 7f2f63b84..b0140bb54 100644 --- a/capabilities/hermes/capability.json +++ b/capabilities/hermes/capability.json @@ -23,7 +23,16 @@ "converter": "convertClaudeCommandToClaudeSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills/gsd", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", diff --git a/capabilities/kilo/capability.json b/capabilities/kilo/capability.json index 98061f955..32136736c 100644 --- a/capabilities/kilo/capability.json +++ b/capabilities/kilo/capability.json @@ -36,7 +36,24 @@ "converter": "convertClaudeCommandToKiloSkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToKiloSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", diff --git a/capabilities/opencode/capability.json b/capabilities/opencode/capability.json index 4f8877c9f..79676ef2c 100644 --- a/capabilities/opencode/capability.json +++ b/capabilities/opencode/capability.json @@ -31,7 +31,24 @@ "converter": "convertClaudeCommandToOpencodeSkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToOpencodeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", diff --git a/capabilities/qwen/capability.json b/capabilities/qwen/capability.json index 859b73bde..15f5ed553 100644 --- a/capabilities/qwen/capability.json +++ b/capabilities/qwen/capability.json @@ -23,7 +23,16 @@ "converter": "convertClaudeCommandToClaudeSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", diff --git a/capabilities/trae/capability.json b/capabilities/trae/capability.json index 811a43a65..287723f84 100644 --- a/capabilities/trae/capability.json +++ b/capabilities/trae/capability.json @@ -23,7 +23,16 @@ "converter": "convertClaudeCommandToTraeSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToTraeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", diff --git a/capabilities/windsurf/capability.json b/capabilities/windsurf/capability.json index 7aec6c584..4cfeb0ee6 100644 --- a/capabilities/windsurf/capability.json +++ b/capabilities/windsurf/capability.json @@ -24,7 +24,16 @@ "converter": "convertClaudeCommandToWindsurfSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToWindsurfSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs index 24310f38f..f80139215 100644 --- a/gsd-core/bin/lib/capability-registry.cjs +++ b/gsd-core/bin/lib/capability-registry.cjs @@ -40,7 +40,16 @@ const capabilities = { "converter": "convertClaudeCommandToAntigravitySkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAntigravitySkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -111,7 +120,24 @@ const capabilities = { "converter": "convertClaudeCommandToAugmentSkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAugmentSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -243,7 +269,24 @@ const capabilities = { "converter": "convertClaudeCommandToCodebuddySkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddyCommand" + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddySkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -279,7 +322,16 @@ const capabilities = { "converter": "convertClaudeCommandToCodexSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodexSkill" + } + ] }, "commandStyle": "shell-var", "hooksSurface": "codex-hooks-json", @@ -316,7 +368,16 @@ const capabilities = { "converter": "convertClaudeCommandToCopilotSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCopilotSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "copilot-inline", @@ -359,7 +420,24 @@ const capabilities = { "converter": "convertClaudeCommandToCursorCommand" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToCursorSkill" + }, + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCursorCommand" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "cursor-hooks-json", @@ -395,7 +473,16 @@ const capabilities = { "converter": null } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "commands/gsd", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -461,7 +548,16 @@ const capabilities = { "converter": "convertClaudeCommandToClaudeSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills/gsd", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -540,7 +636,24 @@ const capabilities = { "converter": "convertClaudeCommandToKiloSkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToKiloSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", @@ -633,7 +746,24 @@ const capabilities = { "converter": "convertClaudeCommandToOpencodeSkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToOpencodeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", @@ -668,7 +798,16 @@ const capabilities = { "converter": "convertClaudeCommandToClaudeSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -704,7 +843,16 @@ const capabilities = { "converter": "convertClaudeCommandToTraeSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToTraeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", @@ -825,7 +973,16 @@ const capabilities = { "converter": "convertClaudeCommandToWindsurfSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToWindsurfSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", @@ -1038,7 +1195,16 @@ const runtimes = { "converter": "convertClaudeCommandToAntigravitySkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAntigravitySkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -1082,7 +1248,24 @@ const runtimes = { "converter": "convertClaudeCommandToAugmentSkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAugmentSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -1214,7 +1397,24 @@ const runtimes = { "converter": "convertClaudeCommandToCodebuddySkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddyCommand" + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddySkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -1250,7 +1450,16 @@ const runtimes = { "converter": "convertClaudeCommandToCodexSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodexSkill" + } + ] }, "commandStyle": "shell-var", "hooksSurface": "codex-hooks-json", @@ -1287,7 +1496,16 @@ const runtimes = { "converter": "convertClaudeCommandToCopilotSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCopilotSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "copilot-inline", @@ -1330,7 +1548,24 @@ const runtimes = { "converter": "convertClaudeCommandToCursorCommand" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToCursorSkill" + }, + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCursorCommand" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "cursor-hooks-json", @@ -1366,7 +1601,16 @@ const runtimes = { "converter": null } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "commands/gsd", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -1402,7 +1646,16 @@ const runtimes = { "converter": "convertClaudeCommandToClaudeSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills/gsd", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -1453,7 +1706,24 @@ const runtimes = { "converter": "convertClaudeCommandToKiloSkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToKiloSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", @@ -1546,7 +1816,24 @@ const runtimes = { "converter": "convertClaudeCommandToOpencodeSkill" } ], - "local": [] + "local": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToOpencodeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", @@ -1581,7 +1868,16 @@ const runtimes = { "converter": "convertClaudeCommandToClaudeSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "settings-json", @@ -1617,7 +1913,16 @@ const runtimes = { "converter": "convertClaudeCommandToTraeSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToTraeSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", @@ -1653,7 +1958,16 @@ const runtimes = { "converter": "convertClaudeCommandToWindsurfSkill" } ], - "local": [] + "local": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToWindsurfSkill" + } + ] }, "commandStyle": "slash-hyphen", "hooksSurface": "none", diff --git a/scripts/gen-capability-registry.cjs b/scripts/gen-capability-registry.cjs index 547a43a95..f2a10026b 100644 --- a/scripts/gen-capability-registry.cjs +++ b/scripts/gen-capability-registry.cjs @@ -581,21 +581,21 @@ function validateArtifactKindEntry(capId, entry, prefix) { errors.push(ctx + '.destSubpath must be a non-empty string'); } - // nesting — optional; if present must be in closed vocab - if (entry.nesting !== undefined) { - if (!VALID_ARTIFACT_NESTINGS.has(entry.nesting)) { - errors.push( - ctx + '.nesting must be one of: ' + [...VALID_ARTIFACT_NESTINGS].join(', ') + - ' (got: ' + JSON.stringify(entry.nesting) + ')', - ); - } + // nesting — required; must be in closed vocab (ADR-857 §5d: now drives install) + if (entry.nesting === undefined || entry.nesting === null) { + errors.push(ctx + '.nesting is required and must be one of: ' + [...VALID_ARTIFACT_NESTINGS].join(', ')); + } else if (!VALID_ARTIFACT_NESTINGS.has(entry.nesting)) { + errors.push( + ctx + '.nesting must be one of: ' + [...VALID_ARTIFACT_NESTINGS].join(', ') + + ' (got: ' + JSON.stringify(entry.nesting) + ')', + ); } - // prefix — optional; if present must be a string - if (entry.prefix !== undefined) { - if (typeof entry.prefix !== 'string') { - errors.push(ctx + '.prefix must be a string if present (got: ' + typeof entry.prefix + ')'); - } + // prefix — required; must be a string (may be empty string '') + if (entry.prefix === undefined || entry.prefix === null) { + errors.push(ctx + '.prefix is required (must be a string, may be empty)'); + } else if (typeof entry.prefix !== 'string') { + errors.push(ctx + '.prefix must be a string (got: ' + typeof entry.prefix + ')'); } // recursive — optional; if present must be a boolean @@ -605,11 +605,11 @@ function validateArtifactKindEntry(capId, entry, prefix) { } } - // converter — optional; if present must be a string or null (closed ConverterName enum in phase 5e) - if (entry.converter !== undefined) { - if (entry.converter !== null && typeof entry.converter !== 'string') { - errors.push(ctx + '.converter must be a string or null if present (got: ' + typeof entry.converter + ')'); - } + // converter — required; must be a string or null (closed ConverterName enum in phase 5e) + if (!Object.prototype.hasOwnProperty.call(entry, 'converter')) { + errors.push(ctx + '.converter is required (must be a string or null)'); + } else if (entry.converter !== null && typeof entry.converter !== 'string') { + errors.push(ctx + '.converter must be a string or null (got: ' + typeof entry.converter + ')'); } return errors; diff --git a/scripts/lint-test-file-count.allowlist.json b/scripts/lint-test-file-count.allowlist.json index 66ef758c8..1e579e369 100644 --- a/scripts/lint-test-file-count.allowlist.json +++ b/scripts/lint-test-file-count.allowlist.json @@ -79,6 +79,7 @@ }, "runtime-artifact-layout": { "files": [ + "runtime-artifact-layout-descriptor-drive.test.cjs", "runtime-artifact-layout-install-profiles.test.cjs", "runtime-artifact-layout-surface.test.cjs", "runtime-artifact-layout.test.cjs" diff --git a/src/runtime-artifact-layout.cts b/src/runtime-artifact-layout.cts index e05f75868..92feb0385 100644 --- a/src/runtime-artifact-layout.cts +++ b/src/runtime-artifact-layout.cts @@ -352,8 +352,72 @@ function convertedCommandsKind( // windsurf — docs.devin.ai/desktop/cascade/skills // codebuddy — codebuddy.ai/docs/cli/skills +// --------------------------------------------------------------------------- +// Descriptor-driven dispatch helpers (ADR-857 phase 5d) +// --------------------------------------------------------------------------- + +interface ArtifactKindDescriptor { + kind: string; + destSubpath: string; + prefix: string; + nesting: 'flat' | 'nested'; + recursive: boolean; + converter: string | null; +} + +interface ArtifactLayoutDescriptor { + global: ArtifactKindDescriptor[]; + local: ArtifactKindDescriptor[]; +} + +/** Lazy registry accessor — mirrors pattern from 5b/5c (runtime-homes.cts). */ +function getRegistry(): { runtimes: Record } { + return _require('./capability-registry.cjs') as { + runtimes: Record; + }; +} + +/** + * Map a single ArtifactKindDescriptor entry to an ArtifactKind using the + * matching builder function. Mirrors the hand-built calls in the old switch. + */ +function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, configDir: string): ArtifactKind { + const { kind, destSubpath, prefix, nesting, converter } = entry; + const nested = nesting === 'nested'; + + switch (kind) { + case 'commands': + if (converter == null) { + return commandsKind(destSubpath, prefix, configDir); + } + return convertedCommandsKind(destSubpath, prefix, converter, configDir); + + case 'agents': + return agentsKind(destSubpath, prefix, configDir); + + case 'skills': + if (converter == null) { + throw new TypeError( + `resolveRuntimeArtifactLayout: skills entry for '${runtime}' has converter=null (converter is required for skills)`, + ); + } + return skillsKind(destSubpath, prefix, converter, runtime, configDir, nested); + + case 'kimi-agents': + return kimiAgentsKind(destSubpath, prefix, configDir); + + default: + throw new TypeError( + `resolveRuntimeArtifactLayout: unknown kind '${kind}' in descriptor for runtime '${runtime}'`, + ); + } +} + /** * Resolve the artifact layout for a given runtime and config directory. + * + * ADR-857 phase 5d: driven by the capability-registry artifactLayout descriptor + * instead of a hardcoded switch statement. */ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope: 'local' | 'global' = 'global'): Layout { if (typeof configDir !== 'string' || configDir === '') { @@ -366,122 +430,15 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope: throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`); } - let kinds: ArtifactKind[]; - switch (runtime) { - case 'claude': - if (scope === 'local') { - kinds = [ - commandsKind('commands/gsd', 'gsd-', configDir), - agentsKind('agents', 'gsd-', configDir), - ]; - } else { - kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'claude', configDir)]; - } - break; - - case 'cursor': - // Cursor 1.6+ supports two artifact surfaces: - // 1. skills/gsd-/SKILL.md — rich skills with frontmatter + adapter header - // 2. commands/gsd-.md — plain markdown slash commands (no frontmatter) - // accessed via '/' in the Agent input (#785) - kinds = [ - skillsKind('skills', 'gsd-', 'convertClaudeCommandToCursorSkill', 'cursor', configDir), - convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCursorCommand', configDir), - ]; - break; - - case 'gemini': - kinds = [commandsKind('commands/gsd', 'gsd-', configDir)]; - break; - - case 'codex': - kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodexSkill', 'codex', configDir)]; - break; - - case 'copilot': - kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToCopilotSkill', 'copilot', configDir)]; - break; - - case 'antigravity': - kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToAntigravitySkill', 'antigravity', configDir, true /* #69 nested */)]; - break; - - case 'windsurf': - kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToWindsurfSkill', 'windsurf', configDir)]; - break; - - case 'augment': - kinds = [ - commandsKind('commands', 'gsd-', configDir), - skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir, true /* #69 nested */), - ]; - break; - - case 'trae': - kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToTraeSkill', 'trae', configDir, true /* #69 nested */)]; - break; - - case 'qwen': - kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'qwen', configDir, true /* #69 nested */)]; - break; - - case 'hermes': - // #947: restore canonical gsd- prefix — skills land at skills/gsd/gsd-/SKILL.md - // and dispatch as /gsd-, consistent with every other runtime. - // The skills/gsd/ category bucket (introduced by #2841) is retained. - // Prior bare-stem layout (prefix='') used by #3664 is reversed here. - kinds = [skillsKind('skills/gsd', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'hermes', configDir, true /* #69 nested */)]; - break; - - case 'codebuddy': - // CodeBuddy (Tencent) reads two user-level surfaces (codebuddy.ai/docs/cli): - // 1. commands/gsd-.md — slash commands shown in the '/' menu (#789) - // 2. skills/gsd-/SKILL.md — model-invocable skills, emitted with - // user-invocable:false so they stay OUT of '/' (the commands surface is - // the sole '/' entry point) — avoids a duplicated /gsd-* per workflow. - // Subagents (~/.codebuddy/agents/) are already emitted by the generic agents - // block in bin/install.js; MCP is excluded (gsd ships no MCP server). - kinds = [ - convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCodebuddyCommand', configDir), - skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodebuddySkill', 'codebuddy', configDir), - ]; - break; - - case 'cline': - kinds = scope === 'global' ? [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClineSkill', 'cline', configDir, true /* #69 nested */)] : []; - break; - - case 'kimi': - kinds = scope === 'global' - ? [ - skillsKind('skills', 'gsd-', 'convertClaudeCommandToKimiSkill', 'kimi', configDir), - kimiAgentsKind('agents', 'gsd', configDir), - ] - : []; - break; - - case 'opencode': - // OpenCode reads flat slash commands from command/ and on-demand skills - // from skills//SKILL.md (https://opencode.ai/docs/skills). Emit both. - kinds = [ - commandsKind('command', 'gsd-', configDir), - skillsKind('skills', 'gsd-', 'convertClaudeCommandToOpencodeSkill', 'opencode', configDir), - ]; - break; - - case 'kilo': - // Kilo derives from OpenCode and shares the skills//SKILL.md layout - // (https://kilo.ai/docs/customize/skills). Emit flat commands + skills. - kinds = [ - commandsKind('command', 'gsd-', configDir), - skillsKind('skills', 'gsd-', 'convertClaudeCommandToKiloSkill', 'kilo', configDir), - ]; - break; - - default: - throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`); + const desc = getRegistry().runtimes[runtime]?.runtime?.artifactLayout; + if (!desc) { + // Runtime is in ALLOWED_RUNTIMES but has no descriptor — reproduce old default: throw. + throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`); } + const entries: ArtifactKindDescriptor[] = desc[scope] ?? []; + const kinds: ArtifactKind[] = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir)); + return { runtime, configDir, scope, kinds }; } diff --git a/tests/runtime-artifact-layout-descriptor-drive.test.cjs b/tests/runtime-artifact-layout-descriptor-drive.test.cjs new file mode 100644 index 000000000..5c779a950 --- /dev/null +++ b/tests/runtime-artifact-layout-descriptor-drive.test.cjs @@ -0,0 +1,346 @@ +'use strict'; + +/** + * Equivalence proof for ADR-857 phase 5d: descriptor-driven resolveRuntimeArtifactLayout. + * + * For every runtime in the 16-entry capability registry × {global, local} scopes, + * this test asserts that: + * 1. kind.kind, kind.destSubpath, kind.prefix are byte-identical to the STEP-0 + * golden captured from the old switch() before any edits. + * 2. typeof kind.stage === 'function' for every kind. + * 3. layout.runtime === runtime, layout.configDir === configDir, layout.scope === scope. + * + * SCOPE-FALL-THROUGH NOTE: + * The old switch() had no scope branches for 13 runtimes (cursor, gemini, codex, + * copilot, antigravity, windsurf, augment, trae, qwen, hermes, codebuddy, opencode, + * kilo), meaning scope='local' returned the same kinds as scope='global'. The 5a + * descriptors incorrectly set local:[] for those runtimes, causing 31 local-install + * test regressions. The 5b backfill sets local == global for these 13, restoring + * the old switch's scope-agnostic behaviour. + * + * For runtimes that had explicit scope branches in the old switch + * (claude: distinct local=commands+agents; cline: local=[]; kimi: local=[]), + * the STEP-0 golden matches the descriptor exactly and is left unchanged. + * + * Unknown runtime case: + * ALLOWED_RUNTIMES guard throws TypeError BEFORE the descriptor lookup, + * so unknown runtimes still throw: + * TypeError: Unknown runtime: 'grok' — add to runtime-artifact-layout.cjs table + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); +const { resolveRuntimeArtifactLayout } = require( + path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs'), +); + +const FAKE_DIR = '/tmp/fake-config-dir-dd'; + +// ── STEP-0 golden (captured from switch BEFORE edits) ──────────────────────── +// Format: { kind, destSubpath, prefix } for each entry in kinds[]. +// 'function' means we assert typeof kind.stage === 'function'. + +const GOLDEN = { + // ── claude ────────────────────────────────────────────────────────────────── + 'claude/global': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'claude/local': [ + { kind: 'commands', destSubpath: 'commands/gsd', prefix: 'gsd-' }, + { kind: 'agents', destSubpath: 'agents', prefix: 'gsd-' }, + ], + + // ── cursor ─────────────────────────────────────────────────────────────────── + // Old switch: BOTH scopes returned [skills, commands] (no scope branch). + // 5b backfill: local == global. + 'cursor/global': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + { kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' }, + ], + 'cursor/local': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + { kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' }, + ], + + // ── gemini ─────────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'gemini/global': [ + { kind: 'commands', destSubpath: 'commands/gsd', prefix: 'gsd-' }, + ], + 'gemini/local': [ + { kind: 'commands', destSubpath: 'commands/gsd', prefix: 'gsd-' }, + ], + + // ── codex ──────────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'codex/global': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'codex/local': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + + // ── copilot ────────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'copilot/global': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'copilot/local': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + + // ── antigravity ────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'antigravity/global': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'antigravity/local': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + + // ── windsurf ───────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'windsurf/global': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'windsurf/local': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + + // ── augment ────────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'augment/global': [ + { kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' }, + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'augment/local': [ + { kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' }, + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + + // ── trae ───────────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'trae/global': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'trae/local': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + + // ── qwen ───────────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'qwen/global': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'qwen/local': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + + // ── hermes ─────────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'hermes/global': [ + { kind: 'skills', destSubpath: 'skills/gsd', prefix: 'gsd-' }, + ], + 'hermes/local': [ + { kind: 'skills', destSubpath: 'skills/gsd', prefix: 'gsd-' }, + ], + + // ── codebuddy ──────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'codebuddy/global': [ + { kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' }, + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'codebuddy/local': [ + { kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' }, + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + + // ── cline ──────────────────────────────────────────────────────────────────── + // Old switch: scope='global' → [skills]; scope='local' → []. Matches descriptor. + 'cline/global': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'cline/local': [], + + // ── kimi ───────────────────────────────────────────────────────────────────── + // Old switch: scope='global' → [skills, kimi-agents]; scope='local' → []. Matches descriptor. + 'kimi/global': [ + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + { kind: 'kimi-agents', destSubpath: 'agents', prefix: 'gsd' }, + ], + 'kimi/local': [], + + // ── opencode ───────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'opencode/global': [ + { kind: 'commands', destSubpath: 'command', prefix: 'gsd-' }, + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'opencode/local': [ + { kind: 'commands', destSubpath: 'command', prefix: 'gsd-' }, + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + + // ── kilo ───────────────────────────────────────────────────────────────────── + // Old switch: no scope branch → local == global. 5b backfill restores this. + 'kilo/global': [ + { kind: 'commands', destSubpath: 'command', prefix: 'gsd-' }, + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], + 'kilo/local': [ + { kind: 'commands', destSubpath: 'command', prefix: 'gsd-' }, + { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + ], +}; + +// ── Parametrized tests ──────────────────────────────────────────────────────── + +const RUNTIMES = [ + 'claude', 'cursor', 'gemini', 'codex', 'copilot', + 'antigravity', 'windsurf', 'augment', 'trae', 'qwen', + 'hermes', 'codebuddy', 'cline', 'kimi', 'opencode', 'kilo', +]; + +for (const runtime of RUNTIMES) { + for (const scope of ['global', 'local']) { + const key = `${runtime}/${scope}`; + const expected = GOLDEN[key]; + assert.ok( + expected !== undefined, + `GOLDEN missing entry for ${key} — update the golden table`, + ); + + describe(`resolveRuntimeArtifactLayout — ${runtime} ${scope} (descriptor-driven)`, () => { + test(`kinds array matches STEP-0 golden for ${runtime}/${scope}`, () => { + const layout = resolveRuntimeArtifactLayout(runtime, FAKE_DIR, /** @type {'local'|'global'} */ (scope)); + + // Structural fields + assert.strictEqual(layout.runtime, runtime, 'layout.runtime'); + assert.strictEqual(layout.configDir, FAKE_DIR, 'layout.configDir'); + assert.strictEqual(layout.scope, scope, 'layout.scope'); + + // kinds length matches golden + assert.strictEqual( + layout.kinds.length, + expected.length, + `kinds.length for ${key}: expected ${expected.length}, got ${layout.kinds.length}`, + ); + + // Per-kind field checks + for (let i = 0; i < expected.length; i++) { + const actual = layout.kinds[i]; + const exp = expected[i]; + + assert.strictEqual( + actual.kind, + exp.kind, + `kinds[${i}].kind for ${key}`, + ); + assert.strictEqual( + actual.destSubpath, + exp.destSubpath, + `kinds[${i}].destSubpath for ${key}`, + ); + assert.strictEqual( + actual.prefix, + exp.prefix, + `kinds[${i}].prefix for ${key}`, + ); + assert.strictEqual( + typeof actual.stage, + 'function', + `kinds[${i}].stage must be a function for ${key}`, + ); + } + }); + }); + } +} + +// ── Unknown runtime ─────────────────────────────────────────────────────────── +// ALLOWED_RUNTIMES guard fires BEFORE descriptor lookup — reproduces old behaviour. + +describe('resolveRuntimeArtifactLayout — unknown runtime (descriptor-driven)', () => { + test('throws TypeError for grok (not in ALLOWED_RUNTIMES)', () => { + assert.throws( + () => resolveRuntimeArtifactLayout('grok', FAKE_DIR, 'global'), + (err) => { + assert.ok(err instanceof TypeError, 'must be TypeError'); + assert.ok( + err.message.includes("Unknown runtime: 'grok'"), + `message must include "Unknown runtime: 'grok'" — got: ${err.message}`, + ); + return true; + }, + ); + }); + + test('throws TypeError for an arbitrary unknown string', () => { + assert.throws( + () => resolveRuntimeArtifactLayout('notaruntime', FAKE_DIR, 'global'), + (err) => { + assert.ok(err instanceof TypeError); + assert.ok(err.message.includes("Unknown runtime: 'notaruntime'")); + return true; + }, + ); + }); +}); + +// ── Scope default ───────────────────────────────────────────────────────────── +// resolveRuntimeArtifactLayout(runtime, configDir) with no scope arg → 'global'. + +describe('resolveRuntimeArtifactLayout — scope defaults to global (descriptor-driven)', () => { + test('omitting scope yields global layout for claude', () => { + const withDefault = resolveRuntimeArtifactLayout('claude', FAKE_DIR); + const withExplicit = resolveRuntimeArtifactLayout('claude', FAKE_DIR, 'global'); + assert.strictEqual(withDefault.scope, 'global', 'default scope must be "global"'); + assert.strictEqual(withDefault.kinds.length, withExplicit.kinds.length); + for (let i = 0; i < withDefault.kinds.length; i++) { + assert.strictEqual(withDefault.kinds[i].kind, withExplicit.kinds[i].kind); + assert.strictEqual(withDefault.kinds[i].destSubpath, withExplicit.kinds[i].destSubpath); + assert.strictEqual(withDefault.kinds[i].prefix, withExplicit.kinds[i].prefix); + } + }); + + test('omitting scope yields global layout for kimi (2 kinds)', () => { + const layout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR); + assert.strictEqual(layout.scope, 'global'); + assert.strictEqual(layout.kinds.length, 2); + assert.strictEqual(layout.kinds[0].kind, 'skills'); + assert.strictEqual(layout.kinds[1].kind, 'kimi-agents'); + }); +}); + +// ── Non-vacuous check: verify at least one multi-kind runtime ───────────────── + +describe('resolveRuntimeArtifactLayout — multi-kind runtimes non-vacuous (descriptor-driven)', () => { + test('augment global returns 2 kinds (commands + skills)', () => { + const layout = resolveRuntimeArtifactLayout('augment', FAKE_DIR, 'global'); + assert.strictEqual(layout.kinds.length, 2); + assert.strictEqual(layout.kinds[0].kind, 'commands'); + assert.strictEqual(layout.kinds[1].kind, 'skills'); + assert.strictEqual(typeof layout.kinds[0].stage, 'function'); + assert.strictEqual(typeof layout.kinds[1].stage, 'function'); + }); + + test('kimi global returns skills then kimi-agents', () => { + const layout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR, 'global'); + assert.strictEqual(layout.kinds.length, 2); + assert.strictEqual(layout.kinds[0].kind, 'skills'); + assert.strictEqual(layout.kinds[1].kind, 'kimi-agents'); + assert.strictEqual(layout.kinds[1].destSubpath, 'agents'); + assert.strictEqual(layout.kinds[1].prefix, 'gsd'); + }); + + test('codebuddy global returns commands then skills', () => { + const layout = resolveRuntimeArtifactLayout('codebuddy', FAKE_DIR, 'global'); + assert.strictEqual(layout.kinds.length, 2); + assert.strictEqual(layout.kinds[0].kind, 'commands'); + assert.strictEqual(layout.kinds[1].kind, 'skills'); + }); +});