@opengsd/gsd-core 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +45 -1
  4. package/README.md +2 -0
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debug-session-manager.md +78 -4
  8. package/agents/gsd-debugger.md +87 -29
  9. package/agents/gsd-executor.md +49 -9
  10. package/agents/gsd-intel-updater.md +3 -3
  11. package/agents/gsd-phase-researcher.md +4 -2
  12. package/agents/gsd-plan-checker.md +20 -0
  13. package/agents/gsd-planner.md +44 -59
  14. package/agents/gsd-project-researcher.md +2 -2
  15. package/agents/gsd-ui-auditor.md +0 -40
  16. package/agents/gsd-verifier.md +2 -2
  17. package/bin/install.js +1338 -135
  18. package/commands/gsd/ai-integration-phase.md +1 -1
  19. package/commands/gsd/mempalace-capture.md +9 -5
  20. package/commands/gsd/new-milestone.md +1 -1
  21. package/commands/gsd/plan-phase.md +5 -3
  22. package/commands/gsd/plan-review-convergence.md +7 -2
  23. package/gsd-core/bin/gsd-tools.cjs +2690 -2472
  24. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  25. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  26. package/gsd-core/bin/lib/api-coverage.cjs +360 -53
  27. package/gsd-core/bin/lib/audit.cjs +8 -8
  28. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  29. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  30. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  31. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  32. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  33. package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
  34. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  35. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  36. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  37. package/gsd-core/bin/lib/check-command-router.cjs +140 -27
  38. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  39. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
  41. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  42. package/gsd-core/bin/lib/commands.cjs +326 -21
  43. package/gsd-core/bin/lib/config-loader.cjs +214 -30
  44. package/gsd-core/bin/lib/config.cjs +158 -22
  45. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  46. package/gsd-core/bin/lib/decisions.cjs +32 -8
  47. package/gsd-core/bin/lib/docs.cjs +6 -0
  48. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  49. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  50. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  51. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  52. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  53. package/gsd-core/bin/lib/init.cjs +155 -66
  54. package/gsd-core/bin/lib/install-engine.cjs +299 -23
  55. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  56. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  57. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  58. package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
  59. package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
  60. package/gsd-core/bin/lib/milestone.cjs +248 -14
  61. package/gsd-core/bin/lib/model-catalog.cjs +69 -4
  62. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  63. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  64. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  65. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  66. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  67. package/gsd-core/bin/lib/phase-id.cjs +304 -9
  68. package/gsd-core/bin/lib/phase.cjs +258 -17
  69. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  70. package/gsd-core/bin/lib/plan-scan.cjs +70 -2
  71. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  72. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  73. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  74. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  75. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  76. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  77. package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
  78. package/gsd-core/bin/lib/roadmap.cjs +23 -7
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
  82. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  83. package/gsd-core/bin/lib/smart-entry.cjs +70 -5
  84. package/gsd-core/bin/lib/state-document.cjs +171 -24
  85. package/gsd-core/bin/lib/state-transition.cjs +50 -11
  86. package/gsd-core/bin/lib/state.cjs +206 -32
  87. package/gsd-core/bin/lib/surface.cjs +51 -9
  88. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  89. package/gsd-core/bin/lib/uat.cjs +428 -11
  90. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  91. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  92. package/gsd-core/bin/lib/validate.cjs +44 -8
  93. package/gsd-core/bin/lib/verification.cjs +163 -31
  94. package/gsd-core/bin/lib/verify.cjs +348 -42
  95. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  96. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  97. package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
  98. package/gsd-core/bin/shared/model-catalog.json +5 -0
  99. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  100. package/gsd-core/references/api-coverage.md +37 -7
  101. package/gsd-core/references/checkpoints.md +1 -1
  102. package/gsd-core/references/common-bug-patterns.md +13 -0
  103. package/gsd-core/references/context-budget.md +40 -0
  104. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  105. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  106. package/gsd-core/references/debugger-philosophy.md +1 -0
  107. package/gsd-core/references/debugger-prevention.md +98 -0
  108. package/gsd-core/references/debugger-rca-branching.md +98 -0
  109. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  110. package/gsd-core/references/debugger-sbfl.md +110 -0
  111. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  112. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  113. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  114. package/gsd-core/references/execute-phase-response-language.md +7 -0
  115. package/gsd-core/references/gate-prompts.md +6 -3
  116. package/gsd-core/references/model-profile-resolution.md +64 -13
  117. package/gsd-core/references/offer-next.md +88 -0
  118. package/gsd-core/references/planner-antipatterns.md +6 -0
  119. package/gsd-core/references/planner-mvp-mode.md +12 -13
  120. package/gsd-core/references/planner-preconditions.md +156 -0
  121. package/gsd-core/references/planner-reversibility.md +132 -0
  122. package/gsd-core/references/planning-config.md +2 -1
  123. package/gsd-core/references/reviewer-instances.md +28 -19
  124. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  125. package/gsd-core/references/skeleton-template.md +1 -1
  126. package/gsd-core/references/thinking-models-planning.md +3 -1
  127. package/gsd-core/references/ui-consideration-probe.md +2 -2
  128. package/gsd-core/references/worktree-branch-check.md +4 -4
  129. package/gsd-core/templates/DEBUG.md +5 -3
  130. package/gsd-core/templates/summary-minimal.md +4 -0
  131. package/gsd-core/templates/summary-standard.md +4 -0
  132. package/gsd-core/templates/summary.md +7 -0
  133. package/gsd-core/workflows/add-phase.md +2 -0
  134. package/gsd-core/workflows/add-tests.md +3 -1
  135. package/gsd-core/workflows/add-todo.md +32 -1
  136. package/gsd-core/workflows/ai-integration-phase.md +8 -6
  137. package/gsd-core/workflows/audit-fix.md +6 -2
  138. package/gsd-core/workflows/audit-milestone.md +8 -0
  139. package/gsd-core/workflows/autonomous.md +19 -15
  140. package/gsd-core/workflows/check-todos.md +5 -3
  141. package/gsd-core/workflows/cleanup.md +7 -1
  142. package/gsd-core/workflows/code-review-fix.md +14 -6
  143. package/gsd-core/workflows/code-review.md +93 -24
  144. package/gsd-core/workflows/complete-milestone.md +3 -0
  145. package/gsd-core/workflows/debug.md +35 -7
  146. package/gsd-core/workflows/diagnose-issues.md +5 -1
  147. package/gsd-core/workflows/discovery-phase.md +7 -0
  148. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  149. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  150. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  151. package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
  152. package/gsd-core/workflows/discuss-phase.md +2 -2
  153. package/gsd-core/workflows/do.md +7 -1
  154. package/gsd-core/workflows/docs-update.md +9 -0
  155. package/gsd-core/workflows/eval-review.md +4 -1
  156. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  157. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  158. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  159. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  160. package/gsd-core/workflows/execute-phase.md +110 -149
  161. package/gsd-core/workflows/execute-plan.md +20 -8
  162. package/gsd-core/workflows/explore.md +4 -0
  163. package/gsd-core/workflows/extract-learnings.md +21 -0
  164. package/gsd-core/workflows/graduation.md +3 -0
  165. package/gsd-core/workflows/health.md +7 -1
  166. package/gsd-core/workflows/help/modes/full.md +9 -5
  167. package/gsd-core/workflows/import.md +11 -2
  168. package/gsd-core/workflows/inbox.md +7 -0
  169. package/gsd-core/workflows/ingest-docs.md +19 -10
  170. package/gsd-core/workflows/manager.md +3 -1
  171. package/gsd-core/workflows/map-codebase.md +17 -10
  172. package/gsd-core/workflows/mvp-phase.md +3 -0
  173. package/gsd-core/workflows/new-milestone.md +79 -23
  174. package/gsd-core/workflows/new-project.md +28 -19
  175. package/gsd-core/workflows/new-workspace.md +3 -1
  176. package/gsd-core/workflows/next.md +5 -2
  177. package/gsd-core/workflows/onboard.md +3 -0
  178. package/gsd-core/workflows/plan-phase.md +56 -51
  179. package/gsd-core/workflows/plan-review-convergence.md +61 -12
  180. package/gsd-core/workflows/plant-seed.md +3 -0
  181. package/gsd-core/workflows/profile-user.md +7 -1
  182. package/gsd-core/workflows/progress.md +31 -3
  183. package/gsd-core/workflows/quick.md +33 -10
  184. package/gsd-core/workflows/remove-workspace.md +3 -0
  185. package/gsd-core/workflows/review.md +172 -585
  186. package/gsd-core/workflows/scan.md +10 -2
  187. package/gsd-core/workflows/secure-phase.md +13 -2
  188. package/gsd-core/workflows/settings-integrations.md +3 -0
  189. package/gsd-core/workflows/settings.md +3 -0
  190. package/gsd-core/workflows/ship.md +88 -11
  191. package/gsd-core/workflows/sketch.md +3 -0
  192. package/gsd-core/workflows/smart-entry.md +4 -1
  193. package/gsd-core/workflows/spike.md +7 -1
  194. package/gsd-core/workflows/ui-phase.md +11 -2
  195. package/gsd-core/workflows/ui-review.md +11 -1
  196. package/gsd-core/workflows/undo.md +7 -0
  197. package/gsd-core/workflows/update.md +106 -5
  198. package/gsd-core/workflows/validate-phase.md +13 -2
  199. package/gsd-core/workflows/verify-phase.md +2 -2
  200. package/gsd-core/workflows/verify-work.md +15 -4
  201. package/hooks/dist/gsd-context-monitor.js +27 -9
  202. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  203. package/hooks/dist/gsd-cursor-stop.js +6 -2
  204. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  205. package/hooks/dist/gsd-graphify-update.sh +9 -0
  206. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  207. package/hooks/dist/gsd-prompt-guard.js +101 -2
  208. package/hooks/dist/gsd-read-guard.js +100 -2
  209. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  210. package/hooks/dist/gsd-statusline.js +97 -9
  211. package/hooks/dist/gsd-workflow-guard.js +110 -6
  212. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  213. package/hooks/dist/lib/cursor-workspace.js +74 -0
  214. package/hooks/gsd-context-monitor.js +27 -9
  215. package/hooks/gsd-cursor-session-start.js +6 -2
  216. package/hooks/gsd-cursor-stop.js +6 -2
  217. package/hooks/gsd-cursor-subagent-start.js +6 -2
  218. package/hooks/gsd-graphify-update.sh +9 -0
  219. package/hooks/gsd-phase-boundary.sh +14 -2
  220. package/hooks/gsd-prompt-guard.js +101 -2
  221. package/hooks/gsd-read-guard.js +100 -2
  222. package/hooks/gsd-read-injection-scanner.js +109 -2
  223. package/hooks/gsd-statusline.js +97 -9
  224. package/hooks/gsd-workflow-guard.js +110 -6
  225. package/hooks/gsd-worktree-path-guard.js +132 -8
  226. package/hooks/lib/cursor-workspace.js +74 -0
  227. package/package.json +10 -8
  228. package/pi/gsd.cjs +34 -3
  229. package/scripts/changeset/lint.cjs +1 -0
  230. package/scripts/changeset/parse.cjs +26 -0
  231. package/scripts/check-coverage-gate.cjs +51 -0
  232. package/scripts/check-glossary-refs.cjs +244 -0
  233. package/scripts/ci-rebase-check.cjs +48 -4
  234. package/scripts/ci-test-scope.cjs +67 -17
  235. package/scripts/gen-adr-index.cjs +528 -0
  236. package/scripts/gen-capability-matrix.cjs +26 -2
  237. package/scripts/gen-capability-registry.cjs +132 -34
  238. package/scripts/gen-emitted-baseline.cjs +145 -0
  239. package/scripts/gen-test-timings.cjs +201 -0
  240. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  241. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  242. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  243. package/scripts/lint-portable-timeout.cjs +140 -0
  244. package/scripts/lint-resolution-provenance.cjs +9 -0
  245. package/scripts/lint-test-file-count.allowlist.json +1 -0
  246. package/scripts/mutation-matrix.cjs +4 -0
  247. package/scripts/prompt-injection-scan.sh +6 -0
  248. package/scripts/registry-schema.cjs +57 -8
  249. package/scripts/release-notes/conventional-title.cjs +19 -1
  250. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  251. package/scripts/release-tarball-smoke.cjs +18 -11
  252. package/scripts/run-tests.cjs +420 -58
  253. package/scripts/workflow-size.cjs +16 -8
  254. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  255. package/skills/gsd-mempalace-capture/SKILL.md +9 -5
  256. package/skills/gsd-new-milestone/SKILL.md +1 -1
  257. package/skills/gsd-plan-phase/SKILL.md +5 -3
  258. package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
  259. package/vscode/package.json +1 -1
  260. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  261. package/scripts/update-size-baseline.cjs +0 -68
