From 2421cf1b4a1fd010bf0ef3e6b4a34c6cdf8bbd90 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 18 Jun 2026 10:45:53 -0400 Subject: [PATCH] feat(#1430): versioned capability manifest + native stamping (ADR-1244 Phase 1) (#1436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(#1430): versioned capability manifest + native stamping (ADR-1244 Phase 1) Make the capability manifest versioned — the data substrate the Capability Ecosystem (ADR-1244) keys off: - capability.json gains a REQUIRED semver `version` plus the optional ecosystem envelope (`engines.gsd`, `compatVersions`, `integrity`, `provenance`); the build-time conformance validator enforces them via a new `validateVersionEnvelope()` (exported for the Phase 2 runtime overlay). - All 32 native capabilities stamped with `version` (= package version, lockstep) + `engines.gsd`; `sync-manifest-versions.cjs` gains a glob sweep that keeps them in sync, and the issue-844 regression guard is extended. - Strict SemVer 2.0.0 grammar blocks metacharacter/space/unicode smuggling in version strings; range/integrity fields are shape-validated (satisfaction and the load-time gate are deferred to Phase 2/4). - Capability rel-paths emitted forward-slash for cross-platform git correctness. Closes #1430 Co-Authored-By: Claude Opus 4.8 * docs(#1430): add changeset for versioned capability manifest Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .changeset/kind-dogs-dart.md | 5 + capabilities/ai-integration/capability.json | 27 +- capabilities/antigravity/capability.json | 14 +- capabilities/audit/capability.json | 11 +- capabilities/augment/capability.json | 8 +- capabilities/claude/capability.json | 15 +- capabilities/cline/capability.json | 8 +- capabilities/code-review/capability.json | 38 ++- capabilities/codebuddy/capability.json | 8 +- capabilities/codex/capability.json | 8 +- capabilities/copilot/capability.json | 9 +- capabilities/cursor/capability.json | 8 +- capabilities/drift/capability.json | 24 +- capabilities/gap-analysis/capability.json | 11 +- capabilities/gemini/capability.json | 14 +- capabilities/graphify/capability.json | 15 +- capabilities/hermes/capability.json | 8 +- capabilities/intel/capability.json | 19 +- capabilities/kilo/capability.json | 10 +- capabilities/kimi/capability.json | 13 +- capabilities/mempalace/capability.json | 78 ++++- capabilities/nyquist/capability.json | 31 +- capabilities/opencode/capability.json | 10 +- capabilities/pattern-mapper/capability.json | 35 ++- capabilities/profile-pipeline/capability.json | 19 +- capabilities/qwen/capability.json | 14 +- capabilities/research/capability.json | 31 +- capabilities/schema-gate/capability.json | 19 +- capabilities/security/capability.json | 43 ++- capabilities/tdd/capability.json | 11 +- capabilities/trae/capability.json | 8 +- capabilities/ui/capability.json | 96 +++++- capabilities/windsurf/capability.json | 8 +- docs/how-to/version-a-capability.md | 4 +- gsd-core/bin/lib/capability-registry.cjs | 192 ++++++++++++ scripts/gen-capability-registry.cjs | 98 ++++++ scripts/sync-manifest-versions.cjs | 82 ++++- tests/capability-manifest-version.test.cjs | 295 ++++++++++++++++++ tests/capability-registry.test.cjs | 6 +- .../issue-844-manifest-version-sync.test.cjs | 83 ++++- 40 files changed, 1321 insertions(+), 115 deletions(-) create mode 100644 .changeset/kind-dogs-dart.md create mode 100644 tests/capability-manifest-version.test.cjs diff --git a/.changeset/kind-dogs-dart.md b/.changeset/kind-dogs-dart.md new file mode 100644 index 000000000..f70851ca7 --- /dev/null +++ b/.changeset/kind-dogs-dart.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1436 +--- +**Capability manifests are now versioned** — every `capability.json` carries a required semver `version`, plus optional `engines.gsd`, `compatVersions`, `integrity` and `provenance` fields, enforced by the capability conformance validator. First-party capabilities are version-stamped in lockstep with the GSD release. Foundation (ADR-1244 Phase 1) for installing, upgrading, and removing capabilities in later releases. diff --git a/capabilities/ai-integration/capability.json b/capabilities/ai-integration/capability.json index 9649ec585..d029fad81 100644 --- a/capabilities/ai-integration/capability.json +++ b/capabilities/ai-integration/capability.json @@ -1,12 +1,23 @@ { "id": "ai-integration", "role": "feature", + "version": "1.5.1-dev.0", "title": "AI design contract", "description": "AI-SPEC design contract workflow for phases that build AI systems; owns the AI integration command, agents, and workflow.ai_integration_phase activation key.", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, - "skills": ["ai-integration-phase"], + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, + "skills": [ + "ai-integration-phase" + ], "agents": [ "gsd-framework-selector", "gsd-ai-researcher", @@ -24,9 +35,15 @@ "steps": [ { "point": "plan:pre", - "ref": { "skill": "ai-integration-phase" }, - "produces": ["AI-SPEC.md"], - "consumes": ["CONTEXT.md"], + "ref": { + "skill": "ai-integration-phase" + }, + "produces": [ + "AI-SPEC.md" + ], + "consumes": [ + "CONTEXT.md" + ], "when": "workflow.ai_integration_phase", "onError": "skip" } diff --git a/capabilities/antigravity/capability.json b/capabilities/antigravity/capability.json index ca14c4c25..dad076a55 100644 --- a/capabilities/antigravity/capability.json +++ b/capabilities/antigravity/capability.json @@ -1,17 +1,27 @@ { "id": "antigravity", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home-nested", "name": "antigravity", "parent": ".gemini", - "env": ["ANTIGRAVITY_CONFIG_DIR"], - "probe": ["antigravity", "antigravity-ide", "antigravity-cli"] + "env": [ + "ANTIGRAVITY_CONFIG_DIR" + ], + "probe": [ + "antigravity", + "antigravity-ide", + "antigravity-cli" + ] }, "configFormat": "settings-json", "artifactLayout": { diff --git a/capabilities/audit/capability.json b/capabilities/audit/capability.json index 508174ccb..7a19ee8cf 100644 --- a/capabilities/audit/capability.json +++ b/capabilities/audit/capability.json @@ -1,11 +1,20 @@ { "id": "audit", "role": "feature", + "version": "1.5.1-dev.0", "title": "Audit", "description": "Open-artifact audit and UAT-gap audit for milestone close gates; exposes `gsd-tools audit-uat` (cross-phase UAT outstanding items) and `gsd-tools audit-open` (structured open-artifact scan across debug, tasks, threads, todos, seeds, UAT, verification, context-questions).", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, "skills": [], "agents": [], "config": {}, diff --git a/capabilities/augment/capability.json b/capabilities/augment/capability.json index 591b86345..116f2f8c1 100644 --- a/capabilities/augment/capability.json +++ b/capabilities/augment/capability.json @@ -1,15 +1,21 @@ { "id": "augment", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".augment", - "env": ["AUGMENT_CONFIG_DIR"] + "env": [ + "AUGMENT_CONFIG_DIR" + ] }, "configFormat": "settings-json", "artifactLayout": { diff --git a/capabilities/claude/capability.json b/capabilities/claude/capability.json index f7a3770bd..3151c4cde 100644 --- a/capabilities/claude/capability.json +++ b/capabilities/claude/capability.json @@ -1,15 +1,21 @@ { "id": "claude", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".claude", - "env": ["CLAUDE_CONFIG_DIR"] + "env": [ + "CLAUDE_CONFIG_DIR" + ] }, "configFormat": "settings-json", "artifactLayout": { @@ -50,6 +56,11 @@ "installSurface": "settings-json", "writesSharedSettings": true, "permissionWriter": null, - "extendedHookEvents": ["SubagentStop", "Stop", "PreCompact", "FileChanged"] + "extendedHookEvents": [ + "SubagentStop", + "Stop", + "PreCompact", + "FileChanged" + ] } } diff --git a/capabilities/cline/capability.json b/capabilities/cline/capability.json index 81dadc8ee..8116f7d59 100644 --- a/capabilities/cline/capability.json +++ b/capabilities/cline/capability.json @@ -1,15 +1,21 @@ { "id": "cline", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".cline", - "env": ["CLINE_CONFIG_DIR"] + "env": [ + "CLINE_CONFIG_DIR" + ] }, "configFormat": "markdown-dir", "artifactLayout": { diff --git a/capabilities/code-review/capability.json b/capabilities/code-review/capability.json index 1713628f5..5a558f01c 100644 --- a/capabilities/code-review/capability.json +++ b/capabilities/code-review/capability.json @@ -1,13 +1,27 @@ { "id": "code-review", "role": "feature", + "version": "1.5.1-dev.0", "title": "Code review", "description": "Source-file code review and review-fix workflow support for completed execution work.", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, - "skills": ["code-review"], - "agents": ["gsd-code-reviewer", "gsd-code-fixer"], + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, + "skills": [ + "code-review" + ], + "agents": [ + "gsd-code-reviewer", + "gsd-code-fixer" + ], "hooks": [], "config": { "workflow.code_review": { @@ -17,7 +31,11 @@ }, "workflow.code_review_depth": { "type": "enum", - "values": ["quick", "standard", "deep"], + "values": [ + "quick", + "standard", + "deep" + ], "default": "standard", "description": "Default depth for code review when no --depth override is supplied." } @@ -25,9 +43,15 @@ "steps": [ { "point": "execute:post", - "ref": { "skill": "code-review" }, - "produces": ["REVIEW.md"], - "consumes": ["SUMMARY.md"], + "ref": { + "skill": "code-review" + }, + "produces": [ + "REVIEW.md" + ], + "consumes": [ + "SUMMARY.md" + ], "when": "workflow.code_review", "onError": "skip" } diff --git a/capabilities/codebuddy/capability.json b/capabilities/codebuddy/capability.json index 06f1d5999..930786fe9 100644 --- a/capabilities/codebuddy/capability.json +++ b/capabilities/codebuddy/capability.json @@ -1,15 +1,21 @@ { "id": "codebuddy", "role": "runtime", + "version": "1.5.1-dev.0", "title": "CodeBuddy", "description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".codebuddy", - "env": ["CODEBUDDY_CONFIG_DIR"] + "env": [ + "CODEBUDDY_CONFIG_DIR" + ] }, "configFormat": "settings-json", "artifactLayout": { diff --git a/capabilities/codex/capability.json b/capabilities/codex/capability.json index ef6c707c0..46fe4ebbb 100644 --- a/capabilities/codex/capability.json +++ b/capabilities/codex/capability.json @@ -1,15 +1,21 @@ { "id": "codex", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".codex", - "env": ["CODEX_HOME"] + "env": [ + "CODEX_HOME" + ] }, "configFormat": "toml", "artifactLayout": { diff --git a/capabilities/copilot/capability.json b/capabilities/copilot/capability.json index d8b03e3fd..5b42a3464 100644 --- a/capabilities/copilot/capability.json +++ b/capabilities/copilot/capability.json @@ -1,15 +1,22 @@ { "id": "copilot", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".copilot", - "env": ["COPILOT_CONFIG_DIR", "COPILOT_HOME"] + "env": [ + "COPILOT_CONFIG_DIR", + "COPILOT_HOME" + ] }, "configFormat": "markdown", "artifactLayout": { diff --git a/capabilities/cursor/capability.json b/capabilities/cursor/capability.json index e05a1e574..b08d547df 100644 --- a/capabilities/cursor/capability.json +++ b/capabilities/cursor/capability.json @@ -1,15 +1,21 @@ { "id": "cursor", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".cursor", - "env": ["CURSOR_CONFIG_DIR"] + "env": [ + "CURSOR_CONFIG_DIR" + ] }, "configFormat": "none", "artifactLayout": { diff --git a/capabilities/drift/capability.json b/capabilities/drift/capability.json index 29775e522..01b31bc88 100644 --- a/capabilities/drift/capability.json +++ b/capabilities/drift/capability.json @@ -1,11 +1,20 @@ { "id": "drift", "role": "feature", + "version": "1.5.1-dev.0", "title": "Drift detection gates", "description": "Post-execution drift detection gates that run after each wave completes. Provides two gates at execute:wave:post: a blocking schema drift gate (detects schema files changed without a database push) and a non-blocking codebase drift gate (detects structural additions not reflected in STRUCTURE.md).", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, "skills": [], "agents": [], "hooks": [], @@ -17,7 +26,10 @@ }, "workflow.drift_action": { "type": "enum", - "values": ["warn", "auto-remap"], + "values": [ + "warn", + "auto-remap" + ], "default": "warn", "description": "Action taken by the codebase drift gate when the threshold is exceeded: warn (advisory message) or auto-remap (spawn gsd-codebase-mapper agent to refresh STRUCTURE.md)." }, @@ -32,14 +44,18 @@ "gates": [ { "point": "execute:wave:post", - "check": { "query": "verify.schema-drift" }, + "check": { + "query": "verify.schema-drift" + }, "when": "workflow.schema_drift_gate", "blocking": true, "onError": "skip" }, { "point": "execute:wave:post", - "check": { "query": "verify.codebase-drift" }, + "check": { + "query": "verify.codebase-drift" + }, "when": "workflow.schema_drift_gate", "blocking": false, "onError": "skip" diff --git a/capabilities/gap-analysis/capability.json b/capabilities/gap-analysis/capability.json index 24d994f5c..6534113c8 100644 --- a/capabilities/gap-analysis/capability.json +++ b/capabilities/gap-analysis/capability.json @@ -1,11 +1,20 @@ { "id": "gap-analysis", "role": "feature", + "version": "1.5.1-dev.0", "title": "Post-planning gap analysis", "description": "Proactive, non-blocking post-planning coverage report. After all PLAN.md files are generated, cross-references every REQ-ID and D-ID from REQUIREMENTS.md and CONTEXT.md against plan bodies. Emits a Source | Item | Status table. Does not block phase advancement.", "tier": "standard", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, "skills": [], "agents": [], "hooks": [], diff --git a/capabilities/gemini/capability.json b/capabilities/gemini/capability.json index 197a1e1a2..4aa3a9af6 100644 --- a/capabilities/gemini/capability.json +++ b/capabilities/gemini/capability.json @@ -1,15 +1,21 @@ { "id": "gemini", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".gemini", - "env": ["GEMINI_CONFIG_DIR"] + "env": [ + "GEMINI_CONFIG_DIR" + ] }, "configFormat": "settings-json", "artifactLayout": { @@ -42,6 +48,10 @@ "installSurface": "settings-json", "writesSharedSettings": true, "permissionWriter": null, - "extendedHookEvents": ["BeforeAgent", "AfterAgent", "BeforeModel"] + "extendedHookEvents": [ + "BeforeAgent", + "AfterAgent", + "BeforeModel" + ] } } diff --git a/capabilities/graphify/capability.json b/capabilities/graphify/capability.json index 187fc53cb..1b3830c48 100644 --- a/capabilities/graphify/capability.json +++ b/capabilities/graphify/capability.json @@ -1,12 +1,23 @@ { "id": "graphify", "role": "feature", + "version": "1.5.1-dev.0", "title": "Knowledge graph", "description": "Build, query, and inspect the project knowledge graph in `.planning/graphs/`; exposes graphify CLI subcommands (build, query, status, diff) and the /gsd-graphify skill.", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, - "skills": ["graphify"], + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, + "skills": [ + "graphify" + ], "agents": [], "activationKey": "graphify.enabled", "config": { diff --git a/capabilities/hermes/capability.json b/capabilities/hermes/capability.json index 29bdfe327..cd195cdac 100644 --- a/capabilities/hermes/capability.json +++ b/capabilities/hermes/capability.json @@ -1,15 +1,21 @@ { "id": "hermes", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".hermes", - "env": ["HERMES_HOME"] + "env": [ + "HERMES_HOME" + ] }, "configFormat": "settings-json", "artifactLayout": { diff --git a/capabilities/intel/capability.json b/capabilities/intel/capability.json index ffd2b2ba6..0a5b9d3fc 100644 --- a/capabilities/intel/capability.json +++ b/capabilities/intel/capability.json @@ -1,11 +1,20 @@ { "id": "intel", "role": "feature", + "version": "1.5.1-dev.0", "title": "Codebase intelligence", "description": "Code-intelligence store for codebase querying, diff, snapshot, and API-surface extraction; exposes `gsd-tools intel` subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and backs `/gsd-map-codebase` and `gsd-intel-updater`.", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, "skills": [], "agents": [], "activationKey": "intel.enabled", @@ -27,8 +36,12 @@ "steps": [ { "point": "plan:pre", - "ref": { "command": "intel api-surface" }, - "produces": [".planning/intel/API-SURFACE.md"], + "ref": { + "command": "intel api-surface" + }, + "produces": [ + ".planning/intel/API-SURFACE.md" + ], "consumes": [], "when": "intel.enabled", "onError": "skip" diff --git a/capabilities/kilo/capability.json b/capabilities/kilo/capability.json index 890c623d2..3c3f0020a 100644 --- a/capabilities/kilo/capability.json +++ b/capabilities/kilo/capability.json @@ -1,15 +1,23 @@ { "id": "kilo", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "xdg", "name": "kilo", - "env": ["KILO_CONFIG_DIR", "KILO_CONFIG", "XDG_CONFIG_HOME"], + "env": [ + "KILO_CONFIG_DIR", + "KILO_CONFIG", + "XDG_CONFIG_HOME" + ], "skillsHome": { "kind": "dot-home", "name": ".kilo", diff --git a/capabilities/kimi/capability.json b/capabilities/kimi/capability.json index c9d225175..705c6bc5a 100644 --- a/capabilities/kimi/capability.json +++ b/capabilities/kimi/capability.json @@ -1,16 +1,25 @@ { "id": "kimi", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "generic-agents-root", "name": "agents", - "env": ["KIMI_CONFIG_DIR"], - "probe": ["~/.config/agents", "~/.agents"], + "env": [ + "KIMI_CONFIG_DIR" + ], + "probe": [ + "~/.config/agents", + "~/.agents" + ], "probeExists": "skills" }, "configFormat": "none", diff --git a/capabilities/mempalace/capability.json b/capabilities/mempalace/capability.json index 534eeecec..8e434a5f7 100644 --- a/capabilities/mempalace/capability.json +++ b/capabilities/mempalace/capability.json @@ -1,13 +1,27 @@ { "id": "mempalace", "role": "feature", + "version": "1.5.1-dev.0", "title": "MemPalace memory", "description": "Cross-session, cross-project memory: deliberate recall before discuss/plan and verbatim capture + temporal-KG sync at phase boundaries, via the MemPalace MCP server and CLI.", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, - "skills": ["mempalace-recall", "mempalace-capture"], - "agents": ["gsd-mempalace-curator"], + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, + "skills": [ + "mempalace-recall", + "mempalace-capture" + ], + "agents": [ + "gsd-mempalace-curator" + ], "hooks": [], "config": { "mempalace.enabled": { @@ -17,7 +31,11 @@ }, "mempalace.memory_mode": { "type": "enum", - "values": ["augment", "kg_backend", "replace"], + "values": [ + "augment", + "kg_backend", + "replace" + ], "default": "augment", "description": "How MemPalace relates to GSD native memory. Only 'augment' (additive) is implemented today; 'kg_backend' and 'replace' are forward-declared (routing seam not yet built) and currently behave as 'augment'." }, @@ -65,41 +83,63 @@ "steps": [ { "point": "discuss:post", - "ref": { "skill": "mempalace-capture" }, + "ref": { + "skill": "mempalace-capture" + }, "produces": [], - "consumes": ["CONTEXT.md"], + "consumes": [ + "CONTEXT.md" + ], "when": "mempalace.enabled", "onError": "skip" }, { "point": "plan:pre", - "ref": { "skill": "mempalace-recall" }, - "produces": ["MEMORY-RECALL.md"], - "consumes": ["CONTEXT.md"], + "ref": { + "skill": "mempalace-recall" + }, + "produces": [ + "MEMORY-RECALL.md" + ], + "consumes": [ + "CONTEXT.md" + ], "when": "mempalace.enabled", "onError": "skip" }, { "point": "plan:post", - "ref": { "skill": "mempalace-capture" }, + "ref": { + "skill": "mempalace-capture" + }, "produces": [], - "consumes": ["PLAN.md"], + "consumes": [ + "PLAN.md" + ], "when": "mempalace.enabled", "onError": "skip" }, { "point": "verify:post", - "ref": { "skill": "mempalace-capture" }, + "ref": { + "skill": "mempalace-capture" + }, "produces": [], - "consumes": ["SUMMARY.md"], + "consumes": [ + "SUMMARY.md" + ], "when": "mempalace.enabled", "onError": "skip" }, { "point": "ship:post", - "ref": { "agent": "gsd-mempalace-curator" }, + "ref": { + "agent": "gsd-mempalace-curator" + }, "produces": [], - "consumes": ["UAT.md"], + "consumes": [ + "UAT.md" + ], "when": "mempalace.enabled", "onError": "skip" } @@ -108,7 +148,9 @@ { "point": "discuss:pre", "into": "orchestrator", - "fragment": { "path": "fragments/recall-discuss.md" }, + "fragment": { + "path": "fragments/recall-discuss.md" + }, "produces": [], "consumes": [], "when": "mempalace.enabled", @@ -117,7 +159,9 @@ { "point": "execute:wave:post", "into": "verifier", - "fragment": { "path": "fragments/capture-problems.md" }, + "fragment": { + "path": "fragments/capture-problems.md" + }, "produces": [], "consumes": [], "when": "mempalace.enabled", diff --git a/capabilities/nyquist/capability.json b/capabilities/nyquist/capability.json index cad899c21..f04f51176 100644 --- a/capabilities/nyquist/capability.json +++ b/capabilities/nyquist/capability.json @@ -1,13 +1,26 @@ { "id": "nyquist", "role": "feature", + "version": "1.5.1-dev.0", "title": "Nyquist validation", "description": "Validation coverage audit that maps executed work back to tests and manual-only evidence.", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, - "skills": ["validate-phase"], - "agents": ["gsd-nyquist-auditor"], + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, + "skills": [ + "validate-phase" + ], + "agents": [ + "gsd-nyquist-auditor" + ], "hooks": [], "config": { "workflow.nyquist_validation": { @@ -19,9 +32,15 @@ "steps": [ { "point": "verify:post", - "ref": { "skill": "validate-phase" }, - "produces": ["VALIDATION.md"], - "consumes": ["SUMMARY.md"], + "ref": { + "skill": "validate-phase" + }, + "produces": [ + "VALIDATION.md" + ], + "consumes": [ + "SUMMARY.md" + ], "when": "workflow.nyquist_validation", "onError": "halt" } diff --git a/capabilities/opencode/capability.json b/capabilities/opencode/capability.json index a00c26281..0ccfebaeb 100644 --- a/capabilities/opencode/capability.json +++ b/capabilities/opencode/capability.json @@ -1,15 +1,23 @@ { "id": "opencode", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "xdg", "name": "opencode", - "env": ["OPENCODE_CONFIG_DIR", "OPENCODE_CONFIG", "XDG_CONFIG_HOME"] + "env": [ + "OPENCODE_CONFIG_DIR", + "OPENCODE_CONFIG", + "XDG_CONFIG_HOME" + ] }, "configFormat": "settings-json", "artifactLayout": { diff --git a/capabilities/pattern-mapper/capability.json b/capabilities/pattern-mapper/capability.json index cadfa9e62..ce3fda24e 100644 --- a/capabilities/pattern-mapper/capability.json +++ b/capabilities/pattern-mapper/capability.json @@ -1,13 +1,26 @@ { "id": "pattern-mapper", "role": "feature", + "version": "1.5.1-dev.0", "title": "Pattern mapping", "description": "Optional codebase-pattern mapping before planning; owns the pattern mapper agent and workflow.pattern_mapper activation key.", "tier": "full", - "requires": ["research"], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, + "requires": [ + "research" + ], + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, "skills": [], - "agents": ["gsd-pattern-mapper"], + "agents": [ + "gsd-pattern-mapper" + ], "hooks": [], "config": { "workflow.pattern_mapper": { @@ -19,10 +32,18 @@ "steps": [ { "point": "plan:pre", - "ref": { "agent": "gsd-pattern-mapper" }, - "fragment": { "path": "fragments/plan-pre.md" }, - "produces": ["PATTERNS.md"], - "consumes": ["RESEARCH.md"], + "ref": { + "agent": "gsd-pattern-mapper" + }, + "fragment": { + "path": "fragments/plan-pre.md" + }, + "produces": [ + "PATTERNS.md" + ], + "consumes": [ + "RESEARCH.md" + ], "when": "workflow.pattern_mapper", "onError": "skip" } diff --git a/capabilities/profile-pipeline/capability.json b/capabilities/profile-pipeline/capability.json index f772ac287..7861e6e4b 100644 --- a/capabilities/profile-pipeline/capability.json +++ b/capabilities/profile-pipeline/capability.json @@ -1,13 +1,26 @@ { "id": "profile-pipeline", "role": "feature", + "version": "1.5.1-dev.0", "title": "Developer profiling pipeline", "description": "Developer behavioral profiling from Claude Code session history; scans session JSONL files, extracts and samples user messages, and generates profile artifacts (USER-PROFILE.md, dev-preferences.md, CLAUDE.md sections). Exposes eight `gsd-tools` commands: scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase). Backs the /gsd-profile-user skill and gsd-user-profiler agent.", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, - "skills": ["profile-user"], - "agents": ["gsd-user-profiler"], + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, + "skills": [ + "profile-user" + ], + "agents": [ + "gsd-user-profiler" + ], "config": { "profile-pipeline.enabled": { "type": "boolean", diff --git a/capabilities/qwen/capability.json b/capabilities/qwen/capability.json index bdc5e77df..dd0fbaabb 100644 --- a/capabilities/qwen/capability.json +++ b/capabilities/qwen/capability.json @@ -1,15 +1,21 @@ { "id": "qwen", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".qwen", - "env": ["QWEN_CONFIG_DIR"] + "env": [ + "QWEN_CONFIG_DIR" + ] }, "configFormat": "settings-json", "artifactLayout": { @@ -42,6 +48,10 @@ "installSurface": "settings-json", "writesSharedSettings": true, "permissionWriter": null, - "extendedHookEvents": ["SubagentStop", "Stop", "PreCompact"] + "extendedHookEvents": [ + "SubagentStop", + "Stop", + "PreCompact" + ] } } diff --git a/capabilities/research/capability.json b/capabilities/research/capability.json index 51138366e..f1c3909da 100644 --- a/capabilities/research/capability.json +++ b/capabilities/research/capability.json @@ -1,13 +1,24 @@ { "id": "research", "role": "feature", + "version": "1.5.1-dev.0", "title": "Phase research", "description": "Optional phase research before planning; owns the phase researcher agent and workflow.research activation key.", "tier": "standard", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, "skills": [], - "agents": ["gsd-phase-researcher"], + "agents": [ + "gsd-phase-researcher" + ], "hooks": [], "config": { "workflow.research": { @@ -19,10 +30,18 @@ "steps": [ { "point": "plan:pre", - "ref": { "agent": "gsd-phase-researcher" }, - "fragment": { "path": "fragments/plan-pre.md" }, - "produces": ["RESEARCH.md"], - "consumes": ["CONTEXT.md"], + "ref": { + "agent": "gsd-phase-researcher" + }, + "fragment": { + "path": "fragments/plan-pre.md" + }, + "produces": [ + "RESEARCH.md" + ], + "consumes": [ + "CONTEXT.md" + ], "when": "workflow.research", "onError": "skip" } diff --git a/capabilities/schema-gate/capability.json b/capabilities/schema-gate/capability.json index a2660e667..0a3a638b8 100644 --- a/capabilities/schema-gate/capability.json +++ b/capabilities/schema-gate/capability.json @@ -1,11 +1,20 @@ { "id": "schema-gate", "role": "feature", + "version": "1.5.1-dev.0", "title": "Schema push detection gate", "description": "Detects ORM schema-relevant files in the phase scope during planning and injects a mandatory [BLOCKING] schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, "skills": [], "agents": [], "hooks": [], @@ -21,9 +30,13 @@ { "point": "plan:pre", "into": "planner", - "fragment": { "path": "fragments/plan-pre.md" }, + "fragment": { + "path": "fragments/plan-pre.md" + }, "produces": [], - "consumes": ["CONTEXT.md"], + "consumes": [ + "CONTEXT.md" + ], "when": "workflow.schema_push_detection", "onError": "skip" } diff --git a/capabilities/security/capability.json b/capabilities/security/capability.json index b11948761..2d17a5a06 100644 --- a/capabilities/security/capability.json +++ b/capabilities/security/capability.json @@ -1,13 +1,26 @@ { "id": "security", "role": "feature", + "version": "1.5.1-dev.0", "title": "Security enforcement", "description": "Threat mitigation verification and ship-time security blocking for phases with security enforcement enabled.", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, - "skills": ["secure-phase"], - "agents": ["gsd-security-auditor"], + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, + "skills": [ + "secure-phase" + ], + "agents": [ + "gsd-security-auditor" + ], "hooks": [], "config": { "workflow.security_enforcement": { @@ -22,7 +35,13 @@ }, "workflow.security_block_on": { "type": "enum", - "values": ["critical", "high", "medium", "low", "none"], + "values": [ + "critical", + "high", + "medium", + "low", + "none" + ], "default": "high", "description": "Minimum open threat severity that blocks advancement." } @@ -30,9 +49,15 @@ "steps": [ { "point": "verify:post", - "ref": { "skill": "secure-phase" }, - "produces": ["SECURITY.md"], - "consumes": ["SUMMARY.md"], + "ref": { + "skill": "secure-phase" + }, + "produces": [ + "SECURITY.md" + ], + "consumes": [ + "SUMMARY.md" + ], "when": "workflow.security_enforcement", "onError": "halt" } @@ -49,7 +74,9 @@ "security_block_on": "workflow.security_block_on" }, "produces": [], - "consumes": ["CONTEXT.md"], + "consumes": [ + "CONTEXT.md" + ], "when": "workflow.security_enforcement" } ], diff --git a/capabilities/tdd/capability.json b/capabilities/tdd/capability.json index c931b9da6..4e6c15b22 100644 --- a/capabilities/tdd/capability.json +++ b/capabilities/tdd/capability.json @@ -1,11 +1,20 @@ { "id": "tdd", "role": "feature", + "version": "1.5.1-dev.0", "title": "Test-driven development", "description": "Injects TDD heuristics into the planner and enforces RED/GREEN gate compliance on type:tdd plans after execution. Owns workflow.tdd_mode; the --tdd CLI flag is the ephemeral override.", "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, "skills": [], "agents": [], "hooks": [], diff --git a/capabilities/trae/capability.json b/capabilities/trae/capability.json index 53fc8962b..4015c7d5f 100644 --- a/capabilities/trae/capability.json +++ b/capabilities/trae/capability.json @@ -1,15 +1,21 @@ { "id": "trae", "role": "runtime", + "version": "1.5.1-dev.0", "title": "Trae IDE", "description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.", "tier": "core", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", "name": ".trae", - "env": ["TRAE_CONFIG_DIR"] + "env": [ + "TRAE_CONFIG_DIR" + ] }, "configFormat": "none", "artifactLayout": { diff --git a/capabilities/ui/capability.json b/capabilities/ui/capability.json index 16c8ea9f8..75dc81922 100644 --- a/capabilities/ui/capability.json +++ b/capabilities/ui/capability.json @@ -1,23 +1,95 @@ { - "id": "ui", "role": "feature", "title": "UI design contracts", + "id": "ui", + "role": "feature", + "version": "1.5.1-dev.0", + "title": "UI design contracts", "description": "UI-SPEC design contract + retrospective UI audit for frontend phases.", - "tier": "full", "requires": [], - "runtimeCompat": { "supported": ["*"], "unsupported": [] }, - "skills": ["ui-phase", "ui-review"], - "agents": ["gsd-ui-checker", "gsd-ui-auditor"], + "tier": "full", + "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, + "runtimeCompat": { + "supported": [ + "*" + ], + "unsupported": [] + }, + "skills": [ + "ui-phase", + "ui-review" + ], + "agents": [ + "gsd-ui-checker", + "gsd-ui-auditor" + ], "hooks": [], "config": { - "workflow.ui_phase": { "type": "boolean", "default": true, "description": "Enable the UI design-contract gate during planning." }, - "workflow.ui_review": { "type": "boolean", "default": true, "description": "Enable the retrospective UI audit." }, - "workflow.ui_safety_gate": { "type": "boolean", "default": true, "description": "Block execution on unmet UI-SPEC contracts." } + "workflow.ui_phase": { + "type": "boolean", + "default": true, + "description": "Enable the UI design-contract gate during planning." + }, + "workflow.ui_review": { + "type": "boolean", + "default": true, + "description": "Enable the retrospective UI audit." + }, + "workflow.ui_safety_gate": { + "type": "boolean", + "default": true, + "description": "Block execution on unmet UI-SPEC contracts." + } }, "steps": [ - { "point": "plan:pre", "ref": { "skill": "ui-phase" }, "produces": ["UI-SPEC.md"], "consumes": ["CONTEXT.md"], "when": "workflow.ui_phase", "onError": "skip" }, - { "point": "verify:post", "ref": { "skill": "ui-review" }, "produces": ["UI-REVIEW.md"], "consumes": ["UI-SPEC.md"], "when": "workflow.ui_review", "onError": "skip" } + { + "point": "plan:pre", + "ref": { + "skill": "ui-phase" + }, + "produces": [ + "UI-SPEC.md" + ], + "consumes": [ + "CONTEXT.md" + ], + "when": "workflow.ui_phase", + "onError": "skip" + }, + { + "point": "verify:post", + "ref": { + "skill": "ui-review" + }, + "produces": [ + "UI-REVIEW.md" + ], + "consumes": [ + "UI-SPEC.md" + ], + "when": "workflow.ui_review", + "onError": "skip" + } ], "contributions": [], "gates": [ - { "point": "plan:pre", "check": { "query": "ui.plan-gate" }, "when": "workflow.ui_safety_gate", "blocking": true, "onError": "halt" }, - { "point": "execute:wave:post", "check": { "query": "ui.safety-gate" }, "when": "workflow.ui_safety_gate", "blocking": true, "onError": "halt" } + { + "point": "plan:pre", + "check": { + "query": "ui.plan-gate" + }, + "when": "workflow.ui_safety_gate", + "blocking": true, + "onError": "halt" + }, + { + "point": "execute:wave:post", + "check": { + "query": "ui.safety-gate" + }, + "when": "workflow.ui_safety_gate", + "blocking": true, + "onError": "halt" + } ] } diff --git a/capabilities/windsurf/capability.json b/capabilities/windsurf/capability.json index a3e74028e..1c95f874c 100644 --- a/capabilities/windsurf/capability.json +++ b/capabilities/windsurf/capability.json @@ -1,16 +1,22 @@ { "id": "windsurf", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home-nested", "name": "windsurf", "parent": ".codeium", - "env": ["WINDSURF_CONFIG_DIR"] + "env": [ + "WINDSURF_CONFIG_DIR" + ] }, "configFormat": "none", "artifactLayout": { diff --git a/docs/how-to/version-a-capability.md b/docs/how-to/version-a-capability.md index 9fe58b48b..2e2fa3af0 100644 --- a/docs/how-to/version-a-capability.md +++ b/docs/how-to/version-a-capability.md @@ -28,6 +28,8 @@ Set the version in your manifest before every release: } ``` +> **First-party capabilities are versioned automatically.** The native capabilities shipped inside GSD (`capabilities//capability.json`) are stamped in lockstep with the GSD package version at release time by `scripts/sync-manifest-versions.cjs` — their `version` always equals the GSD version, so per-capability semver and `compatVersions` only carry independent signal for **third-party** capabilities. As an author of a third-party capability, you own your own version line; the lockstep rule does not apply to you. + ### Decide when to raise `engines.gsd` The `engines.gsd` range expresses which GSD host versions your capability is compatible with. GSD enforces this as a hard gate at install time and again at load time. @@ -137,5 +139,5 @@ If the new version of a capability requires a GSD version newer than what you ha - [How to remove or disable a capability](remove-a-capability.md) - [Develop a Capability for GSD 1.5+](develop-a-capability.md) -- [Capability manifest reference](../reference/capability-matrix.md) +- [Capability manifest reference](../reference/capability-manifest.md) - [Turn a capability off (and keep it off)](turn-a-capability-off.md) diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs index 0efba5681..e7d79fd2c 100644 --- a/gsd-core/bin/lib/capability-registry.cjs +++ b/gsd-core/bin/lib/capability-registry.cjs @@ -10,10 +10,14 @@ const capabilities = { "ai-integration": { "id": "ai-integration", "role": "feature", + "version": "1.5.1-dev.0", "title": "AI design contract", "description": "AI-SPEC design contract workflow for phases that build AI systems; owns the AI integration command, agents, and workflow.ai_integration_phase activation key.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -59,10 +63,14 @@ const capabilities = { "antigravity": { "id": "antigravity", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home-nested", @@ -114,10 +122,14 @@ const capabilities = { "audit": { "id": "audit", "role": "feature", + "version": "1.5.1-dev.0", "title": "Audit", "description": "Open-artifact audit and UAT-gap audit for milestone close gates; exposes `gsd-tools audit-uat` (cross-phase UAT outstanding items) and `gsd-tools audit-open` (structured open-artifact scan across debug, tasks, threads, todos, seeds, UAT, verification, context-questions).", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -147,10 +159,14 @@ const capabilities = { "augment": { "id": "augment", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -212,10 +228,14 @@ const capabilities = { "claude": { "id": "claude", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -274,10 +294,14 @@ const capabilities = { "cline": { "id": "cline", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -313,10 +337,14 @@ const capabilities = { "code-review": { "id": "code-review", "role": "feature", + "version": "1.5.1-dev.0", "title": "Code review", "description": "Source-file code review and review-fix workflow support for completed execution work.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -370,10 +398,14 @@ const capabilities = { "codebuddy": { "id": "codebuddy", "role": "runtime", + "version": "1.5.1-dev.0", "title": "CodeBuddy", "description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -435,10 +467,14 @@ const capabilities = { "codex": { "id": "codex", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -484,10 +520,14 @@ const capabilities = { "copilot": { "id": "copilot", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -533,10 +573,14 @@ const capabilities = { "cursor": { "id": "cursor", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -598,10 +642,14 @@ const capabilities = { "drift": { "id": "drift", "role": "feature", + "version": "1.5.1-dev.0", "title": "Drift detection gates", "description": "Post-execution drift detection gates that run after each wave completes. Provides two gates at execute:wave:post: a blocking schema drift gate (detects schema files changed without a database push) and a non-blocking codebase drift gate (detects structural additions not reflected in STRUCTURE.md).", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -658,10 +706,14 @@ const capabilities = { "gap-analysis": { "id": "gap-analysis", "role": "feature", + "version": "1.5.1-dev.0", "title": "Post-planning gap analysis", "description": "Proactive, non-blocking post-planning coverage report. After all PLAN.md files are generated, cross-references every REQ-ID and D-ID from REQUIREMENTS.md and CONTEXT.md against plan bodies. Emits a Source | Item | Status table. Does not block phase advancement.", "tier": "standard", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -695,10 +747,14 @@ const capabilities = { "gemini": { "id": "gemini", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -748,10 +804,14 @@ const capabilities = { "graphify": { "id": "graphify", "role": "feature", + "version": "1.5.1-dev.0", "title": "Knowledge graph", "description": "Build, query, and inspect the project knowledge graph in `.planning/graphs/`; exposes graphify CLI subcommands (build, query, status, diff) and the /gsd-graphify skill.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -785,10 +845,14 @@ const capabilities = { "hermes": { "id": "hermes", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -834,10 +898,14 @@ const capabilities = { "intel": { "id": "intel", "role": "feature", + "version": "1.5.1-dev.0", "title": "Codebase intelligence", "description": "Code-intelligence store for codebase querying, diff, snapshot, and API-surface extraction; exposes `gsd-tools intel` subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and backs `/gsd-map-codebase` and `gsd-intel-updater`.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -882,10 +950,14 @@ const capabilities = { "kilo": { "id": "kilo", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "xdg", @@ -953,10 +1025,14 @@ const capabilities = { "kimi": { "id": "kimi", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "generic-agents-root", @@ -1005,10 +1081,14 @@ const capabilities = { "mempalace": { "id": "mempalace", "role": "feature", + "version": "1.5.1-dev.0", "title": "MemPalace memory", "description": "Cross-session, cross-project memory: deliberate recall before discuss/plan and verbatim capture + temporal-KG sync at phase boundaries, via the MemPalace MCP server and CLI.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -1175,10 +1255,14 @@ const capabilities = { "nyquist": { "id": "nyquist", "role": "feature", + "version": "1.5.1-dev.0", "title": "Nyquist validation", "description": "Validation coverage audit that maps executed work back to tests and manual-only evidence.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -1221,10 +1305,14 @@ const capabilities = { "opencode": { "id": "opencode", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "xdg", @@ -1287,12 +1375,16 @@ const capabilities = { "pattern-mapper": { "id": "pattern-mapper", "role": "feature", + "version": "1.5.1-dev.0", "title": "Pattern mapping", "description": "Optional codebase-pattern mapping before planning; owns the pattern mapper agent and workflow.pattern_mapper activation key.", "tier": "full", "requires": [ "research" ], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -1337,10 +1429,14 @@ const capabilities = { "profile-pipeline": { "id": "profile-pipeline", "role": "feature", + "version": "1.5.1-dev.0", "title": "Developer profiling pipeline", "description": "Developer behavioral profiling from Claude Code session history; scans session JSONL files, extracts and samples user messages, and generates profile artifacts (USER-PROFILE.md, dev-preferences.md, CLAUDE.md sections). Exposes eight `gsd-tools` commands: scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase). Backs the /gsd-profile-user skill and gsd-user-profiler agent.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -1410,10 +1506,14 @@ const capabilities = { "qwen": { "id": "qwen", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -1463,10 +1563,14 @@ const capabilities = { "research": { "id": "research", "role": "feature", + "version": "1.5.1-dev.0", "title": "Phase research", "description": "Optional phase research before planning; owns the phase researcher agent and workflow.research activation key.", "tier": "standard", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -1511,10 +1615,14 @@ const capabilities = { "schema-gate": { "id": "schema-gate", "role": "feature", + "version": "1.5.1-dev.0", "title": "Schema push detection gate", "description": "Detects ORM schema-relevant files in the phase scope during planning and injects a mandatory [BLOCKING] schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -1553,10 +1661,14 @@ const capabilities = { "security": { "id": "security", "role": "feature", + "version": "1.5.1-dev.0", "title": "Security enforcement", "description": "Threat mitigation verification and ship-time security blocking for phases with security enforcement enabled.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -1648,10 +1760,14 @@ const capabilities = { "tdd": { "id": "tdd", "role": "feature", + "version": "1.5.1-dev.0", "title": "Test-driven development", "description": "Injects TDD heuristics into the planner and enforces RED/GREEN gate compliance on type:tdd plans after execution. Owns workflow.tdd_mode; the --tdd CLI flag is the ephemeral override.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -1697,10 +1813,14 @@ const capabilities = { "trae": { "id": "trae", "role": "runtime", + "version": "1.5.1-dev.0", "title": "Trae IDE", "description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.", "tier": "core", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -1745,10 +1865,14 @@ const capabilities = { "ui": { "id": "ui", "role": "feature", + "version": "1.5.1-dev.0", "title": "UI design contracts", "description": "UI-SPEC design contract + retrospective UI audit for frontend phases.", "tier": "full", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtimeCompat": { "supported": [ "*" @@ -1836,10 +1960,14 @@ const capabilities = { "windsurf": { "id": "windsurf", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home-nested", @@ -2592,10 +2720,14 @@ const runtimes = { "antigravity": { "id": "antigravity", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home-nested", @@ -2647,10 +2779,14 @@ const runtimes = { "augment": { "id": "augment", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -2712,10 +2848,14 @@ const runtimes = { "claude": { "id": "claude", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -2774,10 +2914,14 @@ const runtimes = { "cline": { "id": "cline", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -2813,10 +2957,14 @@ const runtimes = { "codebuddy": { "id": "codebuddy", "role": "runtime", + "version": "1.5.1-dev.0", "title": "CodeBuddy", "description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.", "tier": "core", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -2878,10 +3026,14 @@ const runtimes = { "codex": { "id": "codex", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -2927,10 +3079,14 @@ const runtimes = { "copilot": { "id": "copilot", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -2976,10 +3132,14 @@ const runtimes = { "cursor": { "id": "cursor", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -3041,10 +3201,14 @@ const runtimes = { "gemini": { "id": "gemini", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -3094,10 +3258,14 @@ const runtimes = { "hermes": { "id": "hermes", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -3143,10 +3311,14 @@ const runtimes = { "kilo": { "id": "kilo", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "xdg", @@ -3214,10 +3386,14 @@ const runtimes = { "kimi": { "id": "kimi", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "generic-agents-root", @@ -3266,10 +3442,14 @@ const runtimes = { "opencode": { "id": "opencode", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "xdg", @@ -3332,10 +3512,14 @@ const runtimes = { "qwen": { "id": "qwen", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -3385,10 +3569,14 @@ const runtimes = { "trae": { "id": "trae", "role": "runtime", + "version": "1.5.1-dev.0", "title": "Trae IDE", "description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.", "tier": "core", "requires": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home", @@ -3433,10 +3621,14 @@ const runtimes = { "windsurf": { "id": "windsurf", "role": "runtime", + "version": "1.5.1-dev.0", "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": [], + "engines": { + "gsd": ">=1.6.0" + }, "runtime": { "configHome": { "kind": "dot-home-nested", diff --git a/scripts/gen-capability-registry.cjs b/scripts/gen-capability-registry.cjs index 83135fdf5..b277a9d32 100644 --- a/scripts/gen-capability-registry.cjs +++ b/scripts/gen-capability-registry.cjs @@ -236,6 +236,96 @@ const VALID_TIERS = new Set(['core', 'standard', 'full']); const VALID_ON_ERROR = new Set(['skip', 'halt']); const RUNTIME_COMPAT_WILDCARD = '*'; +// ── ADR-1244 D1: versioned-manifest envelope ───────────────────────────────── +// Official strict SemVer 2.0.0 grammar (https://semver.org). Rejects partials +// ("1.0"), prefixes ("v1.0.0"), leading-zero segments ("01.2.3"), numeric +// prerelease identifiers with leading zeros ("1.2.3-01"), empty identifiers +// ("1.2.3-..") and — critically — prerelease/build identifiers containing +// anything outside [0-9A-Za-z-] (so a version can never smuggle shell +// metacharacters, spaces or unicode into a downstream `git tag v` or +// path). Accepts "1.2.3-dev.0", "1.2.3-rc.1", "1.2.3+build.5". +const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; +// Permissive *shape* check for a semver range (engines.gsd / compatVersions +// values). Range SATISFACTION is enforced by the runtime overlay (ADR-1244 D2); +// here we only reject empty/garbage and shell metacharacters. +const SEMVER_RANGE_RE = /^[0-9A-Za-z.\-+ |<>=~^*()]+$/; +// Subresource-integrity hash: "sha512-" + base64 of a 64-byte digest (86 base64 +// chars + "==" padding). Exact length so malformed pins ("sha512-abc") fail. +const SHA512_INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]{86}==$/; + +// A syntactically plausible semver range (shape-only — see SEMVER_RANGE_RE). +// Requires a digit or a bare wildcard so pure-alpha garbage ("abcx", "()x") is +// rejected; full range satisfaction is the runtime overlay's job (ADR-1244 D2). +function isPlausibleRange(s) { + if (typeof s !== 'string') return false; + const t = s.trim(); + if (t.length === 0 || !SEMVER_RANGE_RE.test(s)) return false; + return /\d/.test(t) || t === '*' || t === 'x' || t === 'X'; +} + +/** + * ADR-1244 D1: validate the versioned-manifest envelope. + * - version REQUIRED semver string (the registry rejects a manifest + * without one). + * - engines optional object; engines.gsd optional semver-range string. + * - compatVersions optional object mapping a capability version (semver) to a + * gsd version range. + * - integrity optional "sha512-" string. + * - provenance optional { sourceRepo, commit } strings. + * + * Shape only — range satisfaction and integrity verification are enforced by + * the source resolver / runtime overlay (ADR-1244 D2/D3). + * + * @param {object} cap The parsed JSON object. + * @returns {string[]} Array of error strings; empty = valid. + */ +function validateVersionEnvelope(cap) { + const errors = []; + + if (typeof cap.version !== 'string' || !SEMVER_RE.test(cap.version)) { + errors.push('version must be a semver string (e.g. "1.2.3"); got: ' + JSON.stringify(cap.version)); + } + + if (cap.engines !== undefined) { + if (typeof cap.engines !== 'object' || cap.engines === null || Array.isArray(cap.engines)) { + errors.push('engines must be an object (e.g. { "gsd": ">=1.6.0" })'); + } else if (cap.engines.gsd !== undefined && !isPlausibleRange(cap.engines.gsd)) { + errors.push('engines.gsd must be a semver range string; got: ' + JSON.stringify(cap.engines.gsd)); + } + } + + if (cap.compatVersions !== undefined) { + if (typeof cap.compatVersions !== 'object' || cap.compatVersions === null || Array.isArray(cap.compatVersions)) { + errors.push('compatVersions must be an object mapping capability versions to gsd version ranges'); + } else { + for (const [k, v] of Object.entries(cap.compatVersions)) { + if (!SEMVER_RE.test(k)) errors.push('compatVersions key "' + k + '" must be a semver string'); + if (!isPlausibleRange(v)) errors.push('compatVersions["' + k + '"] must be a semver range string'); + } + } + } + + if (cap.integrity !== undefined && (typeof cap.integrity !== 'string' || !SHA512_INTEGRITY_RE.test(cap.integrity))) { + errors.push('integrity must be a "sha512-" string'); + } + + if (cap.provenance !== undefined) { + const p = cap.provenance; + if (typeof p !== 'object' || p === null || Array.isArray(p)) { + errors.push('provenance must be an object { sourceRepo, commit }'); + } else { + if (typeof p.sourceRepo !== 'string' || p.sourceRepo.length === 0) { + errors.push('provenance.sourceRepo must be a non-empty string'); + } + if (typeof p.commit !== 'string' || p.commit.length === 0) { + errors.push('provenance.commit must be a non-empty string'); + } + } + } + + return errors; +} + /** * Validate a single capability declaration. * @@ -285,6 +375,9 @@ function validateCapability(cap, folderId) { } } + // ── Versioned-manifest envelope (ADR-1244 D1) ────────────────────────────── + errors.push(...validateVersionEnvelope(cap)); + // ── Role-specific body ──────────────────────────────────────────────────── if (cap.role === 'feature') { @@ -2580,6 +2673,11 @@ function main() { module.exports = { validateCapability, + // ADR-1244 D1: versioned-manifest envelope validation (reused by the runtime overlay, D2) + validateVersionEnvelope, + SEMVER_RE, + SEMVER_RANGE_RE, + SHA512_INTEGRITY_RE, validateAgainstContract, validateConsumesGlobal, validateCrossCapability, diff --git a/scripts/sync-manifest-versions.cjs b/scripts/sync-manifest-versions.cjs index f110b492e..81dc17ed0 100644 --- a/scripts/sync-manifest-versions.cjs +++ b/scripts/sync-manifest-versions.cjs @@ -68,6 +68,66 @@ function findDrift(opts) { return drift; } +// ─── ADR-1244 D6: native capability manifests ──────────────────────────────── +// +// Native capabilities (capabilities//capability.json) carry a `version` +// stamped in lockstep with the package version at release. Unlike +// VERSIONED_MANIFESTS (fixed paths), capabilities are discovered by glob so a +// new capability is auto-covered without editing this file. The version-sync +// regression guard (issue #844) treats every swept capability manifest as +// registered. + +// Discover capabilities//capability.json under `root`, sorted for stable +// staging order. Returns [] when there is no capabilities/ directory. +function listCapabilityManifests(opts) { + const root = (opts && opts.root) || ROOT; + const dir = path.join(root, 'capabilities'); + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return []; + } + return entries + .filter((e) => e.isDirectory()) + // Forward-slash rel paths (NOT path.join) so they match `git ls-files` + // output, git pathspecs, and the forward-slash VERSIONED_MANIFESTS on every + // platform — path.join would emit backslashes on Windows and break the + // issue-844 regression guard's ALLOWED-set comparison. + .map((e) => 'capabilities/' + e.name + '/capability.json') + .filter((rel) => fs.existsSync(path.join(root, rel))) + .sort(); +} + +// Stamp `version` into each native capability manifest. Returns changed rel paths. +function syncCapabilityVersions(opts) { + const root = (opts && opts.root) || ROOT; + const v = (opts && opts.version) != null ? opts.version : getPackageVersion(root); + const changed = []; + for (const rel of listCapabilityManifests({ root })) { + const abs = path.join(root, rel); + const manifest = readJson(abs); + if (manifest.version !== v) { + manifest.version = v; + fs.writeFileSync(abs, JSON.stringify(manifest, null, 2) + '\n'); + changed.push(rel); + } + } + return changed; +} + +// Native capability manifests whose version != package version. +function findCapabilityDrift(opts) { + const root = (opts && opts.root) || ROOT; + const v = (opts && opts.version) != null ? opts.version : getPackageVersion(root); + const drift = []; + for (const rel of listCapabilityManifests({ root })) { + const found = readJson(path.join(root, rel)).version; + if (found !== v) drift.push({ manifest: rel, found, expected: v }); + } + return drift; +} + // Best-effort outside git; fail-closed inside a work tree so a release never // ships a stale manifest that the working-tree test already accepted. function stageManifests(opts) { @@ -83,21 +143,32 @@ function stageManifests(opts) { console.warn('sync-manifest-versions: not a git work tree; skipping staging.'); return; } + const toStage = [...VERSIONED_MANIFESTS, ...listCapabilityManifests({ root })]; try { - execFileSync('git', ['add', '--', ...VERSIONED_MANIFESTS], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] }); + execFileSync('git', ['add', '--', ...toStage], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] }); } catch (err) { const detail = err && err.stderr ? err.stderr.toString().trim() : (err && err.message) || 'unknown error'; throw new Error(`sync-manifest-versions: failed to git-add manifests inside a work tree: ${detail}`); } } -module.exports = { VERSIONED_MANIFESTS, syncManifestVersions, findDrift, getPackageVersion, stageManifests }; +module.exports = { + VERSIONED_MANIFESTS, + syncManifestVersions, + findDrift, + getPackageVersion, + stageManifests, + // ADR-1244 D6: native capability version sweep + listCapabilityManifests, + syncCapabilityVersions, + findCapabilityDrift, +}; if (require.main === module) { const args = process.argv.slice(2); const version = getPackageVersion(); if (args.includes('--check')) { - const drift = findDrift({ version }); + const drift = [...findDrift({ version }), ...findCapabilityDrift({ version })]; if (drift.length) { for (const d of drift) { console.error('Manifest ' + d.manifest + ' version ' + d.found + ' != package.json ' + d.expected); @@ -105,10 +176,11 @@ if (require.main === module) { console.error('Run `node scripts/sync-manifest-versions.cjs` to fix.'); process.exitCode = 1; } else { - console.log('All ' + VERSIONED_MANIFESTS.length + ' versioned manifests in sync at ' + version + '.'); + const total = VERSIONED_MANIFESTS.length + listCapabilityManifests().length; + console.log('All ' + total + ' versioned manifests in sync at ' + version + '.'); } } else { - const changed = syncManifestVersions({ version }); + const changed = [...syncManifestVersions({ version }), ...syncCapabilityVersions({ version })]; if (changed.length) { console.log('Stamped ' + version + ' into: ' + changed.join(', ')); } else { diff --git a/tests/capability-manifest-version.test.cjs b/tests/capability-manifest-version.test.cjs new file mode 100644 index 000000000..3427f74c5 --- /dev/null +++ b/tests/capability-manifest-version.test.cjs @@ -0,0 +1,295 @@ +'use strict'; + +/** + * Phase 1 (ADR-1244 / issue #1430): versioned capability manifest. + * + * The build-time validator in scripts/gen-capability-registry.cjs must: + * - REQUIRE a semver `version` on every capability (the registry rejects a + * manifest without one — ADR-1244 D1). + * - Shape-validate the optional ecosystem envelope fields `engines`, + * `compatVersions`, `integrity`, `provenance` when present. + * + * Every native capabilities//capability.json must carry a valid `version` + * and `engines.gsd` (the conformance / parity requirement: the build fails when + * a native manifest lacks a version). + * + * These are behavioral tests against the exported validator + generator + * pipeline — no source-grep. They mirror the harness in + * tests/capability-registry.test.cjs (makeTempCapDir + loadAndValidate + + * buildRegistry). + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const helpers = require(path.join(__dirname, 'helpers.cjs')); +const { + validateCapability, + loadAndValidate, + buildRegistry, + SEMVER_RE, +} = require(path.join(ROOT, 'scripts', 'gen-capability-registry.cjs')); + +const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version; +const CAPABILITIES_DIR = path.join(ROOT, 'capabilities'); + +// Single source of truth: the validator's own strict-semver regex. +const SEMVER = SEMVER_RE; + +// ─── Minimal, otherwise-valid fixtures ─────────────────────────────────────── + +function featureCap(overrides) { + return { + id: 'demo', + role: 'feature', + version: '1.2.3', + title: 'Demo', + description: 'A demo capability.', + tier: 'standard', + requires: [], + engines: { gsd: '>=1.6.0' }, + runtimeCompat: { supported: ['*'], unsupported: [] }, + skills: [], + agents: [], + hooks: [], + config: {}, + steps: [], + contributions: [], + gates: [], + ...overrides, + }; +} + +function runtimeCap(overrides) { + return { + id: 'demo-rt', + role: 'runtime', + version: '1.2.3', + title: 'Demo RT', + description: 'A demo runtime.', + tier: 'standard', + requires: [], + engines: { gsd: '>=1.6.0' }, + runtime: { + configHome: { kind: 'dot-home', name: '.demo', env: [] }, + configFormat: 'settings-json', + artifactLayout: { global: [], local: [] }, + commandStyle: 'slash-hyphen', + hooksSurface: 'settings-json', + sandboxTier: 'none', + supportTier: 2, + installSurface: 'settings-json', + writesSharedSettings: false, + permissionWriter: null, + extendedHookEvents: [], + }, + ...overrides, + }; +} + +function makeTempCapDir(capabilities) { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-ver-test-')); + for (const [id, cap] of Object.entries(capabilities)) { + const subDir = path.join(tmpDir, id); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(subDir, 'capability.json'), JSON.stringify(cap), 'utf8'); + } + return tmpDir; +} + +// ─── Sanity: the base fixtures are valid as-is ─────────────────────────────── + +describe('version envelope — base fixtures are valid', () => { + test('a feature cap with a valid version passes validation', () => { + assert.deepEqual(validateCapability(featureCap(), 'demo'), []); + }); + test('a runtime cap with a valid version passes validation', () => { + assert.deepEqual(validateCapability(runtimeCap(), 'demo-rt'), []); + }); +}); + +// ─── version is required + semver ──────────────────────────────────────────── + +describe('version is required and must be semver', () => { + test('missing version is rejected (feature)', () => { + const { version: _v, ...cap } = featureCap(); + const errors = validateCapability(cap, 'demo'); + assert.ok(errors.some((e) => e.includes('version')), `expected a version error, got: ${JSON.stringify(errors)}`); + }); + + test('missing version is rejected (runtime)', () => { + const { version: _v, ...cap } = runtimeCap(); + const errors = validateCapability(cap, 'demo-rt'); + assert.ok(errors.some((e) => e.includes('version')), `expected a version error, got: ${JSON.stringify(errors)}`); + }); + + test('empty-string version is rejected', () => { + const errors = validateCapability(featureCap({ version: '' }), 'demo'); + assert.ok(errors.some((e) => e.includes('version'))); + }); + + test('whitespace-only version is rejected', () => { + const errors = validateCapability(featureCap({ version: ' ' }), 'demo'); + assert.ok(errors.some((e) => e.includes('version'))); + }); + + test('non-semver versions are rejected', () => { + for (const bad of ['1.0', 'v1.0.0', '1.0.0.0', '1', 'latest', '1.x', '1.0.0 ', '01.2.3']) { + const errors = validateCapability(featureCap({ version: bad }), 'demo'); + assert.ok(errors.some((e) => e.includes('version')), `expected "${bad}" to be rejected`); + } + }); + + test('malformed/hostile prerelease & build identifiers are rejected (strict semver)', () => { + // The prerelease/build suffix must be dot-separated [0-9A-Za-z-] identifiers + // with no leading-zero numerics and no empty segments — so a version can + // never smuggle shell metacharacters, spaces or unicode downstream. + for (const bad of ['1.2.3-01', '1.2.3-..', '1.2.3-', '1.2.3+', '1.2.3-foo bar', '1.2.3-$(whoami)', '1.2.3-`id`', '1.2.3-😈', '1.2.3+build meta']) { + const errors = validateCapability(featureCap({ version: bad }), 'demo'); + assert.ok(errors.some((e) => e.includes('version')), `expected hostile suffix "${bad}" to be rejected`); + } + }); + + test('non-string version is rejected', () => { + for (const bad of [123, null, {}, ['1.0.0']]) { + const errors = validateCapability(featureCap({ version: bad }), 'demo'); + assert.ok(errors.some((e) => e.includes('version')), `expected ${JSON.stringify(bad)} to be rejected`); + } + }); + + test('valid semver versions (incl. prerelease/build) pass', () => { + for (const ok of ['1.0.0', '0.0.1', '10.20.30', '1.2.3-dev.0', '1.2.3-rc.1', '1.2.3+build.5', PKG_VERSION]) { + const errors = validateCapability(featureCap({ version: ok }), 'demo'); + assert.deepEqual(errors, [], `expected "${ok}" to pass, got: ${JSON.stringify(errors)}`); + } + }); + + test('hostile version strings are rejected (shell metachars, newline, unicode)', () => { + for (const bad of ['1.0.0; rm -rf /', '1.0.0\n2.0.0', '1.0.0$(whoami)', '१.२.३', '1.0.0`id`']) { + const errors = validateCapability(featureCap({ version: bad }), 'demo'); + assert.ok(errors.some((e) => e.includes('version')), `expected hostile "${bad}" to be rejected`); + } + }); +}); + +// ─── engines (optional; shape-validated) ───────────────────────────────────── + +describe('engines is optional but shape-validated when present', () => { + test('omitting engines is valid', () => { + const { engines: _e, ...cap } = featureCap(); + assert.deepEqual(validateCapability(cap, 'demo'), []); + }); + + test('engines must be an object', () => { + for (const bad of ['>=1.6.0', 123, ['gsd'], null]) { + const errors = validateCapability(featureCap({ engines: bad }), 'demo'); + assert.ok(errors.some((e) => e.includes('engines')), `expected engines=${JSON.stringify(bad)} rejected`); + } + }); + + test('engines.gsd must be a non-empty range string', () => { + for (const bad of ['', ' ', 123, {}, 'not a range!!', '>=1.0.0; rm -rf', 'abcx', '()x']) { + const errors = validateCapability(featureCap({ engines: { gsd: bad } }), 'demo'); + assert.ok(errors.some((e) => e.includes('engines')), `expected engines.gsd=${JSON.stringify(bad)} rejected`); + } + }); + + test('valid engines.gsd ranges pass', () => { + for (const ok of ['>=1.6.0', '>=1.6.0 <3.0.0', '^1.0.0', '~1.2.0', '1.x', '*', '>=1.6.0 || >=2.0.0']) { + const errors = validateCapability(featureCap({ engines: { gsd: ok } }), 'demo'); + assert.deepEqual(errors, [], `expected range "${ok}" to pass, got: ${JSON.stringify(errors)}`); + } + }); +}); + +// ─── compatVersions / integrity / provenance (optional; shape-validated) ────── + +describe('optional ecosystem envelope fields are shape-validated', () => { + test('compatVersions must be an object of semver→range strings', () => { + assert.deepEqual(validateCapability(featureCap({ compatVersions: { '1.0.0': '>=1.6.0' } }), 'demo'), []); + for (const bad of ['x', 123, { '1.0.0': 5 }, { 'not-semver': '>=1.6.0' }]) { + const errors = validateCapability(featureCap({ compatVersions: bad }), 'demo'); + assert.ok(errors.some((e) => e.includes('compatVersions')), `expected compatVersions=${JSON.stringify(bad)} rejected`); + } + }); + + test('integrity must be sha512-', () => { + const good = 'sha512-' + 'a'.repeat(86) + '=='; + assert.deepEqual(validateCapability(featureCap({ integrity: good }), 'demo'), []); + for (const bad of ['abc', 'sha256-deadbeef', 'sha512-', 'sha512-abc', 'sha512-' + 'a'.repeat(40) + '==', 123, 'sha512-not base64!!']) { + const errors = validateCapability(featureCap({ integrity: bad }), 'demo'); + assert.ok(errors.some((e) => e.includes('integrity')), `expected integrity=${JSON.stringify(bad)} rejected`); + } + }); + + test('provenance must be { sourceRepo, commit } strings', () => { + assert.deepEqual(validateCapability(featureCap({ provenance: { sourceRepo: 'https://x/y', commit: 'abc123' } }), 'demo'), []); + for (const bad of ['x', 123, { sourceRepo: 5, commit: 'c' }, { sourceRepo: 'r' }, { commit: 'c' }]) { + const errors = validateCapability(featureCap({ provenance: bad }), 'demo'); + assert.ok(errors.some((e) => e.includes('provenance')), `expected provenance=${JSON.stringify(bad)} rejected`); + } + }); +}); + +// ─── Registry pass-through ──────────────────────────────────────────────────── + +describe('generated registry preserves version + engines', () => { + test('buildRegistry carries version and engines onto the capability object', (t) => { + const capDir = makeTempCapDir({ demo: featureCap({ id: 'demo', version: '2.5.0', engines: { gsd: '>=1.6.0 <2.0.0' } }) }); + t.after(() => helpers.cleanup(capDir)); + + const { capMap, errors } = loadAndValidate(new Set(), capDir); + assert.deepEqual(errors, [], `loadAndValidate errors: ${JSON.stringify(errors)}`); + const registry = buildRegistry(capMap); + assert.equal(registry.capabilities.demo.version, '2.5.0'); + assert.equal(registry.capabilities.demo.engines.gsd, '>=1.6.0 <2.0.0'); + }); + + test('loadAndValidate rejects a capability dir whose manifest lacks a version', (t) => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-ver-noversion-')); + t.after(() => helpers.cleanup(tmpDir)); + const sub = path.join(tmpDir, 'demo'); + fs.mkdirSync(sub, { recursive: true }); + const { version: _v, ...noVersion } = featureCap({ id: 'demo' }); + fs.writeFileSync(path.join(sub, 'capability.json'), JSON.stringify(noVersion), 'utf8'); + + const { errors } = loadAndValidate(new Set(), tmpDir); + assert.ok(errors.some((e) => e.includes('version')), `expected a version error, got: ${JSON.stringify(errors)}`); + }); +}); + +// ─── Native manifest conformance (the ADR-1244 parity requirement) ──────────── + +describe('every native capability.json carries a valid version + engines.gsd', () => { + const ids = fs + .readdirSync(CAPABILITIES_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + + test('there are native capabilities to check', () => { + assert.ok(ids.length >= 30, `expected the full native capability set, found ${ids.length}`); + }); + + for (const id of ids) { + test(`capabilities/${id}/capability.json has a semver version`, () => { + const cap = JSON.parse(fs.readFileSync(path.join(CAPABILITIES_DIR, id, 'capability.json'), 'utf8')); + assert.equal(typeof cap.version, 'string', `${id}: version must be a string`); + assert.ok(SEMVER.test(cap.version), `${id}: version "${cap.version}" must be semver`); + }); + + test(`capabilities/${id}/capability.json declares engines.gsd`, () => { + const cap = JSON.parse(fs.readFileSync(path.join(CAPABILITIES_DIR, id, 'capability.json'), 'utf8')); + assert.ok(cap.engines && typeof cap.engines.gsd === 'string' && cap.engines.gsd.length > 0, + `${id}: engines.gsd must be a non-empty string`); + }); + + test(`capabilities/${id}/capability.json passes validateCapability`, () => { + const cap = JSON.parse(fs.readFileSync(path.join(CAPABILITIES_DIR, id, 'capability.json'), 'utf8')); + assert.deepEqual(validateCapability(cap, id), [], `${id}: native manifest must validate`); + }); + } +}); diff --git a/tests/capability-registry.test.cjs b/tests/capability-registry.test.cjs index 15610aeb4..830cee607 100644 --- a/tests/capability-registry.test.cjs +++ b/tests/capability-registry.test.cjs @@ -1470,6 +1470,7 @@ describe('S1: fragment.path traversal guard', () => { JSON.stringify({ id: 'planning-advice', role: 'feature', + version: '1.0.0', title: 'Planning advice', description: 'Synthetic fixture for fragment path materialization.', tier: 'full', @@ -1515,6 +1516,7 @@ describe('S1: fragment.path traversal guard', () => { JSON.stringify({ id: 'research', role: 'feature', + version: '1.0.0', title: 'Research', description: 'Synthetic fixture for step fragment materialization.', tier: 'standard', @@ -1702,7 +1704,7 @@ describe('C3: role:runtime body validation', () => { // 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', + id: 'cursor', role: 'runtime', version: '1.0.0', title: 'Cursor', description: 'Cursor IDE runtime', tier: 'standard', requires: [], runtime: { configHome: { kind: 'dot-home', name: '.cursor', env: ['CURSOR_CONFIG_DIR'] }, @@ -2886,6 +2888,7 @@ function makeCommandCap(id, commands) { return { id, role: 'feature', + version: '1.0.0', title: 'Test cap ' + id, description: 'Synthetic capability for ADR-959 command tests.', tier: 'full', @@ -3085,6 +3088,7 @@ function makeRuntimeCap(overrides) { return { id: 'test-rt', role: 'runtime', + version: '1.0.0', title: 'Test Runtime', description: 'A synthetic runtime capability for testing.', tier: 'core', diff --git a/tests/issue-844-manifest-version-sync.test.cjs b/tests/issue-844-manifest-version-sync.test.cjs index 30afe0e56..7bca6dd7d 100644 --- a/tests/issue-844-manifest-version-sync.test.cjs +++ b/tests/issue-844-manifest-version-sync.test.cjs @@ -27,6 +27,8 @@ const { syncManifestVersions, getPackageVersion, stageManifests, + listCapabilityManifests, + syncCapabilityVersions, } = require(path.join(ROOT, 'scripts', 'sync-manifest-versions.cjs')); // ─── A: RED→GREEN repro via temp fixture ───────────────────────────────────── @@ -172,12 +174,91 @@ describe('B: real manifests match package.json version', () => { } }); +// ─── B2: native capability manifests track package.json version (ADR-1244 D6) ─ +describe('B2: native capability manifests match package.json version', () => { + + const pkgVersion = getPackageVersion(ROOT); + const capManifests = listCapabilityManifests({ root: ROOT }); + + test('there is at least one native capability manifest', () => { + assert.ok(capManifests.length >= 30, `expected the native capability set, found ${capManifests.length}`); + }); + + for (const rel of capManifests) { + test(`${rel} version === ${pkgVersion}`, () => { + const m = JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8')); + assert.equal( + m.version, + pkgVersion, + `${rel} version (${m.version}) must match package.json version (${pkgVersion}). ` + + 'Run `node scripts/sync-manifest-versions.cjs` to fix.' + ); + }); + } +}); + +// ─── B3: syncCapabilityVersions stamps + is idempotent (temp fixture) ───────── +describe('B3: syncCapabilityVersions — temp fixture', () => { + + test('stamps stale capability manifests to package version, then is idempotent', () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-844-cap-')); + try { + fs.writeFileSync( + path.join(tmpRoot, 'package.json'), + JSON.stringify({ name: 'x', version: '9.9.9-test.0' }, null, 2) + '\n' + ); + // Two stale capability manifests. + for (const id of ['alpha', 'beta']) { + const dir = path.join(tmpRoot, 'capabilities', id); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync( + path.join(dir, 'capability.json'), + JSON.stringify({ id, role: 'feature', version: '0.0.0', title: id }, null, 2) + '\n' + ); + } + + const found = listCapabilityManifests({ root: tmpRoot }); + assert.equal(found.length, 2, 'should discover both capability manifests'); + + const changed = syncCapabilityVersions({ root: tmpRoot }); + assert.equal(changed.length, 2, 'both manifests should be stamped on first run'); + for (const rel of found) { + const m = JSON.parse(fs.readFileSync(path.join(tmpRoot, rel), 'utf8')); + assert.equal(m.version, '9.9.9-test.0', `${rel} should be stamped`); + assert.equal(m.title, m.id, `${rel} non-version fields preserved`); + } + + // Idempotent second run. + assert.deepEqual(syncCapabilityVersions({ root: tmpRoot }), [], 'second run is a no-op'); + } finally { + helpers.cleanup(tmpRoot); + } + }); + + test('listCapabilityManifests returns [] when there is no capabilities/ dir', () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-844-nocaps-')); + try { + assert.deepEqual(listCapabilityManifests({ root: tmpRoot }), []); + } finally { + helpers.cleanup(tmpRoot); + } + }); +}); + // ─── C: Regression guard — all version-bearing JSON files are registered ────── describe('C: regression guard — version-bearing JSON files must be registered', () => { // package.json is the version source; package-lock.json is npm-managed. // Both inherently track the version without the sync script. - const ALLOWED = new Set([...VERSIONED_MANIFESTS, 'package.json', 'package-lock.json']); + // Native capability manifests (capabilities//capability.json) are + // version-swept by syncCapabilityVersions (ADR-1244 D6) — discovered by glob, + // so every one is "registered" without an explicit entry here. + const ALLOWED = new Set([ + ...VERSIONED_MANIFESTS, + ...listCapabilityManifests({ root: ROOT }), + 'package.json', + 'package-lock.json', + ]); // Semver-ish: matches X.Y.Z with optional pre-release/build metadata. const SEMVER = /^\d+\.\d+\.\d+(?:[-+].+)?$/;