Author capabilities/<runtime>/capability.json for all 16 runtimes (role:runtime),
populating the registry's runtimes index ({} -> 16). Each carries the 6 ADR-1016
axes (configHome structured, configFormat, artifactLayout structured, commandStyle,
hooksSurface + hookEvents, sandboxTier, supportTier), extracted from the live
modules (runtime-homes/runtime-slash/runtime-artifact-layout/runtime-config-adapter
+ CODEX_AGENT_SANDBOX). validateRuntimeBody tightened to enforce the closed
vocabularies + structured configHome/artifactLayout (rejects old string configHome;
env required; skillsHome recursively validated).
Registry-only: bin/install.js + src/*.cts untouched, zero behavior change. The 5b-5f
drive steps consume these one axis at a time (ADR-1016 §8).
Codex accuracy review caught + fixed: opencode/kilo register ZERO lifecycle hooks
(install.js skips the whole block) -> hooksSurface:none (configFormat stays
settings-json); kimi probe selects on <root>/skills existence -> probeExists:"skills".
ArtifactKind required-field strictness + ConverterName closing deferred to 5d/5e.
ADR-1016 (Proposed) amended to match (Decisions 1 + 5).
Closes #1035
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
36
capabilities/antigravity/capability.json
Normal file
36
capabilities/antigravity/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
42
capabilities/augment/capability.json
Normal file
42
capabilities/augment/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
51
capabilities/claude/capability.json
Normal file
51
capabilities/claude/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
33
capabilities/cline/capability.json
Normal file
33
capabilities/cline/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
42
capabilities/codebuddy/capability.json
Normal file
42
capabilities/codebuddy/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
34
capabilities/codex/capability.json
Normal file
34
capabilities/codex/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
33
capabilities/copilot/capability.json
Normal file
33
capabilities/copilot/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
42
capabilities/cursor/capability.json
Normal file
42
capabilities/cursor/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
34
capabilities/gemini/capability.json
Normal file
34
capabilities/gemini/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
34
capabilities/hermes/capability.json
Normal file
34
capabilities/hermes/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
46
capabilities/kilo/capability.json
Normal file
46
capabilities/kilo/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
43
capabilities/kimi/capability.json
Normal file
43
capabilities/kimi/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
41
capabilities/opencode/capability.json
Normal file
41
capabilities/opencode/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
34
capabilities/qwen/capability.json
Normal file
34
capabilities/qwen/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
33
capabilities/trae/capability.json
Normal file
33
capabilities/trae/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
34
capabilities/windsurf/capability.json
Normal file
34
capabilities/windsurf/capability.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-06-10",
|
||||
"generated": "2026-06-11",
|
||||
"families": {
|
||||
"agents": [
|
||||
"gsd-advisor-researcher",
|
||||
|
||||
@@ -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 <candidate>/<probeExists> 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)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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; },
|
||||
|
||||
@@ -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 <candidate>/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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user