@@ -16,41 +16,108 @@
16
16
  * detected. The instance→cli mapping lives HERE (single source; see the parity
17
17
  * test in tests/review-reviewer-instances.test.cjs — DEFECT.GENERATIVE-FIX).
18
18
  *
19
- * KNOWN_REVIEWER_SLUGS (post-review #2092): registry-derived, not a flat
20
- * hand-maintained array. Each capability-runtime descriptor that is a valid
21
- * reviewer CLI declares `runtime.hostBehaviors.reviewerCli: true`
22
- * (capabilities/<id>/capability.json); this module reads that flag off the
23
- * generated capability-registry.cjs at require-time. A handful of reviewer
24
- * CLIs are NOT install-time runtimes at all (no capabilities/<id>/ descriptor
25
- * exists) — those stay a small hardcoded tail:
26
- * - `gemini` — hook-event dialect name only (see runtime-hooks-surface.cts);
27
- * the Gemini CLI reviewer is not an installable runtime (#1928 folded
28
- * gemini into antigravity's descriptor).
29
- * - `coderabbit` / `ollama` / `lm_studio` / `llama_cpp` — third-party
30
- * review/model CLIs with no GSD install surface at all.
19
+ * KNOWN_REVIEWER_SLUGS (ADR-2782 D9, Phase 5a #2798): derived from declared
20
+ * `reviewer` bodies in the capability registry, not a hand-maintained tail.
21
+ * A capability of EITHER `role: "runtime"` (the six dual-purpose hosts —
22
+ * antigravity/claude/codex/cursor/opencode/qwen) or the lane-only
23
+ * `role: "reviewer"` (gemini/coderabbit/ollama/lm_studio/llama_cpp) may carry
24
+ * a `reviewer` body, and it is the body's `reviewer.slug` — NOT the capability
25
+ * id — that becomes the roster entry: the two differ for `lm-studio` (id) /
26
+ * `lm_studio` (slug) and `llama-cpp` (id) / `llama_cpp` (slug), ADR-2782's
27
+ * three-namespace trap (`id` must be kebab; `slug` keeps the shipped roster's
28
+ * snake form).
29
+ *
30
+ * `runtime.hostBehaviors.reviewerCli: true` survives as a DERIVED LEGACY ALIAS
31
+ * for one release (D9): a capability that only sets the flag (no `reviewer`
32
+ * body yet) still contributes its capability id as a slug, exactly as before
33
+ * this phase. Where a capability carries BOTH a declared body and the alias,
34
+ * the BODY WINS — that capability contributes only the body's slug, never
35
+ * both. Alias removal is a named later phase (#2801), not done here.
36
+ *
37
+ * Before this phase the five non-runtime reviewers (`gemini`, `coderabbit`,
38
+ * `ollama`, `lm_studio`, `llama_cpp`) had no `capabilities/<id>/` descriptor at
39
+ * all and were a hardcoded `NON_RUNTIME_REVIEWER_SLUGS` tail. Phase 5a gives
40
+ * each one a lane-only `role: "reviewer"` capability (ADR-2782 D3), so the
41
+ * tail is deleted outright. See
42
+ * `docs/adr/2782-reviewer-lane-capability-surface.md`.
31
43
  */
32
44
  Object.defineProperty(exports, "__esModule", { value: true });
33
45
  exports.INSTANCE_NAME_PATTERN = exports.KNOWN_REVIEWER_SLUGS = void 0;
46
+ exports.deriveReviewerSlugs = deriveReviewerSlugs;
34
47
  exports.normalizeConfiguredDefaultReviewers = normalizeConfiguredDefaultReviewers;
35
48
  exports.normalizeReviewerInstances = normalizeReviewerInstances;
36
49
  exports.resolveReviewerSelection = resolveReviewerSelection;
37
- const NON_RUNTIME_REVIEWER_SLUGS = [
38
- 'gemini',
39
- 'coderabbit',
40
- 'ollama',
41
- 'lm_studio',
42
- 'llama_cpp',
43
- ];
44
- function deriveRuntimeReviewerSlugs() {
45
- // eslint-disable-next-line @typescript-eslint/no-require-imports
46
- const registry = require('./capability-registry.cjs');
47
- const runtimes = registry.runtimes || {};
48
- return Object.keys(runtimes).filter((id) => runtimes[id]?.runtime?.hostBehaviors?.reviewerCli === true);
50
+ /**
51
+ * Derive the reviewer-slug roster from a capability registry.
52
+ *
53
+ * Exported (rather than a require()-time-only side effect) so tests can
54
+ * exercise the derivation directly against a synthetic registry — the real
55
+ * roster below is a single call against the generated
56
+ * `capability-registry.cjs`, and `tests/review-lane-descriptor.test.cjs`'s
57
+ * `checkReviewerLaneParity` separately guards that the real roster still
58
+ * agrees with `src/review-lane-descriptor.cts` and the workflow.
59
+ *
60
+ * Reads `registry.capabilities` — every declared capability regardless of
61
+ * role — because a `role: "reviewer"` lane-only capability is never stored in
62
+ * `registry.runtimes` (that map is role:"runtime" only). Per capability: a
63
+ * declared `reviewer.slug` wins outright (D9) and the capability contributes
64
+ * ONLY that slug; only when there is no body does the legacy
65
+ * `hostBehaviors.reviewerCli` alias contribute the capability id instead. The
66
+ * result is collected into a Set (so a slug can never appear twice, even if
67
+ * two distinct capabilities somehow named the same one) and returned as a
68
+ * SORTED array, so the roster's order never depends on `Object.entries()`
69
+ * iteration / registry build order.
70
+ */
71
+ function deriveReviewerSlugs(registry) {
72
+ const capabilities = registry.capabilities || {};
73
+ const slugs = new Set();
74
+ for (const [capId, cap] of Object.entries(capabilities)) {
75
+ // Trim before the emptiness test: `length > 0` alone admits a whitespace-only
76
+ // slug (" ") verbatim into the roster, where it can never match a real lane
77
+ // but still occupies a roster entry. Unreachable through the checked-in
78
+ // registry today, but this function is EXPORTED for reuse and carries no
79
+ // other validation, so it should not depend on its caller's hygiene.
80
+ const rawSlug = cap?.reviewer?.slug;
81
+ const declaredSlug = typeof rawSlug === 'string' ? rawSlug.trim() : '';
82
+ if (declaredSlug.length > 0) {
83
+ slugs.add(declaredSlug);
84
+ continue; // the body wins — never ALSO add this capability's alias below.
85
+ }
86
+ if (cap?.runtime?.hostBehaviors?.reviewerCli === true) {
87
+ slugs.add(capId);
88
+ }
89
+ }
90
+ return [...slugs].sort();
49
91
  }
50
- exports.KNOWN_REVIEWER_SLUGS = [
51
- ...deriveRuntimeReviewerSlugs(),
52
- ...NON_RUNTIME_REVIEWER_SLUGS,
53
- ];
92
+ /**
93
+ * The roster, derived once at module load.
94
+ *
95
+ * GUARDED, because this runs at `require()` time: an uncaught throw here does not
96
+ * degrade reviewer selection, it breaks `require()` of this module for EVERY
97
+ * consumer. The sibling `capability-trust.cjs` already holds this line — its
98
+ * `discloseExecutableSurfaces` is documented "TOTAL: never throws, for any
99
+ * manifest shape" and wrapped accordingly — and this module handles the same
100
+ * class of registry-derived input, so it should not be the asymmetric one.
101
+ *
102
+ * A malformed registry yields an EMPTY roster rather than a crash. That is a
103
+ * visible degradation, not a silent one: under ADR-2782 D4 an explicitly
104
+ * requested reviewer that is unavailable is an ERROR, so `/gsd:review --claude`
105
+ * against an empty roster fails loudly instead of quietly reviewing with nobody.
106
+ *
107
+ * Unreachable through the checked-in registry today (it is generated, JSON-sourced
108
+ * and code-reviewed, so it cannot carry a getter or a Proxy). Guarded anyway —
109
+ * reachability analysis is not a contract, and the next caller should not have to
110
+ * redo it.
111
+ */
112
+ exports.KNOWN_REVIEWER_SLUGS = (() => {
113
+ try {
114
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
115
+ return deriveReviewerSlugs(require('./capability-registry.cjs'));
116
+ }
117
+ catch {
118
+ return [];
119
+ }
120
+ })();
54
121
  /** Instance names are lowercase slugs that must not shadow a built-in slug. */
55
122
  exports.INSTANCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
56
123
  function normalizeConfiguredDefaultReviewers(rawValue) {
@@ -164,12 +231,27 @@ function resolveReviewerSelection(input) {
164
231
  let selected = [];
165
232
  if (explicitFlags.size > 0) {
166
233
  source = 'explicit_flags';
234
+ // ADR-2782 D4: absent-safe governs DISCOVERY, never explicit selection.
235
+ // Not finding a lane nobody asked for is normal; failing to run a lane
236
+ // somebody asked for is an error. Every miss used to be an `info`, so a
237
+ // PARTIAL miss (`--gemini --qwen` with qwen absent) ran the review with a
238
+ // thinner reviewer set while present_results reported success — "a cross-AI
239
+ // review that silently drops a lane is blind in one eye" (review.md).
240
+ // A total miss already errored, but only as a side effect of the selected
241
+ // set being empty; the partial case had no signal at all.
242
+ const priorErrorCount = errors.length;
167
243
  selected = [...explicitFlags].filter((slug) => detected.has(slug));
168
- const missing = [...explicitFlags].filter((slug) => !detected.has(slug));
169
- if (missing.length > 0) {
170
- infos.push(`explicit reviewers missing on host: ${missing.join(', ')}`);
244
+ // Sorted so the error order does not depend on flag order on the command line.
245
+ const missing = [...explicitFlags].filter((slug) => !detected.has(slug)).sort();
246
+ for (const slug of missing) {
247
+ errors.push(`explicit reviewer '${slug}' is not available on this host`);
171
248
  }
172
- if (selected.length === 0 && errors.length === 0) {
249
+ // Guarded on the error count as it stood BEFORE this branch, so the
250
+ // aggregate message fires under exactly the conditions it did previously.
251
+ // Guarding on `errors.length` would let the per-slug errors just pushed
252
+ // suppress it, silently dropping a message this change did not intend to
253
+ // remove.
254
+ if (selected.length === 0 && priorErrorCount === 0) {
173
255
  errors.push('no selected reviewers are available for explicit flags');
174
256
  }
175
257
  }
@@ -31,6 +31,9 @@ roadmapPhaseLookupSources, } = phaseIdModule;
31
31
  const planningWorkspace = require("./planning-workspace.cjs");
32
32
  const { planningDir } = planningWorkspace;
33
33
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
35
+ const unusableInputMod = require("./unusable-input.cjs");
36
+ const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
34
37
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
35
38
  // ─── Roadmap milestone scoping ───────────────────────────────────────────────
36
39
  /**
@@ -271,10 +274,18 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
271
274
  const normalizedPhase = stripProjectCodePrefix(phaseNum);
272
275
  if (/^999(?:\.|$)/.test(normalizedPhase))
273
276
  return null;
274
- const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
275
- if (!node_fs_1.default.existsSync(roadmapPath))
276
- return null;
277
+ // Resolved INSIDE the try for the same reason as getMilestoneInfo below: planningDir
278
+ // throws a plain Error for an invalid GSD_WORKSTREAM/GSD_PROJECT segment, and resolving
279
+ // it outside let that escape uncaught, crashing every caller for a malformed workstream
280
+ // name. ADR-227 is explicit that throwing breaks pipeline continuity, and this read path
281
+ // has no reason to be the exception -- it already degrades to null for every other
282
+ // failure. Absence still returns null before any diagnostic, and when the path never
283
+ // resolved there is nothing to name.
284
+ let roadmapPath;
277
285
  try {
286
+ roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
287
+ if (!node_fs_1.default.existsSync(roadmapPath))
288
+ return null;
278
289
  const roadmapRaw = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
279
290
  if (roadmapRaw === null)
280
291
  throw new Error('missing');
@@ -301,10 +312,33 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
301
312
  }
302
313
  return null;
303
314
  }
304
- catch {
315
+ catch (err) {
316
+ // Absence already returned above via existsSync; anything caught here is a read fault
317
+ // or the synthetic missing-marker. The null is preserved exactly either way.
318
+ if (roadmapPath !== undefined)
319
+ reportUnreadableRoadmap(err, roadmapPath);
305
320
  return null;
306
321
  }
307
322
  }
323
+ /**
324
+ * Report a ROADMAP.md that exists but could not be read (#1881, ADR-1411).
325
+ *
326
+ * The discriminator is the errno, and it matters in the SILENT direction.
327
+ * platformReadSync returns null for ENOENT and both callers convert that null into a
328
+ * synthetic Error carrying no code, which lands in the same catch as a real EACCES.
329
+ * Reporting unconditionally here would flag every project that has no ROADMAP.md yet --
330
+ * every brand-new project -- as corrupt. A genuine read fault always carries an errno;
331
+ * absence never does.
332
+ *
333
+ * The parse itself is regex over text and cannot throw, so anything reaching a catch is
334
+ * either a read fault or that synthetic absence marker. Nothing else gets here.
335
+ */
336
+ function reportUnreadableRoadmap(err, roadmapPath) {
337
+ const code = err?.code;
338
+ if (typeof code !== 'string')
339
+ return;
340
+ warnUnusableInput({ reason: UNUSABLE_REASON.ROADMAP_UNREADABLE, source: roadmapPath });
341
+ }
308
342
  /**
309
343
  * Strip a leading delimiter run (whitespace, em/en-dash, colon, hyphen) from a
310
344
  * milestone-name capture. Markdown headings commonly take the shape
@@ -318,8 +352,16 @@ function stripLeadingDelimiter(s) {
318
352
  return s.replace(/^[\s—–:-]+/, '').trim();
319
353
  }
320
354
  function getMilestoneInfo(cwd) {
355
+ // Declared here but RESOLVED INSIDE the try, so the catch can name the file without
356
+ // moving planningDir() out of the protected region. planningDir throws a plain Error
357
+ // for an invalid GSD_WORKSTREAM/GSD_PROJECT segment, and hoisting the call let that
358
+ // escape uncaught — breaking the invariant #2245 relies on, that this function never
359
+ // throws. When the path never resolved there is nothing to name, so the diagnostic is
360
+ // skipped and the default is returned exactly as before.
361
+ let roadmapPath;
321
362
  try {
322
- const roadmap = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'));
363
+ roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
364
+ const roadmap = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
323
365
  if (roadmap === null)
324
366
  throw new Error('missing');
325
367
  let stateVersion = null;
@@ -388,7 +430,13 @@ function getMilestoneInfo(cwd) {
388
430
  name: 'milestone',
389
431
  };
390
432
  }
391
- catch {
433
+ catch (err) {
434
+ // This function has no existsSync guard, so an absent ROADMAP arrives here too, as a
435
+ // synthetic Error with no errno. Only a real read fault is reported; the populated
436
+ // default is returned unchanged either way, and a plausible-looking default needs the
437
+ // diagnostic more than an empty sentinel does, not less (ADR-1411).
438
+ if (roadmapPath !== undefined)
439
+ reportUnreadableRoadmap(err, roadmapPath);
392
440
  return { version: 'v1.0', name: 'milestone' };
393
441
  }
394
442
  }
@@ -511,12 +559,15 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
511
559
  return id.split('-').map(seg => seg.replace(/^0+(?=\d)/, '') || '0').join('-');
512
560
  }
513
561
  const roadmapUsesHyphenedIds = [...normalized].some(n => n.includes('-'));
514
- // #2043: milestone-prefixed sub-phase components must be zero-padded (≥2 digits)
515
- // — "-\d{2,}" instead of "-0*\d+" — so a single-digit slug word after the phase
562
+ // #2043: milestone-prefixed sub-phase components must be zero-padded — so a
563
+ // single-digit slug word after the phase
516
564
  // number (e.g. dir "46-6-rs-…") captures "46" and is not silently excluded from
517
- // the milestone as a bogus "46-6" id.
565
+ // the milestone as a bogus "46-6" id. #2232: the continuation width is exactly 2
566
+ // (PHASE_CONTINUATION_SEGMENT_SOURCE), so a year-leading slug word (dir
567
+ // "14-2026-photos-…") captures "14" and is not excluded as a bogus "14-2026" id.
568
+ // Built via new RegExp (no /i — the [A-Za-z] letter class does real case handling).
518
569
  const numericRe = roadmapUsesHyphenedIds
519
- ? /^0*(\d+(?:-\d{2,})*[A-Za-z]?(?:\.\d+)*)/
570
+ ? new RegExp(`^0*(\\d+(?:-${phaseIdModule.PHASE_CONTINUATION_SEGMENT_SOURCE})*[A-Za-z]?(?:\\.\\d+)*)`)
520
571
  // phase-id-owner: the [A-Za-z] letter class does real case handling here — this regex carries NO /i flag; kept literal, not source-byte-equal to the canonical PHASE_NUMBER_TOKEN_SOURCE.
521
572
  : /^0*(\d+[A-Za-z]?(?:\.\d+)*)/;
522
573
  function isDirInMilestone(dirName) {
@@ -137,10 +137,16 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
137
137
  // comparison; unrecognized values are preserved verbatim for forward-compat.
138
138
  const modeMatch = section.match(/\*\*Mode(?::\*\*|\*\*:)\s*([^\n]+)/i);
139
139
  const mode = modeMatch ? modeMatch[1].trim().toLowerCase() : null;
140
- // Extract success criteria as structured array
141
- const criteriaMatch = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i);
140
+ // Extract success criteria as structured array. A criterion may wrap onto extra
141
+ // indented lines (no `N.` prefix); those continuations must fold INTO their
142
+ // criterion, not end the run (#2522 — the old `(?:\s*\d+\.\s*[^\n]+)+` broke on a
143
+ // wrapped line, truncating it and silently dropping every criterion below it).
144
+ // `\n*` before each numbered line keeps blank-line-separated criteria working.
145
+ const criteriaMatch = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\n*[ \t]*\d+\.[^\n]*\n?(?:[ \t]+(?!\d+\.)[^\n]*\n?)*)+)/i);
142
146
  const success_criteria = criteriaMatch
143
- ? criteriaMatch[1].trim().split('\n').map(line => line.replace(/^\s*\d+\.\s*/, '').trim()).filter(Boolean)
147
+ ? criteriaMatch[1].trim().split(/\n+(?=[ \t]*\d+\.)/)
148
+ .map(entry => entry.replace(/^\s*\d+\.\s*/, '').replace(/\s*\n\s*/g, ' ').trim())
149
+ .filter(Boolean)
144
150
  : [];
145
151
  return {
146
152
  found: true,
@@ -170,9 +176,19 @@ function getRoadmapPhaseWithFallback(cwd, phaseNum) {
170
176
  if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum)))
171
177
  return null;
172
178
  const roadmapPath = planningPaths(cwd).roadmap;
173
- if (!node_fs_1.default.existsSync(roadmapPath))
174
- return null;
175
- const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
179
+ // Read directly rather than gating on fs.existsSync: existsSync returns false
180
+ // on EACCES/EIO too, which would mask an UNREADABLE roadmap as "missing" and
181
+ // let a blocking gate certify empty scope (#2365 review). Honor the documented
182
+ // contract — null only when genuinely absent (ENOENT), otherwise throw.
183
+ let rawContent;
184
+ try {
185
+ rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
186
+ }
187
+ catch (err) {
188
+ if (err?.code === 'ENOENT')
189
+ return null;
190
+ throw err;
191
+ }
176
192
  const milestoneContent = extractCurrentMilestone(rawContent, cwd);
177
193
  const fullContent = stripShippedMilestones(rawContent);
178
194
  // #2121/#2114: iterate the shared lookup-source list (exact → numeric →
@@ -648,7 +664,7 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
648
664
  const planPath = node_path_1.default.join(node_path_1.default.resolve(cwd, phaseInfo.directory), planFile);
649
665
  try {
650
666
  const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
651
- const fm = extractFrontmatter(content);
667
+ const fm = extractFrontmatter(content, planPath);
652
668
  const wave = parseInt(fm.wave, 10) || 1;
653
669
  const planId = planFile.replace(/-PLAN\.md$/i, '').replace(/PLAN\.md$/i, '');
654
670
  const truths = parseMustHavesBlock(content, 'truths') || [];
@@ -518,6 +518,28 @@ function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cm
518
518
  const normalizedBody = convertGsdCommandReferencesToKimiSkillInvocations(frontmatter ? body : content, names);
519
519
  return `---\nname: ${kimiSkillName}\ndescription: ${yamlQuote(toSingleLine(description))}\n---\nInvoke this Kimi skill with \`/skill:${kimiSkillName}\`.\n\n${normalizedBody}`;
520
520
  }
521
+ /**
522
+ * Convert a Claude command-markdown source into a Kimi Code Agent Skill.
523
+ *
524
+ * Kimi Code (Moonshot's Node CLI) uses the standard Agent Skills format —
525
+ * a directory containing SKILL.md with frontmatter (name/description) and
526
+ * body — auto-discovered from `~/.kimi-code/skills/` (per Kimi Code docs:
527
+ * "Agent Skills is an open format for adding specialized knowledge and
528
+ * workflows to AI agents"). The invocation prefix is `/skill:<name>`,
529
+ * identical to Python kimi-cli.
530
+ *
531
+ * Today the output is byte-identical to `convertClaudeCommandToKimiSkill`
532
+ * (the Python kimi-cli converter) because both products consume the same
533
+ * Agent Skills format + `/skill:` invocation. The distinct function name
534
+ * lets a future divergence land cleanly if Kimi Code's skill format evolves
535
+ * independently of Python kimi-cli.
536
+ *
537
+ * Registered as `convertClaudeCommandToKimiCodeSkill` in the capabilities/
538
+ * kimi-code/capability.json `artifactLayout` `converter` field (#2454 PR 2).
539
+ */
540
+ function convertClaudeCommandToKimiCodeSkill(content, skillName, _runtime = null, cmdNames = null) {
541
+ return convertClaudeCommandToKimiSkill(content, skillName, _runtime, cmdNames);
542
+ }
521
543
  const KIMI_CANONICAL_GSD_AGENT_RE = /^gsd-[a-z0-9-]+$/;
522
544
  function parseKimiAgentSource(source) {
523
545
  if (typeof source === 'string') {
@@ -868,7 +890,13 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
868
890
  description = toSingleLine(description);
869
891
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
870
892
  const adapter = getCursorSkillAdapterHeader(skillName);
871
- return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
893
+ // #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/'
894
+ // menu (it defaults to true). Cursor also writes a commands/ surface (#785),
895
+ // and surfacing both duplicated every /gsd-* entry. This mirrors the #789
896
+ // CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills
897
+ // stay model-invocable background knowledge. (user-invocable:false hides from
898
+ // '/' while keeping model invocation — distinct from disable-model-invocation.)
899
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`;
872
900
  }
873
901
  /**
874
902
  * Convert a Claude Code command to a Cursor 1.6 slash command (#785).
@@ -1385,10 +1413,14 @@ Typed mapping (agent_type-capable schema only):
1385
1413
  inherited, or unsupported values; do not invent one-off effort literals in
1386
1414
  workflow prose.
1387
1415
  - \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
1388
- - \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct Codex mapping.
1389
- Codex \`spawn_agent\` does not create or bind a git worktree automatically.
1390
- Workflows that require this isolation must fail closed or use an explicit
1391
- manual worktree protocol before spawning (#3360).
1416
+ - \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
1417
+ but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
1418
+ \`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
1419
+ creates the worktree and process-spawns the executor into it with
1420
+ \`codex exec --cd <dir>\`, performing every git operation on the executor's behalf
1421
+ (its \`workspace-write\` sandbox makes \`.git\` read-only). Workflows must therefore
1422
+ never fabricate a manual worktree protocol — route through the negotiated
1423
+ isolation adapter, which still fails closed for hosts declaring \`none\` (#3360).
1392
1424
 
1393
1425
  Generic-agent workaround (multi_agent_v1 schema — NO agent_type field):
1394
1426
  When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch
@@ -2597,6 +2629,7 @@ module.exports = {
2597
2629
  convertClaudeCommandToAntigravitySkill,
2598
2630
  convertClaudeCommandToClaudeSkill,
2599
2631
  convertClaudeCommandToKimiSkill,
2632
+ convertClaudeCommandToKimiCodeSkill,
2600
2633
  buildKimiAgentArtifacts,
2601
2634
  convertClaudeToCursorMarkdown,
2602
2635
  convertClaudeCommandToCursorSkill,
@@ -227,8 +227,12 @@ function kimiAgentsKind(destSubpath, prefix, configDir) {
227
227
  * arg so scope-aware converters (antigravity, copilot) can choose
228
228
  * between global home paths and workspace-relative paths without
229
229
  * colliding with the `runtime` string at position 3.
230
+ * @param capabilityRegistry #2322: optional capability registry — captured in the
231
+ * stage() closure so third-party capability skills are bound to
232
+ * their declaring capId at staging time. Absent -> stage() stages
233
+ * nothing third-party (fail closed).
230
234
  */
231
- function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nested = false, scope = 'global') {
235
+ function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nested = false, scope = 'global', capabilityRegistry) {
232
236
  return {
233
237
  kind: 'skills',
234
238
  destSubpath,
@@ -247,7 +251,7 @@ function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nest
247
251
  : [];
248
252
  const isGlobal = scope === 'global';
249
253
  const wrappedConverter = (content, skillName) => realConverter(content, skillName, runtime, cmdNames, isGlobal);
250
- return stageSkillsForRuntimeAsSkills(findInstallSourceRoot(configDir), resolved, wrappedConverter, prefix, nested);
254
+ return stageSkillsForRuntimeAsSkills(findInstallSourceRoot(configDir), resolved, wrappedConverter, prefix, nested, capabilityRegistry);
251
255
  },
252
256
  };
253
257
  }
@@ -285,7 +289,7 @@ function getRegistry() {
285
289
  * Map a single ArtifactKindDescriptor entry to an ArtifactKind using the
286
290
  * matching builder function. Mirrors the hand-built calls in the old switch.
287
291
  */
288
- function dispatchKindEntry(entry, runtime, configDir, scope) {
292
+ function dispatchKindEntry(entry, runtime, configDir, scope, capabilityRegistry) {
289
293
  const { kind, destSubpath, prefix, nesting, converter } = entry;
290
294
  const nested = nesting === 'nested';
291
295
  let result;
@@ -304,7 +308,7 @@ function dispatchKindEntry(entry, runtime, configDir, scope) {
304
308
  if (converter == null) {
305
309
  throw new TypeError(`resolveRuntimeArtifactLayout: skills entry for '${runtime}' has converter=null (converter is required for skills)`);
306
310
  }
307
- result = skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope);
311
+ result = skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope, capabilityRegistry);
308
312
  break;
309
313
  case 'kimi-agents':
310
314
  result = kimiAgentsKind(destSubpath, prefix, configDir);
@@ -312,7 +316,7 @@ function dispatchKindEntry(entry, runtime, configDir, scope) {
312
316
  default:
313
317
  throw new TypeError(`resolveRuntimeArtifactLayout: unknown kind '${kind}' in descriptor for runtime '${runtime}'`);
314
318
  }
315
- if (typeof entry.home === 'string' && entry.home !== '') {
319
+ if (scope === 'global' && typeof entry.home === 'string' && entry.home !== '') {
316
320
  result.home = node_path_1.default.join(node_os_1.default.homedir(), entry.home);
317
321
  }
318
322
  return result;
@@ -322,11 +326,21 @@ function dispatchKindEntry(entry, runtime, configDir, scope) {
322
326
  *
323
327
  * ADR-857 phase 5d: driven by the capability-registry artifactLayout descriptor
324
328
  * instead of a hardcoded switch statement.
329
+ *
330
+ * @param capabilityRegistry #2322: optional — when the caller has a composed
331
+ * capability registry in scope (e.g. capability-writer.cts's `capability set`
332
+ * path, or a fresh install's registry-aware profile resolution), pass it here
333
+ * so the skills kind's stage() closure can materialize installed third-party
334
+ * capability skills bound to their declaring capId. Both call paths (surface
335
+ * apply AND the installer) must pass their registry here — resolveProfile's
336
+ * own `'*'` (full profile) short-circuit never carries a registry, so if it
337
+ * is not threaded in at layout-build time a `full`-profile install stages no
338
+ * third-party capability skills regardless of registration (#2322 blocker 2).
325
339
  */
326
- function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
327
- return resolveRuntimeArtifactLayoutFromRegistry(getRegistry(), runtime, configDir, scope);
340
+ function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global', capabilityRegistry) {
341
+ return resolveRuntimeArtifactLayoutFromRegistry(getRegistry(), runtime, configDir, scope, capabilityRegistry);
328
342
  }
329
- function resolveRuntimeArtifactLayoutFromRegistry(registry, runtime, configDir, scope = 'global') {
343
+ function resolveRuntimeArtifactLayoutFromRegistry(registry, runtime, configDir, scope = 'global', capabilityRegistry) {
330
344
  if (typeof configDir !== 'string' || configDir === '') {
331
345
  throw new TypeError('configDir must be a non-empty string');
332
346
  }
@@ -338,7 +352,7 @@ function resolveRuntimeArtifactLayoutFromRegistry(registry, runtime, configDir,
338
352
  throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
339
353
  }
340
354
  const entries = desc[scope] ?? [];
341
- const kinds = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir, scope));
355
+ const kinds = entries.map((entry) => dispatchKindEntry(entry, runtime, configDir, scope, capabilityRegistry));
342
356
  return { runtime, configDir, scope, kinds };
343
357
  }
344
358
  module.exports = { resolveRuntimeArtifactLayout, resolveRuntimeArtifactLayoutFromRegistry, findInstallSourceRoot };