diff --git a/capabilities/antigravity/capability.json b/capabilities/antigravity/capability.json new file mode 100644 index 000000000..c8da3c395 --- /dev/null +++ b/capabilities/antigravity/capability.json @@ -0,0 +1,36 @@ +{ + "id": "antigravity", + "role": "runtime", + "title": "Antigravity", + "description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; nested skill layout; tier-1 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home-nested", + "name": "antigravity", + "parent": ".gemini", + "env": ["ANTIGRAVITY_CONFIG_DIR"], + "probe": ["antigravity", "antigravity-ide", "antigravity-cli"] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAntigravitySkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "gemini", + "sandboxTier": "none", + "supportTier": 1 + } +} diff --git a/capabilities/augment/capability.json b/capabilities/augment/capability.json new file mode 100644 index 000000000..d1a986635 --- /dev/null +++ b/capabilities/augment/capability.json @@ -0,0 +1,42 @@ +{ + "id": "augment", + "role": "runtime", + "title": "Augment Code", + "description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".augment", + "env": ["AUGMENT_CONFIG_DIR"] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAugmentSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/claude/capability.json b/capabilities/claude/capability.json new file mode 100644 index 000000000..77e22ba3b --- /dev/null +++ b/capabilities/claude/capability.json @@ -0,0 +1,51 @@ +{ + "id": "claude", + "role": "runtime", + "title": "Claude Code", + "description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".claude", + "env": ["CLAUDE_CONFIG_DIR"] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ], + "local": [ + { + "kind": "commands", + "destSubpath": "commands/gsd", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "agents", + "destSubpath": "agents", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + } + ] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 1 + } +} diff --git a/capabilities/cline/capability.json b/capabilities/cline/capability.json new file mode 100644 index 000000000..fb21cb700 --- /dev/null +++ b/capabilities/cline/capability.json @@ -0,0 +1,33 @@ +{ + "id": "cline", + "role": "runtime", + "title": "Cline", + "description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".cline", + "env": ["CLINE_CONFIG_DIR"] + }, + "configFormat": "markdown-dir", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClineSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "cline-rules", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/codebuddy/capability.json b/capabilities/codebuddy/capability.json new file mode 100644 index 000000000..4707a112a --- /dev/null +++ b/capabilities/codebuddy/capability.json @@ -0,0 +1,42 @@ +{ + "id": "codebuddy", + "role": "runtime", + "title": "CodeBuddy", + "description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".codebuddy", + "env": ["CODEBUDDY_CONFIG_DIR"] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddyCommand" + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddySkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/codex/capability.json b/capabilities/codex/capability.json new file mode 100644 index 000000000..02d79a6d7 --- /dev/null +++ b/capabilities/codex/capability.json @@ -0,0 +1,34 @@ +{ + "id": "codex", + "role": "runtime", + "title": "OpenAI Codex CLI", + "description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".codex", + "env": ["CODEX_HOME"] + }, + "configFormat": "toml", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodexSkill" + } + ], + "local": [] + }, + "commandStyle": "shell-var", + "hooksSurface": "codex-hooks-json", + "hookEvents": "claude", + "sandboxTier": "codex-agent-sandbox", + "supportTier": 1 + } +} diff --git a/capabilities/copilot/capability.json b/capabilities/copilot/capability.json new file mode 100644 index 000000000..475764178 --- /dev/null +++ b/capabilities/copilot/capability.json @@ -0,0 +1,33 @@ +{ + "id": "copilot", + "role": "runtime", + "title": "GitHub Copilot", + "description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".copilot", + "env": ["COPILOT_CONFIG_DIR", "COPILOT_HOME"] + }, + "configFormat": "markdown", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCopilotSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "copilot-inline", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/cursor/capability.json b/capabilities/cursor/capability.json new file mode 100644 index 000000000..359d2efef --- /dev/null +++ b/capabilities/cursor/capability.json @@ -0,0 +1,42 @@ +{ + "id": "cursor", + "role": "runtime", + "title": "Cursor", + "description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".cursor", + "env": ["CURSOR_CONFIG_DIR"] + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToCursorSkill" + }, + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCursorCommand" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "cursor-hooks-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/gemini/capability.json b/capabilities/gemini/capability.json new file mode 100644 index 000000000..624a820fa --- /dev/null +++ b/capabilities/gemini/capability.json @@ -0,0 +1,34 @@ +{ + "id": "gemini", + "role": "runtime", + "title": "Gemini CLI", + "description": "Google Gemini CLI — commands-only artifact layout (TOML); Gemini hook event dialect; settings-json hook surface; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".gemini", + "env": ["GEMINI_CONFIG_DIR"] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "commands/gsd", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "gemini", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/hermes/capability.json b/capabilities/hermes/capability.json new file mode 100644 index 000000000..7f2f63b84 --- /dev/null +++ b/capabilities/hermes/capability.json @@ -0,0 +1,34 @@ +{ + "id": "hermes", + "role": "runtime", + "title": "Hermes Agent", + "description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".hermes", + "env": ["HERMES_HOME"] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills/gsd", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/kilo/capability.json b/capabilities/kilo/capability.json new file mode 100644 index 000000000..98061f955 --- /dev/null +++ b/capabilities/kilo/capability.json @@ -0,0 +1,46 @@ +{ + "id": "kilo", + "role": "runtime", + "title": "Kilo Code", + "description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "xdg", + "name": "kilo", + "env": ["KILO_CONFIG_DIR", "KILO_CONFIG", "XDG_CONFIG_HOME"], + "skillsHome": { + "kind": "dot-home", + "name": ".kilo", + "env": [] + } + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToKiloSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/kimi/capability.json b/capabilities/kimi/capability.json new file mode 100644 index 000000000..73f06ecb7 --- /dev/null +++ b/capabilities/kimi/capability.json @@ -0,0 +1,43 @@ +{ + "id": "kimi", + "role": "runtime", + "title": "Kimi CLI", + "description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "generic-agents-root", + "name": "agents", + "env": ["KIMI_CONFIG_DIR"], + "probe": ["~/.config/agents", "~/.agents"], + "probeExists": "skills" + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToKimiSkill" + }, + { + "kind": "kimi-agents", + "destSubpath": "agents", + "prefix": "gsd", + "nesting": "flat", + "recursive": false, + "converter": null + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/opencode/capability.json b/capabilities/opencode/capability.json new file mode 100644 index 000000000..4f8877c9f --- /dev/null +++ b/capabilities/opencode/capability.json @@ -0,0 +1,41 @@ +{ + "id": "opencode", + "role": "runtime", + "title": "OpenCode", + "description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "xdg", + "name": "opencode", + "env": ["OPENCODE_CONFIG_DIR", "OPENCODE_CONFIG", "XDG_CONFIG_HOME"] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToOpencodeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/qwen/capability.json b/capabilities/qwen/capability.json new file mode 100644 index 000000000..859b73bde --- /dev/null +++ b/capabilities/qwen/capability.json @@ -0,0 +1,34 @@ +{ + "id": "qwen", + "role": "runtime", + "title": "Qwen Code", + "description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".qwen", + "env": ["QWEN_CONFIG_DIR"] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/trae/capability.json b/capabilities/trae/capability.json new file mode 100644 index 000000000..811a43a65 --- /dev/null +++ b/capabilities/trae/capability.json @@ -0,0 +1,33 @@ +{ + "id": "trae", + "role": "runtime", + "title": "Trae IDE", + "description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".trae", + "env": ["TRAE_CONFIG_DIR"] + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToTraeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/capabilities/windsurf/capability.json b/capabilities/windsurf/capability.json new file mode 100644 index 000000000..7aec6c584 --- /dev/null +++ b/capabilities/windsurf/capability.json @@ -0,0 +1,34 @@ +{ + "id": "windsurf", + "role": "runtime", + "title": "Windsurf", + "description": "Windsurf (Codeium) — nested under ~/.codeium/windsurf; skills-only artifact layout; no hook surface; no hook events; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home-nested", + "name": "windsurf", + "parent": ".codeium", + "env": ["WINDSURF_CONFIG_DIR"] + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToWindsurfSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } +} diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index 9018a6279..fee57e5d0 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -1,5 +1,5 @@ { - "generated": "2026-06-10", + "generated": "2026-06-11", "families": { "agents": [ "gsd-advisor-researcher", diff --git a/docs/adr/1016-runtime-capability-descriptor.md b/docs/adr/1016-runtime-capability-descriptor.md index 7015fe2ae..1020cc009 100644 --- a/docs/adr/1016-runtime-capability-descriptor.md +++ b/docs/adr/1016-runtime-capability-descriptor.md @@ -32,8 +32,9 @@ configHome: { kind: 'dot-home' | 'dot-home-nested' | 'xdg' | 'generic-agents-root', name: string, // e.g. 'claude', 'opencode' parent?: string, // for dot-home-nested: e.g. '.gemini' (antigravity), '.codeium' (windsurf) - env: string[], // ordered override env vars, e.g. ['CLAUDE_CONFIG_DIR'] + env: string[], // ordered override env vars, e.g. ['CLAUDE_CONFIG_DIR'] (required; may be empty) probe?: string[], // ordered candidate subpaths; first existing wins (antigravity, kimi) + probeExists?: string, // if set, select the first probe candidate where / exists (kimi: 'skills') skillsHome?: { kind, name, ... } // override when the skills dir ≠ config dir (kilo only) } ``` @@ -82,7 +83,7 @@ hookEvents?: 'claude' // SessionStart/PreToolUse/PostToolUse | 'opencode-subset' // settings-json surface but SessionStart/PostToolUse skipped ``` -`settings-json` covers claude, gemini, antigravity, augment, qwen, hermes, codebuddy, opencode; the event-name and registration-subset differences ride on `hookEvents` rather than splitting the surface enum. `none` = windsurf, trae, kimi, kilo. +`settings-json` covers claude, gemini, antigravity, augment, qwen, hermes, codebuddy; the event-name and registration-subset differences ride on `hookEvents` rather than splitting the surface enum. `none` = windsurf, trae, kimi, kilo, opencode — these five runtimes register **zero** managed lifecycle hooks today; `opencode-subset` is reserved ADR vocabulary with no current consumer (opencode and kilo write a `settings.json` for config/permissions, which is the `configFormat` axis, not the hook-registration axis). ### 6. `sandboxTier` — the agent-sandbox primitive (closed enum) diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs index 49329b265..24310f38f 100644 --- a/gsd-core/bin/lib/capability-registry.cjs +++ b/gsd-core/bin/lib/capability-registry.cjs @@ -7,6 +7,48 @@ */ const capabilities = { + "antigravity": { + "id": "antigravity", + "role": "runtime", + "title": "Antigravity", + "description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; nested skill layout; tier-1 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home-nested", + "name": "antigravity", + "parent": ".gemini", + "env": [ + "ANTIGRAVITY_CONFIG_DIR" + ], + "probe": [ + "antigravity", + "antigravity-ide", + "antigravity-cli" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAntigravitySkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "gemini", + "sandboxTier": "none", + "supportTier": 1 + } + }, "audit": { "id": "audit", "role": "feature", @@ -34,6 +76,334 @@ const capabilities = { "contributions": [], "gates": [] }, + "augment": { + "id": "augment", + "role": "runtime", + "title": "Augment Code", + "description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".augment", + "env": [ + "AUGMENT_CONFIG_DIR" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAugmentSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "claude": { + "id": "claude", + "role": "runtime", + "title": "Claude Code", + "description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".claude", + "env": [ + "CLAUDE_CONFIG_DIR" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ], + "local": [ + { + "kind": "commands", + "destSubpath": "commands/gsd", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "agents", + "destSubpath": "agents", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + } + ] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 1 + } + }, + "cline": { + "id": "cline", + "role": "runtime", + "title": "Cline", + "description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".cline", + "env": [ + "CLINE_CONFIG_DIR" + ] + }, + "configFormat": "markdown-dir", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClineSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "cline-rules", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "codebuddy": { + "id": "codebuddy", + "role": "runtime", + "title": "CodeBuddy", + "description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".codebuddy", + "env": [ + "CODEBUDDY_CONFIG_DIR" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddyCommand" + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddySkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "codex": { + "id": "codex", + "role": "runtime", + "title": "OpenAI Codex CLI", + "description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".codex", + "env": [ + "CODEX_HOME" + ] + }, + "configFormat": "toml", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodexSkill" + } + ], + "local": [] + }, + "commandStyle": "shell-var", + "hooksSurface": "codex-hooks-json", + "hookEvents": "claude", + "sandboxTier": "codex-agent-sandbox", + "supportTier": 1 + } + }, + "copilot": { + "id": "copilot", + "role": "runtime", + "title": "GitHub Copilot", + "description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".copilot", + "env": [ + "COPILOT_CONFIG_DIR", + "COPILOT_HOME" + ] + }, + "configFormat": "markdown", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCopilotSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "copilot-inline", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "cursor": { + "id": "cursor", + "role": "runtime", + "title": "Cursor", + "description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".cursor", + "env": [ + "CURSOR_CONFIG_DIR" + ] + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToCursorSkill" + }, + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCursorCommand" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "cursor-hooks-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "gemini": { + "id": "gemini", + "role": "runtime", + "title": "Gemini CLI", + "description": "Google Gemini CLI — commands-only artifact layout (TOML); Gemini hook event dialect; settings-json hook surface; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".gemini", + "env": [ + "GEMINI_CONFIG_DIR" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "commands/gsd", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "gemini", + "sandboxTier": "none", + "supportTier": 2 + } + }, "graphify": { "id": "graphify", "role": "feature", @@ -64,6 +434,42 @@ const capabilities = { "contributions": [], "gates": [] }, + "hermes": { + "id": "hermes", + "role": "runtime", + "title": "Hermes Agent", + "description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".hermes", + "env": [ + "HERMES_HOME" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills/gsd", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } + }, "intel": { "id": "intel", "role": "feature", @@ -92,6 +498,220 @@ const capabilities = { "contributions": [], "gates": [] }, + "kilo": { + "id": "kilo", + "role": "runtime", + "title": "Kilo Code", + "description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "xdg", + "name": "kilo", + "env": [ + "KILO_CONFIG_DIR", + "KILO_CONFIG", + "XDG_CONFIG_HOME" + ], + "skillsHome": { + "kind": "dot-home", + "name": ".kilo", + "env": [] + } + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToKiloSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "kimi": { + "id": "kimi", + "role": "runtime", + "title": "Kimi CLI", + "description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "generic-agents-root", + "name": "agents", + "env": [ + "KIMI_CONFIG_DIR" + ], + "probe": [ + "~/.config/agents", + "~/.agents" + ], + "probeExists": "skills" + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToKimiSkill" + }, + { + "kind": "kimi-agents", + "destSubpath": "agents", + "prefix": "gsd", + "nesting": "flat", + "recursive": false, + "converter": null + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "opencode": { + "id": "opencode", + "role": "runtime", + "title": "OpenCode", + "description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "xdg", + "name": "opencode", + "env": [ + "OPENCODE_CONFIG_DIR", + "OPENCODE_CONFIG", + "XDG_CONFIG_HOME" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToOpencodeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "qwen": { + "id": "qwen", + "role": "runtime", + "title": "Qwen Code", + "description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".qwen", + "env": [ + "QWEN_CONFIG_DIR" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "trae": { + "id": "trae", + "role": "runtime", + "title": "Trae IDE", + "description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".trae", + "env": [ + "TRAE_CONFIG_DIR" + ] + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToTraeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } + }, "ui": { "id": "ui", "role": "feature", @@ -176,6 +796,42 @@ const capabilities = { "onError": "halt" } ] + }, + "windsurf": { + "id": "windsurf", + "role": "runtime", + "title": "Windsurf", + "description": "Windsurf (Codeium) — nested under ~/.codeium/windsurf; skills-only artifact layout; no hook surface; no hook events; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home-nested", + "name": "windsurf", + "parent": ".codeium", + "env": [ + "WINDSURF_CONFIG_DIR" + ] + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToWindsurfSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } } }; @@ -348,7 +1004,664 @@ const configSchema = { } }; -const runtimes = {}; +const runtimes = { + "antigravity": { + "id": "antigravity", + "role": "runtime", + "title": "Antigravity", + "description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; nested skill layout; tier-1 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home-nested", + "name": "antigravity", + "parent": ".gemini", + "env": [ + "ANTIGRAVITY_CONFIG_DIR" + ], + "probe": [ + "antigravity", + "antigravity-ide", + "antigravity-cli" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAntigravitySkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "gemini", + "sandboxTier": "none", + "supportTier": 1 + } + }, + "augment": { + "id": "augment", + "role": "runtime", + "title": "Augment Code", + "description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".augment", + "env": [ + "AUGMENT_CONFIG_DIR" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToAugmentSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "claude": { + "id": "claude", + "role": "runtime", + "title": "Claude Code", + "description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".claude", + "env": [ + "CLAUDE_CONFIG_DIR" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ], + "local": [ + { + "kind": "commands", + "destSubpath": "commands/gsd", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "agents", + "destSubpath": "agents", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + } + ] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 1 + } + }, + "cline": { + "id": "cline", + "role": "runtime", + "title": "Cline", + "description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".cline", + "env": [ + "CLINE_CONFIG_DIR" + ] + }, + "configFormat": "markdown-dir", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClineSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "cline-rules", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "codebuddy": { + "id": "codebuddy", + "role": "runtime", + "title": "CodeBuddy", + "description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".codebuddy", + "env": [ + "CODEBUDDY_CONFIG_DIR" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddyCommand" + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodebuddySkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "codex": { + "id": "codex", + "role": "runtime", + "title": "OpenAI Codex CLI", + "description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".codex", + "env": [ + "CODEX_HOME" + ] + }, + "configFormat": "toml", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCodexSkill" + } + ], + "local": [] + }, + "commandStyle": "shell-var", + "hooksSurface": "codex-hooks-json", + "hookEvents": "claude", + "sandboxTier": "codex-agent-sandbox", + "supportTier": 1 + } + }, + "copilot": { + "id": "copilot", + "role": "runtime", + "title": "GitHub Copilot", + "description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".copilot", + "env": [ + "COPILOT_CONFIG_DIR", + "COPILOT_HOME" + ] + }, + "configFormat": "markdown", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCopilotSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "copilot-inline", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "cursor": { + "id": "cursor", + "role": "runtime", + "title": "Cursor", + "description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".cursor", + "env": [ + "CURSOR_CONFIG_DIR" + ] + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToCursorSkill" + }, + { + "kind": "commands", + "destSubpath": "commands", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToCursorCommand" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "cursor-hooks-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "gemini": { + "id": "gemini", + "role": "runtime", + "title": "Gemini CLI", + "description": "Google Gemini CLI — commands-only artifact layout (TOML); Gemini hook event dialect; settings-json hook surface; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".gemini", + "env": [ + "GEMINI_CONFIG_DIR" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "commands/gsd", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "gemini", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "hermes": { + "id": "hermes", + "role": "runtime", + "title": "Hermes Agent", + "description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".hermes", + "env": [ + "HERMES_HOME" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills/gsd", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "kilo": { + "id": "kilo", + "role": "runtime", + "title": "Kilo Code", + "description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "xdg", + "name": "kilo", + "env": [ + "KILO_CONFIG_DIR", + "KILO_CONFIG", + "XDG_CONFIG_HOME" + ], + "skillsHome": { + "kind": "dot-home", + "name": ".kilo", + "env": [] + } + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToKiloSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "kimi": { + "id": "kimi", + "role": "runtime", + "title": "Kimi CLI", + "description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "generic-agents-root", + "name": "agents", + "env": [ + "KIMI_CONFIG_DIR" + ], + "probe": [ + "~/.config/agents", + "~/.agents" + ], + "probeExists": "skills" + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToKimiSkill" + }, + { + "kind": "kimi-agents", + "destSubpath": "agents", + "prefix": "gsd", + "nesting": "flat", + "recursive": false, + "converter": null + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "opencode": { + "id": "opencode", + "role": "runtime", + "title": "OpenCode", + "description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "xdg", + "name": "opencode", + "env": [ + "OPENCODE_CONFIG_DIR", + "OPENCODE_CONFIG", + "XDG_CONFIG_HOME" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "commands", + "destSubpath": "command", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": null + }, + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": true, + "converter": "convertClaudeCommandToOpencodeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "qwen": { + "id": "qwen", + "role": "runtime", + "title": "Qwen Code", + "description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".qwen", + "env": [ + "QWEN_CONFIG_DIR" + ] + }, + "configFormat": "settings-json", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToClaudeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "settings-json", + "hookEvents": "claude", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "trae": { + "id": "trae", + "role": "runtime", + "title": "Trae IDE", + "description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home", + "name": ".trae", + "env": [ + "TRAE_CONFIG_DIR" + ] + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "nested", + "recursive": false, + "converter": "convertClaudeCommandToTraeSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } + }, + "windsurf": { + "id": "windsurf", + "role": "runtime", + "title": "Windsurf", + "description": "Windsurf (Codeium) — nested under ~/.codeium/windsurf; skills-only artifact layout; no hook surface; no hook events; tier-2 support.", + "tier": "core", + "requires": [], + "runtime": { + "configHome": { + "kind": "dot-home-nested", + "name": "windsurf", + "parent": ".codeium", + "env": [ + "WINDSURF_CONFIG_DIR" + ] + }, + "configFormat": "none", + "artifactLayout": { + "global": [ + { + "kind": "skills", + "destSubpath": "skills", + "prefix": "gsd-", + "nesting": "flat", + "recursive": false, + "converter": "convertClaudeCommandToWindsurfSkill" + } + ], + "local": [] + }, + "commandStyle": "slash-hyphen", + "hooksSurface": "none", + "sandboxTier": "none", + "supportTier": 2 + } + } +}; const commandFamilies = { "audit-open": { @@ -399,10 +1712,26 @@ const profileMembership = { }; const _requiresGraph = { + "antigravity": [], "audit": [], + "augment": [], + "claude": [], + "cline": [], + "codebuddy": [], + "codex": [], + "copilot": [], + "cursor": [], + "gemini": [], "graphify": [], + "hermes": [], "intel": [], - "ui": [] + "kilo": [], + "kimi": [], + "opencode": [], + "qwen": [], + "trae": [], + "ui": [], + "windsurf": [] }; function requiresClosure(id) { diff --git a/scripts/gen-capability-registry.cjs b/scripts/gen-capability-registry.cjs index d7b191f81..547a43a95 100644 --- a/scripts/gen-capability-registry.cjs +++ b/scripts/gen-capability-registry.cjs @@ -452,8 +452,201 @@ function validateFeatureBody(cap) { // C3: Validate role:runtime body const VALID_CONFIG_FORMATS = new Set(['settings-json', 'toml', 'markdown', 'markdown-dir', 'none']); +const VALID_CONFIG_HOME_KINDS = new Set(['dot-home', 'dot-home-nested', 'xdg', 'generic-agents-root']); +const VALID_COMMAND_STYLES = new Set(['slash-hyphen', 'shell-var']); +const VALID_HOOKS_SURFACES = new Set(['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'none']); +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']; +/** + * Validate a runtime.configHome object per ADR-1016 Decision 1. + * Returns an array of error strings. + * + * @param {string} capId Capability id (for error messages) + * @param {*} ch The configHome value + * @returns {string[]} + */ +function validateConfigHome(capId, ch) { + const errors = []; + const ctx = 'capability "' + capId + '" runtime.configHome'; + + if (typeof ch !== 'object' || ch === null || Array.isArray(ch)) { + errors.push(ctx + ' must be an object (got: ' + (ch === null ? 'null' : typeof ch) + ')'); + return errors; + } + + // kind — must be in closed vocab; inline literal guard (CodeQL barrier) + if (ch.kind === '__proto__' || ch.kind === 'constructor' || ch.kind === 'prototype') { + errors.push(ctx + '.kind "' + ch.kind + '" is a reserved name'); + } else if (!VALID_CONFIG_HOME_KINDS.has(ch.kind)) { + errors.push( + ctx + '.kind must be one of: ' + [...VALID_CONFIG_HOME_KINDS].join(', ') + + ' (got: ' + JSON.stringify(ch.kind) + ')', + ); + } + + // name — required string + if (typeof ch.name !== 'string' || ch.name.length === 0) { + errors.push(ctx + '.name must be a non-empty string'); + } + + // parent — required when kind == dot-home-nested + if (ch.kind === 'dot-home-nested') { + if (typeof ch.parent !== 'string' || ch.parent.length === 0) { + errors.push(ctx + '.parent must be a non-empty string when kind is "dot-home-nested"'); + } + } + + // env — required; must be an array of strings (every runtime has ≥0 env overrides) + if (!Array.isArray(ch.env)) { + errors.push(ctx + '.env is required and must be an array of strings (got: ' + JSON.stringify(ch.env) + ')'); + } else { + for (let i = 0; i < ch.env.length; i++) { + if (typeof ch.env[i] !== 'string') { + errors.push(ctx + '.env[' + i + '] must be a string'); + } + } + } + + // probe — optional; if present must be an array of strings + if (ch.probe !== undefined) { + if (!Array.isArray(ch.probe)) { + errors.push(ctx + '.probe must be an array of strings if present'); + } else { + for (let i = 0; i < ch.probe.length; i++) { + if (typeof ch.probe[i] !== 'string') { + errors.push(ctx + '.probe[' + i + '] must be a string'); + } + } + } + } + + // probeExists — optional; if present must be a non-empty string (sub-path existence check for probe) + if (ch.probeExists !== undefined) { + if (typeof ch.probeExists !== 'string' || ch.probeExists.length === 0) { + errors.push(ctx + '.probeExists must be a non-empty string if present (got: ' + JSON.stringify(ch.probeExists) + ')'); + } + } + + // skillsHome — optional; if present must be a full valid configHome object (recursive validation) + if (ch.skillsHome !== undefined) { + // Recursive call: validate skillsHome as a nested configHome. + // Use a synthetic capId to surface the sub-path in error messages. + const skillsHomeErrors = validateConfigHome(capId + '.skillsHome', ch.skillsHome); + // Rewrite the inner ctx prefix so errors read as "...runtime.configHome.skillsHome..." + for (const e of skillsHomeErrors) { + errors.push(e.replace( + 'capability "' + capId + '.skillsHome" runtime.configHome', + ctx + '.skillsHome', + )); + } + } + + return errors; +} + +/** + * Validate a single ArtifactKind entry per ADR-1016 Decision 3. + * Returns an array of error strings. + * + * @param {string} capId Capability id (for error messages) + * @param {*} entry The ArtifactKind object + * @param {string} prefix Path prefix for error messages (e.g. "artifactLayout.global[0]") + * @returns {string[]} + */ +function validateArtifactKindEntry(capId, entry, prefix) { + const errors = []; + const ctx = 'capability "' + capId + '" runtime.' + prefix; + + if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) { + errors.push(ctx + ' must be an object'); + return errors; + } + + // kind — must be in closed vocab; inline literal guard (CodeQL barrier) + if (entry.kind === '__proto__' || entry.kind === 'constructor' || entry.kind === 'prototype') { + errors.push(ctx + '.kind "' + entry.kind + '" is a reserved name'); + } else if (!VALID_ARTIFACT_KIND_NAMES.has(entry.kind)) { + errors.push( + ctx + '.kind must be one of: ' + [...VALID_ARTIFACT_KIND_NAMES].join(', ') + + ' (got: ' + JSON.stringify(entry.kind) + ')', + ); + } + + // destSubpath — required non-empty string + if (typeof entry.destSubpath !== 'string' || entry.destSubpath.length === 0) { + 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) + ')', + ); + } + } + + // 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 + ')'); + } + } + + // recursive — optional; if present must be a boolean + if (entry.recursive !== undefined) { + if (typeof entry.recursive !== 'boolean') { + errors.push(ctx + '.recursive must be a boolean if present (got: ' + typeof entry.recursive + ')'); + } + } + + // 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 + ')'); + } + } + + return errors; +} + +/** + * Validate runtime.artifactLayout per ADR-1016 Decision 3. + * Accepts the structured { global, local } shape. + * Returns an array of error strings. + * + * @param {string} capId Capability id (for error messages) + * @param {*} layout The artifactLayout value + * @returns {string[]} + */ +function validateArtifactLayout(capId, layout) { + const errors = []; + const ctx = 'capability "' + capId + '" runtime.artifactLayout'; + + if (typeof layout !== 'object' || layout === null || Array.isArray(layout)) { + errors.push(ctx + ' must be an object with "global" and "local" arrays'); + return errors; + } + + for (const scope of ['global', 'local']) { + const arr = layout[scope]; + if (!Array.isArray(arr)) { + errors.push(ctx + '.' + scope + ' must be an array'); + } else { + for (let i = 0; i < arr.length; i++) { + errors.push(...validateArtifactKindEntry(capId, arr[i], 'artifactLayout.' + scope + '[' + i + ']')); + } + } + } + + return errors; +} + function validateRuntimeBody(cap) { const errors = []; @@ -471,24 +664,61 @@ function validateRuntimeBody(cap) { } const r = cap.runtime; - if (typeof r.configHome !== 'string' || r.configHome.length === 0) { - errors.push('runtime.configHome must be a non-empty string'); - } + + // configHome — must be a structured object (ADR-1016 Decision 1) + errors.push(...validateConfigHome(cap.id || '(unknown)', r.configHome)); + + // configFormat — closed 5-enum (unchanged) if (!VALID_CONFIG_FORMATS.has(r.configFormat)) { errors.push('runtime.configFormat must be one of: ' + [...VALID_CONFIG_FORMATS].join(', ') + ' (got: ' + r.configFormat + ')'); } - if (!Array.isArray(r.artifactLayout)) { - errors.push('runtime.artifactLayout must be an array'); + + // artifactLayout — structured { global, local } per ADR-1016 Decision 3 + errors.push(...validateArtifactLayout(cap.id || '(unknown)', r.artifactLayout)); + + // commandStyle — closed 2-enum (ADR-1016 Decision 4); inline literal guard (CodeQL barrier) + if (r.commandStyle === '__proto__' || r.commandStyle === 'constructor' || r.commandStyle === 'prototype') { + errors.push('runtime.commandStyle "' + r.commandStyle + '" is a reserved name'); + } else if (!VALID_COMMAND_STYLES.has(r.commandStyle)) { + errors.push( + 'runtime.commandStyle must be one of: ' + [...VALID_COMMAND_STYLES].join(', ') + + ' (got: ' + JSON.stringify(r.commandStyle) + ')', + ); } - if (typeof r.commandStyle !== 'string' || r.commandStyle.length === 0) { - errors.push('runtime.commandStyle must be a non-empty string'); + + // hooksSurface — closed 6-enum (ADR-1016 Decision 5); inline literal guard (CodeQL barrier) + if (r.hooksSurface === '__proto__' || r.hooksSurface === 'constructor' || r.hooksSurface === 'prototype') { + errors.push('runtime.hooksSurface "' + r.hooksSurface + '" is a reserved name'); + } else if (!VALID_HOOKS_SURFACES.has(r.hooksSurface)) { + errors.push( + 'runtime.hooksSurface must be one of: ' + [...VALID_HOOKS_SURFACES].join(', ') + + ' (got: ' + JSON.stringify(r.hooksSurface) + ')', + ); } - if (typeof r.hooksSurface !== 'string' || r.hooksSurface.length === 0) { - errors.push('runtime.hooksSurface must be a non-empty string'); + + // hookEvents — optional; if present must be in closed 3-enum (ADR-1016 Decision 5) + if (r.hookEvents !== undefined) { + if (r.hookEvents === '__proto__' || r.hookEvents === 'constructor' || r.hookEvents === 'prototype') { + errors.push('runtime.hookEvents "' + r.hookEvents + '" is a reserved name'); + } else if (!VALID_HOOK_EVENTS.has(r.hookEvents)) { + errors.push( + 'runtime.hookEvents must be one of: ' + [...VALID_HOOK_EVENTS].join(', ') + + ' (got: ' + JSON.stringify(r.hookEvents) + ')', + ); + } } - if (typeof r.sandboxTier !== 'string' || r.sandboxTier.length === 0) { - errors.push('runtime.sandboxTier must be a non-empty string'); + + // sandboxTier — closed 2-enum (ADR-1016 Decision 6); inline literal guard (CodeQL barrier) + if (r.sandboxTier === '__proto__' || r.sandboxTier === 'constructor' || r.sandboxTier === 'prototype') { + errors.push('runtime.sandboxTier "' + r.sandboxTier + '" is a reserved name'); + } else if (!VALID_SANDBOX_TIERS.has(r.sandboxTier)) { + errors.push( + 'runtime.sandboxTier must be one of: ' + [...VALID_SANDBOX_TIERS].join(', ') + + ' (got: ' + JSON.stringify(r.sandboxTier) + ')', + ); } + + // supportTier — 1 or 2 (unchanged) if (r.supportTier !== 1 && r.supportTier !== 2) { errors.push('runtime.supportTier must be 1 or 2 (got: ' + r.supportTier + ')'); } @@ -1833,6 +2063,17 @@ module.exports = { runConsistencyGate, // ADR-959: command entry validation validateCommandEntry, + // ADR-1016 phase 5a: runtime body validators + closed-vocab sets + validateConfigHome, + validateArtifactLayout, + validateArtifactKindEntry, + VALID_CONFIG_HOME_KINDS, + VALID_COMMAND_STYLES, + VALID_HOOKS_SURFACES, + VALID_HOOK_EVENTS, + VALID_SANDBOX_TIERS, + VALID_ARTIFACT_KIND_NAMES, + VALID_ARTIFACT_NESTINGS, // FIX 5 (lazy): PROFILE_RANK and CLUSTERS are loaded on first access via getters // so importing the generator on a fresh/unbuilt worktree doesn't fail at module load. get PROFILE_RANK() { return getInstallProfiles().PROFILE_RANK; }, diff --git a/tests/capability-registry.test.cjs b/tests/capability-registry.test.cjs index c10c85ad9..567229524 100644 --- a/tests/capability-registry.test.cjs +++ b/tests/capability-registry.test.cjs @@ -1150,15 +1150,19 @@ describe('C2: cross-capability consumes satisfiability (global pass)', () => { // ─── 10. C3: role:runtime validation ───────────────────────────────────────── describe('C3: role:runtime body validation', () => { + // ADR-1016 phase 5a: fixture updated to match tightened structured-value shapes. + // configHome is now an object (Decision 1), artifactLayout is { global, local } (Decision 3), + // commandStyle is closed enum (Decision 4), hooksSurface is closed enum (Decision 5). const VALID_RUNTIME_CAP = { id: 'cursor', role: 'runtime', title: 'Cursor', description: 'Cursor IDE runtime', tier: 'standard', requires: [], runtime: { - configHome: '~/.cursor', + configHome: { kind: 'dot-home', name: '.cursor', env: ['CURSOR_CONFIG_DIR'] }, configFormat: 'settings-json', - artifactLayout: [], - commandStyle: 'slash', - hooksSurface: 'rules', + artifactLayout: { global: [], local: [] }, + commandStyle: 'slash-hyphen', + hooksSurface: 'cursor-hooks-json', + hookEvents: 'claude', sandboxTier: 'none', supportTier: 2, }, @@ -2496,3 +2500,784 @@ describe('ADR-959: validateCapability — commands field on feature role', () => assert.ok(errors.some((e) => e.includes('commands')), 'Expected commands error, got: ' + JSON.stringify(errors)); }); }); + +// ─── 24. ADR-1016 phase 5a: runtime capability descriptors ─────────────────── + +const { + validateConfigHome, + validateArtifactLayout, + VALID_CONFIG_HOME_KINDS, + VALID_COMMAND_STYLES, + VALID_HOOKS_SURFACES, + VALID_HOOK_EVENTS, + VALID_SANDBOX_TIERS, + VALID_ARTIFACT_KIND_NAMES, + VALID_ARTIFACT_NESTINGS, +} = require('../scripts/gen-capability-registry.cjs'); + +const RUNTIME_IDS = [ + 'claude', 'codex', 'antigravity', 'gemini', 'cursor', 'opencode', + 'kilo', 'copilot', 'augment', 'trae', 'qwen', 'hermes', + 'codebuddy', 'cline', 'kimi', 'windsurf', +]; + +// Helper: build a minimal valid runtime capability object for fixture-based tests +function makeRuntimeCap(overrides) { + return { + id: 'test-rt', + role: 'runtime', + title: 'Test Runtime', + description: 'A synthetic runtime capability for testing.', + tier: 'core', + requires: [], + runtime: { + configHome: { kind: 'dot-home', name: '.test-rt', env: ['TEST_RT_DIR'] }, + configFormat: 'settings-json', + artifactLayout: { global: [], local: [] }, + commandStyle: 'slash-hyphen', + hooksSurface: 'settings-json', + hookEvents: 'claude', + sandboxTier: 'none', + supportTier: 1, + ...((overrides && overrides.runtime) ? overrides.runtime : {}), + }, + ...overrides, + }; +} + +// ── 24a. All 16 runtime ids appear in the runtimes index ───────────────────── + +describe('ADR-1016 phase 5a: all 16 runtimes in registry index', () => { + let registry; + + test('loadAndValidate + buildRegistry produces runtimes index with 16 entries', () => { + const { capMap, errors } = loadAndValidate(new Set()); + const hardErrors = errors.filter((e) => !e.includes('pending-migration')); + assert.deepEqual(hardErrors, [], 'Expected no hard errors: ' + JSON.stringify(hardErrors)); + registry = buildRegistry(capMap); + const runtimeKeys = Object.keys(registry.runtimes).sort(); + assert.strictEqual(runtimeKeys.length, 16, 'Expected 16 runtime entries, got: ' + runtimeKeys.join(', ')); + for (const id of RUNTIME_IDS) { + assert.ok( + Object.prototype.hasOwnProperty.call(registry.runtimes, id), + 'runtimes index must contain "' + id + '"', + ); + } + }); + + test('every runtimes entry has role: "runtime"', () => { + const { capMap } = loadAndValidate(new Set()); + registry = buildRegistry(capMap); + for (const id of RUNTIME_IDS) { + assert.strictEqual( + registry.runtimes[id] && registry.runtimes[id].role, 'runtime', + 'runtimes["' + id + '"].role must be "runtime"', + ); + } + }); + + test('every runtimes entry has all 6 axes present in runtime object', () => { + const { capMap } = loadAndValidate(new Set()); + registry = buildRegistry(capMap); + const requiredAxes = ['configHome', 'configFormat', 'artifactLayout', 'commandStyle', 'hooksSurface', 'sandboxTier']; + for (const id of RUNTIME_IDS) { + const rt = registry.runtimes[id] && registry.runtimes[id].runtime; + assert.ok(rt, 'runtimes["' + id + '"].runtime must exist'); + for (const axis of requiredAxes) { + assert.ok( + Object.prototype.hasOwnProperty.call(rt, axis), + 'runtimes["' + id + '"].runtime.' + axis + ' must be present', + ); + } + // supportTier also required + assert.ok( + rt.supportTier === 1 || rt.supportTier === 2, + 'runtimes["' + id + '"].runtime.supportTier must be 1 or 2 (got: ' + rt.supportTier + ')', + ); + } + }); +}); + +// ── 24b. Sample axis value assertions ──────────────────────────────────────── + +describe('ADR-1016 phase 5a: sample axis value assertions', () => { + test('codex: commandStyle === shell-var and sandboxTier === codex-agent-sandbox', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['codex'].runtime; + assert.strictEqual(rt.commandStyle, 'shell-var', 'codex.commandStyle must be "shell-var"'); + assert.strictEqual(rt.sandboxTier, 'codex-agent-sandbox', 'codex.sandboxTier must be "codex-agent-sandbox"'); + }); + + test('codex: configHome.kind === dot-home, name === .codex', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['codex'].runtime; + assert.strictEqual(rt.configHome.kind, 'dot-home', 'codex.configHome.kind must be "dot-home"'); + assert.strictEqual(rt.configHome.name, '.codex', 'codex.configHome.name must be ".codex"'); + assert.ok(Array.isArray(rt.configHome.env) && rt.configHome.env.includes('CODEX_HOME'), + 'codex.configHome.env must include "CODEX_HOME"'); + }); + + test('claude: commandStyle === slash-hyphen, sandboxTier === none', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['claude'].runtime; + assert.strictEqual(rt.commandStyle, 'slash-hyphen', 'claude.commandStyle must be "slash-hyphen"'); + assert.strictEqual(rt.sandboxTier, 'none', 'claude.sandboxTier must be "none"'); + }); + + test('claude: configHome.kind === dot-home', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['claude'].runtime; + assert.strictEqual(rt.configHome.kind, 'dot-home', 'claude.configHome.kind must be "dot-home"'); + }); + + test('antigravity: configHome.kind === dot-home-nested with parent .gemini', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['antigravity'].runtime; + assert.strictEqual(rt.configHome.kind, 'dot-home-nested', 'antigravity.configHome.kind must be "dot-home-nested"'); + assert.strictEqual(rt.configHome.parent, '.gemini', 'antigravity.configHome.parent must be ".gemini"'); + assert.ok(Array.isArray(rt.configHome.probe), 'antigravity.configHome.probe must be an array'); + assert.ok(rt.configHome.probe.length > 0, 'antigravity.configHome.probe must be non-empty'); + }); + + test('kilo: configHome.skillsHome is present', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['kilo'].runtime; + assert.ok(rt.configHome.skillsHome, 'kilo.configHome.skillsHome must be present'); + assert.ok(typeof rt.configHome.skillsHome.kind === 'string', 'kilo.configHome.skillsHome.kind must be a string'); + }); + + test('windsurf: configHome.kind === dot-home-nested with parent .codeium', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['windsurf'].runtime; + assert.strictEqual(rt.configHome.kind, 'dot-home-nested', 'windsurf.configHome.kind must be "dot-home-nested"'); + assert.strictEqual(rt.configHome.parent, '.codeium', 'windsurf.configHome.parent must be ".codeium"'); + }); + + test('kimi: configHome.kind === generic-agents-root', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['kimi'].runtime; + assert.strictEqual(rt.configHome.kind, 'generic-agents-root', 'kimi.configHome.kind must be "generic-agents-root"'); + }); + + test('antigravity: hookEvents === gemini', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['antigravity'].runtime; + assert.strictEqual(rt.hookEvents, 'gemini', 'antigravity.hookEvents must be "gemini"'); + }); + + test('opencode: hooksSurface === none, no hookEvents (registers zero lifecycle hooks)', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['opencode'].runtime; + assert.strictEqual(rt.hooksSurface, 'none', 'opencode.hooksSurface must be "none" (no managed lifecycle hooks)'); + assert.ok( + !Object.prototype.hasOwnProperty.call(rt, 'hookEvents'), + 'opencode.runtime must NOT have hookEvents (no lifecycle hook registration)', + ); + }); + + test('kilo: hooksSurface === none, no hookEvents (registers zero lifecycle hooks)', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['kilo'].runtime; + assert.strictEqual(rt.hooksSurface, 'none', 'kilo.hooksSurface must be "none" (no managed lifecycle hooks)'); + assert.ok( + !Object.prototype.hasOwnProperty.call(rt, 'hookEvents'), + 'kilo.runtime must NOT have hookEvents (no lifecycle hook registration)', + ); + }); + + test('kimi: configHome.probeExists === "skills" (probe selects first candidate with skills/ dir)', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const rt = registry.runtimes['kimi'].runtime; + assert.strictEqual( + rt.configHome.probeExists, 'skills', + 'kimi.configHome.probeExists must be "skills" (selects first probe candidate where /skills exists)', + ); + }); + + test('copilot/trae/kimi/windsurf/cline/opencode/kilo: hooksSurface none or copilot-inline/cline-rules, no hookEvents', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + // opencode and kilo register ZERO lifecycle hooks → hooksSurface none, no hookEvents + const noEventsRuntimes = ['trae', 'kimi', 'windsurf', 'opencode', 'kilo']; + for (const id of noEventsRuntimes) { + const rt = registry.runtimes[id].runtime; + assert.ok( + !Object.prototype.hasOwnProperty.call(rt, 'hookEvents'), + id + '.runtime must NOT have hookEvents (no hook events for this runtime)', + ); + } + // cline has cline-rules surface but no hookEvents + const clineRt = registry.runtimes['cline'].runtime; + assert.strictEqual(clineRt.hooksSurface, 'cline-rules', 'cline.hooksSurface must be "cline-rules"'); + assert.ok( + !Object.prototype.hasOwnProperty.call(clineRt, 'hookEvents'), + 'cline.runtime must NOT have hookEvents', + ); + // copilot has copilot-inline surface but no hookEvents + const copilotRt = registry.runtimes['copilot'].runtime; + assert.strictEqual(copilotRt.hooksSurface, 'copilot-inline', 'copilot.hooksSurface must be "copilot-inline"'); + assert.ok( + !Object.prototype.hasOwnProperty.call(copilotRt, 'hookEvents'), + 'copilot.runtime must NOT have hookEvents', + ); + }); + + test('codex: hooksSurface === codex-hooks-json, cursor: hooksSurface === cursor-hooks-json', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + assert.strictEqual(registry.runtimes['codex'].runtime.hooksSurface, 'codex-hooks-json', + 'codex.hooksSurface must be "codex-hooks-json"'); + assert.strictEqual(registry.runtimes['cursor'].runtime.hooksSurface, 'cursor-hooks-json', + 'cursor.hooksSurface must be "cursor-hooks-json"'); + }); + + test('tier-1 runtimes: claude, codex, antigravity have supportTier === 1', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + for (const id of ['claude', 'codex', 'antigravity']) { + assert.strictEqual( + registry.runtimes[id].runtime.supportTier, 1, + id + '.runtime.supportTier must be 1 (tier-1 support)', + ); + } + }); + + test('tier-2 runtimes: gemini through windsurf have supportTier === 2', () => { + const { capMap } = loadAndValidate(new Set()); + const registry = buildRegistry(capMap); + const tier2 = ['gemini', 'cursor', 'opencode', 'kilo', 'copilot', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'windsurf']; + for (const id of tier2) { + assert.strictEqual( + registry.runtimes[id].runtime.supportTier, 2, + id + '.runtime.supportTier must be 2 (tier-2 support)', + ); + } + }); +}); + +// ── 24c. Closed-vocab REJECTION tests ──────────────────────────────────────── + +describe('ADR-1016 phase 5a: closed-vocab rejection — tightened validateRuntimeBody', () => { + test('bad configHome.kind → validation error', () => { + const cap = makeRuntimeCap({ id: 'test-rt', runtime: { configHome: { kind: 'custom-home', name: '.test', env: [] } } }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('configHome') && e.includes('kind')), + 'Expected configHome.kind error, got: ' + JSON.stringify(errors), + ); + }); + + test('configHome is a string (old shape) → validation error', () => { + const cap = makeRuntimeCap({ id: 'test-rt', runtime: { configHome: '~/.test-rt' } }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('configHome')), + 'Expected configHome object error (string not accepted), got: ' + JSON.stringify(errors), + ); + }); + + test('dot-home-nested without parent → validation error', () => { + const cap = makeRuntimeCap({ id: 'test-rt', runtime: { configHome: { kind: 'dot-home-nested', name: 'foo', env: [] } } }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('parent') && e.includes('dot-home-nested')), + 'Expected parent required for dot-home-nested, got: ' + JSON.stringify(errors), + ); + }); + + test('bad commandStyle → validation error', () => { + const cap = makeRuntimeCap({ id: 'test-rt', runtime: { commandStyle: 'slash-colon' } }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('commandStyle')), + 'Expected commandStyle error, got: ' + JSON.stringify(errors), + ); + }); + + test('bad hooksSurface → validation error', () => { + const cap = makeRuntimeCap({ id: 'test-rt', runtime: { hooksSurface: 'custom-hooks' } }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('hooksSurface')), + 'Expected hooksSurface error, got: ' + JSON.stringify(errors), + ); + }); + + test('bad hookEvents → validation error', () => { + const cap = makeRuntimeCap({ id: 'test-rt', runtime: { hookEvents: 'my-dialect' } }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('hookEvents')), + 'Expected hookEvents error, got: ' + JSON.stringify(errors), + ); + }); + + test('bad sandboxTier → validation error', () => { + const cap = makeRuntimeCap({ id: 'test-rt', runtime: { sandboxTier: 'workspace-sandbox' } }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('sandboxTier')), + 'Expected sandboxTier error, got: ' + JSON.stringify(errors), + ); + }); + + test('artifactLayout is an array (old shape) → validation error', () => { + const cap = makeRuntimeCap({ id: 'test-rt', runtime: { artifactLayout: [] } }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('artifactLayout')), + 'Expected artifactLayout shape error (old array not accepted), got: ' + JSON.stringify(errors), + ); + }); + + test('bad ArtifactKind.kind in artifactLayout → validation error', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { + artifactLayout: { + global: [{ kind: 'magic-kind', destSubpath: 'x', prefix: 'g-', nesting: 'flat', recursive: false, converter: null }], + local: [], + }, + }, + }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('kind')), + 'Expected ArtifactKind.kind error, got: ' + JSON.stringify(errors), + ); + }); + + test('bad ArtifactKind.nesting → validation error', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { + artifactLayout: { + global: [{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-', nesting: 'deep', recursive: false, converter: null }], + local: [], + }, + }, + }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('nesting')), + 'Expected nesting error, got: ' + JSON.stringify(errors), + ); + }); + + test('valid runtime descriptor passes validateCapability with no errors', () => { + const cap = makeRuntimeCap({ id: 'test-rt' }); + const errors = validateCapability(cap, 'test-rt'); + assert.deepEqual(errors, [], 'Expected no errors for valid runtime cap: ' + JSON.stringify(errors)); + }); +}); + +// ── 24d. validateConfigHome + validateArtifactLayout unit tests ─────────────── + +describe('ADR-1016 phase 5a: validateConfigHome unit tests', () => { + test('valid dot-home with env → no errors', () => { + const errors = validateConfigHome('test', { kind: 'dot-home', name: '.test', env: ['TEST_DIR'] }); + assert.deepEqual(errors, []); + }); + + test('valid dot-home-nested with parent → no errors', () => { + const errors = validateConfigHome('test', { kind: 'dot-home-nested', name: 'foo', parent: '.bar', env: [] }); + assert.deepEqual(errors, []); + }); + + test('valid xdg → no errors', () => { + const errors = validateConfigHome('test', { kind: 'xdg', name: 'opencode', env: ['OPENCODE_CONFIG_DIR', 'XDG_CONFIG_HOME'] }); + assert.deepEqual(errors, []); + }); + + test('valid generic-agents-root with probe → no errors', () => { + const errors = validateConfigHome('test', { kind: 'generic-agents-root', name: 'agents', env: ['KIMI_CONFIG_DIR'], probe: ['~/.config/agents'] }); + assert.deepEqual(errors, []); + }); + + test('null configHome → error', () => { + const errors = validateConfigHome('test', null); + assert.ok(errors.length > 0 && errors.some((e) => e.includes('configHome'))); + }); + + test('string configHome → error', () => { + const errors = validateConfigHome('test', '~/.test'); + assert.ok(errors.length > 0 && errors.some((e) => e.includes('configHome'))); + }); + + test('unknown kind → error mentioning the valid set', () => { + const errors = validateConfigHome('test', { kind: 'unknown', name: '.x', env: [] }); + assert.ok(errors.some((e) => e.includes('kind') && e.includes('dot-home')), + 'Error should list valid kinds, got: ' + JSON.stringify(errors)); + }); + + test('dot-home-nested missing parent → error', () => { + const errors = validateConfigHome('test', { kind: 'dot-home-nested', name: 'x', env: [] }); + assert.ok(errors.some((e) => e.includes('parent'))); + }); + + test('skillsHome with valid kind → no errors', () => { + const errors = validateConfigHome('test', { + kind: 'xdg', name: 'kilo', env: [], + skillsHome: { kind: 'dot-home', name: '.kilo', env: [] }, + }); + assert.deepEqual(errors, []); + }); + + test('skillsHome with bad kind → error', () => { + const errors = validateConfigHome('test', { + kind: 'xdg', name: 'kilo', env: [], + skillsHome: { kind: 'exotic', name: '.kilo', env: [] }, + }); + assert.ok(errors.some((e) => e.includes('skillsHome') && e.includes('kind'))); + }); +}); + +describe('ADR-1016 phase 5a: validateArtifactLayout unit tests', () => { + test('valid empty global/local → no errors', () => { + const errors = validateArtifactLayout('test', { global: [], local: [] }); + assert.deepEqual(errors, []); + }); + + test('valid skills entry in global → no errors', () => { + const errors = validateArtifactLayout('test', { + global: [{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-', nesting: 'flat', recursive: false, converter: null }], + local: [], + }); + assert.deepEqual(errors, []); + }); + + test('array (old shape) → error', () => { + const errors = validateArtifactLayout('test', []); + assert.ok(errors.some((e) => e.includes('artifactLayout'))); + }); + + test('missing global → error', () => { + const errors = validateArtifactLayout('test', { local: [] }); + assert.ok(errors.some((e) => e.includes('global'))); + }); + + test('missing local → error', () => { + const errors = validateArtifactLayout('test', { global: [] }); + assert.ok(errors.some((e) => e.includes('local'))); + }); + + test('bad kind in global[0] → error', () => { + const errors = validateArtifactLayout('test', { + global: [{ kind: 'bad-kind', destSubpath: 'x', prefix: '', nesting: 'flat', recursive: false, converter: null }], + local: [], + }); + assert.ok(errors.some((e) => e.includes('kind'))); + }); + + test('bad nesting in global[0] → error', () => { + const errors = validateArtifactLayout('test', { + global: [{ kind: 'skills', destSubpath: 'x', prefix: '', nesting: 'trilateral', recursive: false, converter: null }], + local: [], + }); + assert.ok(errors.some((e) => e.includes('nesting'))); + }); + + test('kimi-agents kind → no errors', () => { + const errors = validateArtifactLayout('test', { + global: [{ kind: 'kimi-agents', destSubpath: 'agents', prefix: 'gsd', nesting: 'flat', recursive: false, converter: null }], + local: [], + }); + assert.deepEqual(errors, []); + }); +}); + +// ── 24d-extra. FIX 3: tightened validateRuntimeBody / validateConfigHome ────── + +describe('FIX 3: tightened runtime validator — configHome.env required', () => { + test('configHome missing env → validation error', () => { + // env is now required; omitting it must produce an error + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { configHome: { kind: 'dot-home', name: '.test-rt' } }, // no env + }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('env') && (e.includes('required') || e.includes('array'))), + 'Expected configHome.env required error, got: ' + JSON.stringify(errors), + ); + }); + + test('configHome with env: [] (empty array) is accepted', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { configHome: { kind: 'dot-home', name: '.test-rt', env: [] } }, + }); + const errors = validateCapability(cap, 'test-rt'); + const envErrors = errors.filter((e) => e.includes('env')); + assert.deepEqual(envErrors, [], 'Empty env array should be accepted, got: ' + JSON.stringify(envErrors)); + }); + + test('configHome with env: null → validation error (not an array)', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { configHome: { kind: 'dot-home', name: '.test-rt', env: null } }, + }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('env')), + 'Expected env error for null, got: ' + JSON.stringify(errors), + ); + }); +}); + +describe('FIX 3: tightened runtime validator — skillsHome recursive validation', () => { + test('skillsHome with bad kind → validation error via full recursive check', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { + configHome: { + kind: 'xdg', name: 'test', env: [], + skillsHome: { kind: 'exotic-kind', name: '.test', env: [] }, + }, + }, + }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('skillsHome') && e.includes('kind')), + 'Expected skillsHome.kind error for exotic-kind, got: ' + JSON.stringify(errors), + ); + }); + + test('skillsHome missing env → validation error (env required in recursive check)', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { + configHome: { + kind: 'xdg', name: 'test', env: [], + skillsHome: { kind: 'dot-home', name: '.test' }, // no env + }, + }, + }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('skillsHome') && e.includes('env')), + 'Expected skillsHome.env required error, got: ' + JSON.stringify(errors), + ); + }); + + test('kilo descriptor: skillsHome passes full validation', () => { + // kilo has skillsHome: { kind: "dot-home", name: ".kilo", env: [] } — must pass + const { capMap, errors } = loadAndValidate(new Set()); + const hardErrors = errors.filter((e) => !e.includes('pending-migration')); + assert.deepEqual(hardErrors, [], 'No hard errors expected for kilo, got: ' + JSON.stringify(hardErrors)); + const kiloRt = capMap.get('kilo'); + assert.ok(kiloRt, 'kilo must be in capMap'); + assert.ok(kiloRt.runtime.configHome.skillsHome, 'kilo.configHome.skillsHome must be present'); + assert.strictEqual(kiloRt.runtime.configHome.skillsHome.kind, 'dot-home'); + assert.strictEqual(kiloRt.runtime.configHome.skillsHome.name, '.kilo'); + }); +}); + +describe('FIX 3: tightened runtime validator — ArtifactKind field type checks', () => { + test('ArtifactKind with recursive: "yes" (non-boolean) → validation error', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { + artifactLayout: { + global: [{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-', nesting: 'flat', recursive: 'yes', converter: null }], + local: [], + }, + }, + }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('recursive') && e.includes('boolean')), + 'Expected recursive non-boolean error, got: ' + JSON.stringify(errors), + ); + }); + + test('ArtifactKind with recursive: true (boolean) → no error', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { + artifactLayout: { + global: [{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-', nesting: 'flat', recursive: true, converter: null }], + local: [], + }, + }, + }); + const errors = validateCapability(cap, 'test-rt'); + const recursiveErrors = errors.filter((e) => e.includes('recursive')); + assert.deepEqual(recursiveErrors, [], 'recursive: true must be accepted, got: ' + JSON.stringify(recursiveErrors)); + }); + + test('ArtifactKind with prefix: 42 (non-string) → validation error', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { + artifactLayout: { + global: [{ kind: 'skills', destSubpath: 'skills', prefix: 42, nesting: 'flat', recursive: false, converter: null }], + local: [], + }, + }, + }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('prefix') && e.includes('string')), + 'Expected prefix non-string error, got: ' + JSON.stringify(errors), + ); + }); + + test('ArtifactKind with converter: 123 (non-string, non-null) → validation error', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { + artifactLayout: { + global: [{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-', nesting: 'flat', recursive: false, converter: 123 }], + local: [], + }, + }, + }); + const errors = validateCapability(cap, 'test-rt'); + assert.ok( + errors.some((e) => e.includes('converter') && (e.includes('string') || e.includes('null'))), + 'Expected converter type error, got: ' + JSON.stringify(errors), + ); + }); + + test('ArtifactKind with converter: null → no error', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { + artifactLayout: { + global: [{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-', nesting: 'flat', recursive: false, converter: null }], + local: [], + }, + }, + }); + const errors = validateCapability(cap, 'test-rt'); + const converterErrors = errors.filter((e) => e.includes('converter')); + assert.deepEqual(converterErrors, [], 'converter: null must be accepted, got: ' + JSON.stringify(converterErrors)); + }); + + test('ArtifactKind with converter: "convertClaudeCommandToKiloSkill" (string) → no error', () => { + const cap = makeRuntimeCap({ + id: 'test-rt', + runtime: { + artifactLayout: { + global: [{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-', nesting: 'flat', recursive: true, converter: 'convertClaudeCommandToKiloSkill' }], + local: [], + }, + }, + }); + const errors = validateCapability(cap, 'test-rt'); + const converterErrors = errors.filter((e) => e.includes('converter')); + assert.deepEqual(converterErrors, [], 'converter: string must be accepted, got: ' + JSON.stringify(converterErrors)); + }); +}); + +describe('FIX 3: tightened runtime validator — probeExists optional string', () => { + test('probeExists: "skills" is accepted', () => { + const errors = validateConfigHome('test', { + kind: 'generic-agents-root', name: 'agents', env: ['KIMI_CONFIG_DIR'], + probe: ['~/.config/agents', '~/.agents'], + probeExists: 'skills', + }); + assert.deepEqual(errors, [], 'probeExists: "skills" must be accepted, got: ' + JSON.stringify(errors)); + }); + + test('probeExists: "" (empty string) → error', () => { + const errors = validateConfigHome('test', { + kind: 'generic-agents-root', name: 'agents', env: [], + probeExists: '', + }); + assert.ok( + errors.some((e) => e.includes('probeExists')), + 'Expected probeExists empty string error, got: ' + JSON.stringify(errors), + ); + }); + + test('probeExists: 42 (non-string) → error', () => { + const errors = validateConfigHome('test', { + kind: 'generic-agents-root', name: 'agents', env: [], + probeExists: 42, + }); + assert.ok( + errors.some((e) => e.includes('probeExists')), + 'Expected probeExists non-string error, got: ' + JSON.stringify(errors), + ); + }); + + test('probeExists absent → no error (optional)', () => { + const errors = validateConfigHome('test', { + kind: 'generic-agents-root', name: 'agents', env: ['KIMI_CONFIG_DIR'], + probe: ['~/.config/agents'], + }); + const probeExistsErrors = errors.filter((e) => e.includes('probeExists')); + assert.deepEqual(probeExistsErrors, [], 'probeExists is optional — no error when absent, got: ' + JSON.stringify(probeExistsErrors)); + }); +}); + +// ── 24e. Closed-vocab set exports are correct ───────────────────────────────── + +describe('ADR-1016 phase 5a: closed-vocab set exports', () => { + test('VALID_CONFIG_HOME_KINDS has exactly the 4 expected values', () => { + assert.ok(VALID_CONFIG_HOME_KINDS instanceof Set); + for (const v of ['dot-home', 'dot-home-nested', 'xdg', 'generic-agents-root']) { + assert.ok(VALID_CONFIG_HOME_KINDS.has(v), 'VALID_CONFIG_HOME_KINDS must contain "' + v + '"'); + } + assert.strictEqual(VALID_CONFIG_HOME_KINDS.size, 4, 'VALID_CONFIG_HOME_KINDS must have exactly 4 members'); + }); + + test('VALID_COMMAND_STYLES has exactly 2 values', () => { + assert.ok(VALID_COMMAND_STYLES instanceof Set); + assert.ok(VALID_COMMAND_STYLES.has('slash-hyphen')); + assert.ok(VALID_COMMAND_STYLES.has('shell-var')); + assert.strictEqual(VALID_COMMAND_STYLES.size, 2); + }); + + test('VALID_HOOKS_SURFACES has exactly 6 values', () => { + assert.ok(VALID_HOOKS_SURFACES instanceof Set); + for (const v of ['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'none']) { + assert.ok(VALID_HOOKS_SURFACES.has(v), 'VALID_HOOKS_SURFACES must contain "' + v + '"'); + } + assert.strictEqual(VALID_HOOKS_SURFACES.size, 6); + }); + + test('VALID_HOOK_EVENTS has exactly 3 values', () => { + assert.ok(VALID_HOOK_EVENTS instanceof Set); + for (const v of ['claude', 'gemini', 'opencode-subset']) { + assert.ok(VALID_HOOK_EVENTS.has(v), 'VALID_HOOK_EVENTS must contain "' + v + '"'); + } + assert.strictEqual(VALID_HOOK_EVENTS.size, 3); + }); + + test('VALID_SANDBOX_TIERS has exactly 2 values', () => { + assert.ok(VALID_SANDBOX_TIERS instanceof Set); + assert.ok(VALID_SANDBOX_TIERS.has('none')); + assert.ok(VALID_SANDBOX_TIERS.has('codex-agent-sandbox')); + assert.strictEqual(VALID_SANDBOX_TIERS.size, 2); + }); + + test('VALID_ARTIFACT_KIND_NAMES has exactly 4 values', () => { + assert.ok(VALID_ARTIFACT_KIND_NAMES instanceof Set); + for (const v of ['commands', 'agents', 'skills', 'kimi-agents']) { + assert.ok(VALID_ARTIFACT_KIND_NAMES.has(v), 'VALID_ARTIFACT_KIND_NAMES must contain "' + v + '"'); + } + assert.strictEqual(VALID_ARTIFACT_KIND_NAMES.size, 4); + }); + + test('VALID_ARTIFACT_NESTINGS has exactly 2 values', () => { + assert.ok(VALID_ARTIFACT_NESTINGS instanceof Set); + assert.ok(VALID_ARTIFACT_NESTINGS.has('flat')); + assert.ok(VALID_ARTIFACT_NESTINGS.has('nested')); + assert.strictEqual(VALID_ARTIFACT_NESTINGS.size, 2); + }); +});