@opengsd/gsd-core 1.4.3 → 1.5.0-rc.1

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 (149) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -3
  3. package/agents/gsd-executor.md +1 -1
  4. package/agents/gsd-plan-checker.md +11 -2
  5. package/agents/gsd-planner.md +11 -7
  6. package/agents/gsd-verifier.md +3 -3
  7. package/bin/install.js +825 -149
  8. package/commands/gsd/autonomous.md +5 -1
  9. package/commands/gsd/ns-manage.md +8 -1
  10. package/commands/gsd/ns-project.md +5 -0
  11. package/commands/gsd/ns-review.md +4 -1
  12. package/commands/gsd/ns-workflow.md +7 -1
  13. package/commands/gsd/plan-review-convergence.md +5 -4
  14. package/commands/gsd/surface.md +12 -5
  15. package/gemini-extension.json +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +198 -101
  17. package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
  18. package/gsd-core/bin/lib/capability-registry.cjs +417 -0
  19. package/gsd-core/bin/lib/capability-state.cjs +336 -0
  20. package/gsd-core/bin/lib/cli-exit.cjs +22 -3
  21. package/gsd-core/bin/lib/config-loader.cjs +716 -0
  22. package/gsd-core/bin/lib/configuration.cjs +4 -34
  23. package/gsd-core/bin/lib/core-utils.cjs +198 -0
  24. package/gsd-core/bin/lib/core.cjs +57 -1817
  25. package/gsd-core/bin/lib/federated-config.cjs +182 -0
  26. package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
  27. package/gsd-core/bin/lib/init.cjs +58 -12
  28. package/gsd-core/bin/lib/install-profiles.cjs +157 -3
  29. package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
  30. package/gsd-core/bin/lib/io.cjs +167 -0
  31. package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
  32. package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
  33. package/gsd-core/bin/lib/model-resolver.cjs +426 -0
  34. package/gsd-core/bin/lib/phase-id.cjs +215 -0
  35. package/gsd-core/bin/lib/phase-locator.cjs +148 -0
  36. package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
  37. package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
  38. package/gsd-core/bin/lib/roadmap.cjs +4 -1
  39. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +96 -9
  40. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +2 -1
  41. package/gsd-core/bin/lib/runtime-homes.cjs +40 -0
  42. package/gsd-core/bin/lib/runtime-name-policy.cjs +1 -0
  43. package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
  44. package/gsd-core/bin/lib/state.cjs +114 -2
  45. package/gsd-core/bin/lib/surface.cjs +66 -14
  46. package/gsd-core/bin/lib/verify.cjs +1 -1
  47. package/gsd-core/bin/shared/model-catalog.json +5 -0
  48. package/gsd-core/bin/shared/runtime-aliases.manifest.json +3 -0
  49. package/gsd-core/references/planner-reviews.md +5 -2
  50. package/gsd-core/templates/phase-prompt.md +7 -7
  51. package/gsd-core/templates/project.md +19 -2
  52. package/gsd-core/templates/summary-complex.md +1 -0
  53. package/gsd-core/templates/summary-minimal.md +1 -0
  54. package/gsd-core/templates/summary-standard.md +1 -0
  55. package/gsd-core/templates/summary.md +1 -0
  56. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  57. package/gsd-core/workflows/add-backlog.md +1 -1
  58. package/gsd-core/workflows/add-phase.md +1 -1
  59. package/gsd-core/workflows/add-tests.md +1 -1
  60. package/gsd-core/workflows/add-todo.md +1 -1
  61. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  62. package/gsd-core/workflows/audit-fix.md +1 -1
  63. package/gsd-core/workflows/audit-milestone.md +1 -1
  64. package/gsd-core/workflows/audit-uat.md +1 -1
  65. package/gsd-core/workflows/autonomous.md +86 -15
  66. package/gsd-core/workflows/check-todos.md +1 -1
  67. package/gsd-core/workflows/cleanup.md +1 -1
  68. package/gsd-core/workflows/code-review-fix.md +6 -4
  69. package/gsd-core/workflows/code-review.md +3 -2
  70. package/gsd-core/workflows/complete-milestone.md +11 -5
  71. package/gsd-core/workflows/debug.md +1 -1
  72. package/gsd-core/workflows/diagnose-issues.md +1 -1
  73. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  74. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  75. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  76. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  77. package/gsd-core/workflows/discuss-phase.md +8 -1
  78. package/gsd-core/workflows/do.md +1 -1
  79. package/gsd-core/workflows/docs-update.md +1 -1
  80. package/gsd-core/workflows/edit-phase.md +1 -1
  81. package/gsd-core/workflows/eval-review.md +4 -1
  82. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  83. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  84. package/gsd-core/workflows/execute-phase.md +8 -1
  85. package/gsd-core/workflows/execute-plan.md +1 -1
  86. package/gsd-core/workflows/explore.md +1 -1
  87. package/gsd-core/workflows/extract-learnings.md +1 -1
  88. package/gsd-core/workflows/forensics.md +1 -1
  89. package/gsd-core/workflows/graduation.md +1 -1
  90. package/gsd-core/workflows/health.md +1 -1
  91. package/gsd-core/workflows/help/modes/full.md +1 -1
  92. package/gsd-core/workflows/import.md +1 -1
  93. package/gsd-core/workflows/ingest-docs.md +1 -1
  94. package/gsd-core/workflows/insert-phase.md +1 -1
  95. package/gsd-core/workflows/list-workspaces.md +1 -1
  96. package/gsd-core/workflows/manager.md +1 -1
  97. package/gsd-core/workflows/map-codebase.md +1 -1
  98. package/gsd-core/workflows/milestone-summary.md +1 -1
  99. package/gsd-core/workflows/mvp-phase.md +1 -1
  100. package/gsd-core/workflows/new-milestone.md +1 -1
  101. package/gsd-core/workflows/new-project.md +1 -1
  102. package/gsd-core/workflows/new-workspace.md +1 -1
  103. package/gsd-core/workflows/next.md +1 -1
  104. package/gsd-core/workflows/pause-work.md +1 -1
  105. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  106. package/gsd-core/workflows/plan-phase.md +27 -2
  107. package/gsd-core/workflows/plan-review-convergence.md +60 -33
  108. package/gsd-core/workflows/plant-seed.md +1 -1
  109. package/gsd-core/workflows/profile-user.md +1 -1
  110. package/gsd-core/workflows/progress.md +1 -1
  111. package/gsd-core/workflows/quick.md +2 -2
  112. package/gsd-core/workflows/remove-phase.md +1 -1
  113. package/gsd-core/workflows/remove-workspace.md +1 -1
  114. package/gsd-core/workflows/resume-project.md +1 -1
  115. package/gsd-core/workflows/review.md +1 -1
  116. package/gsd-core/workflows/scan.md +1 -1
  117. package/gsd-core/workflows/secure-phase.md +1 -1
  118. package/gsd-core/workflows/settings-advanced.md +1 -1
  119. package/gsd-core/workflows/settings-integrations.md +1 -1
  120. package/gsd-core/workflows/settings.md +1 -1
  121. package/gsd-core/workflows/ship.md +8 -1
  122. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  123. package/gsd-core/workflows/sketch.md +1 -1
  124. package/gsd-core/workflows/spec-phase.md +1 -1
  125. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  126. package/gsd-core/workflows/spike.md +1 -1
  127. package/gsd-core/workflows/stats.md +1 -1
  128. package/gsd-core/workflows/thread.md +1 -1
  129. package/gsd-core/workflows/transition.md +1 -1
  130. package/gsd-core/workflows/ui-phase.md +1 -1
  131. package/gsd-core/workflows/ui-review.md +1 -1
  132. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  133. package/gsd-core/workflows/validate-phase.md +1 -1
  134. package/gsd-core/workflows/verify-phase.md +1 -1
  135. package/gsd-core/workflows/verify-work.md +8 -1
  136. package/package.json +7 -2
  137. package/scripts/base64-scan.sh +1 -1
  138. package/scripts/changeset/cli.cjs +8 -1
  139. package/scripts/changeset/lint.cjs +38 -2
  140. package/scripts/ci-test-scope.cjs +21 -10
  141. package/scripts/gen-capability-registry.cjs +1808 -0
  142. package/scripts/gen-loop-host-contract.cjs +471 -0
  143. package/scripts/lint-regression-test-names.allowlist.json +269 -0
  144. package/scripts/lint-regression-test-names.cjs +117 -0
  145. package/scripts/lint-test-file-count.allowlist.json +12 -4
  146. package/scripts/prompt-injection-scan.sh +4 -4
  147. package/scripts/run-tests.cjs +79 -16
  148. package/scripts/secret-scan.sh +3 -3
  149. package/scripts/run-cross-platform-tests.cjs +0 -67
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd:autonomous
3
3
  description: Run all remaining phases autonomously — discuss→plan→execute per phase
4
- argument-hint: "[--from N] [--to N] [--only N] [--interactive]"
4
+ argument-hint: "[--from N] [--to N] [--only N] [--interactive] [--converge]"
5
5
  effort: xhigh
6
6
  allowed-tools:
7
7
  - Read
@@ -37,6 +37,10 @@ Optional flags:
37
37
  - `--to N` — stop after phase N completes (halt instead of advancing to next phase).
38
38
  - `--only N` — execute only phase N (single-phase mode).
39
39
  - `--interactive` — run discuss inline with questions (not auto-answered), then dispatch plan→execute as background agents. Keeps the main context lean while preserving user input on decisions.
40
+ - `--converge` — run each phase's planning step through `gsd-plan-review-convergence` instead of plain `gsd-plan-phase`. Requires `workflow.plan_review_convergence=true`.
41
+ - `--cross-ai` — compatibility alias for `--converge`.
42
+
43
+ When `--converge` or `--cross-ai` is set, reviewer selector flags supported by `gsd-plan-review-convergence` may be passed through: `--codex`, `--gemini`, `--claude`, `--opencode`, `--ollama`, `--lm-studio`, `--llama-cpp`, `--all`, and `--max-cycles N`.
40
44
 
41
45
  Project context, phase list, and state are resolved inside the workflow using init commands (`gsd-tools query init.milestone-op`, `gsd-tools query roadmap.analyze`). No upfront context loading needed.
42
46
  </context>
@@ -5,7 +5,7 @@ argument-hint: ""
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Skill
8
- requires: [config, workspace, workstreams, thread, pause-work, resume-work, update, ship, inbox, pr-branch, undo]
8
+ requires: [config, workspace, workstreams, thread, pause-work, resume-work, update, ship, inbox, pr-branch, undo, cleanup, health, manager, settings, stats, surface, help]
9
9
  ---
10
10
 
11
11
  Route to the appropriate management skill based on the user's intent.
@@ -25,5 +25,12 @@ Route to the appropriate management skill based on the user's intent.
25
25
  | Process inbox items | gsd-inbox |
26
26
  | Create a clean PR branch | gsd-pr-branch |
27
27
  | Undo the last GSD action | gsd-undo |
28
+ | Archive accumulated phase directories | gsd-cleanup |
29
+ | Diagnose planning directory health | gsd-health |
30
+ | Open the interactive command center | gsd-manager |
31
+ | Configure workflow toggles and model profile | gsd-settings |
32
+ | Show project statistics | gsd-stats |
33
+ | Toggle which skills are surfaced | gsd-surface |
34
+ | Show the GSD command guide | gsd-help |
28
35
 
29
36
  Invoke the matched skill directly using the Skill tool.
@@ -5,6 +5,7 @@ argument-hint: ""
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Skill
8
+ requires: [new-project, new-milestone, complete-milestone, audit-milestone, milestone-summary, import, ingest-docs, profile-user, review-backlog]
8
9
  ---
9
10
 
10
11
  Route to the appropriate project / milestone skill based on the user's intent.
@@ -18,5 +19,9 @@ inline as part of `gsd-audit-milestone`'s output.
18
19
  | Complete the current milestone | gsd-complete-milestone |
19
20
  | Audit a milestone for issues | gsd-audit-milestone |
20
21
  | Summarize milestone status | gsd-milestone-summary |
22
+ | Import an external plan | gsd-import |
23
+ | Bootstrap planning from existing docs | gsd-ingest-docs |
24
+ | Generate a developer profile | gsd-profile-user |
25
+ | Review and promote backlog items | gsd-review-backlog |
21
26
 
22
27
  Invoke the matched skill directly using the Skill tool.
@@ -5,7 +5,7 @@ argument-hint: ""
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Skill
8
- requires: [code-review, audit-uat, secure-phase, eval-review, ui-review, validate-phase, debug, forensics]
8
+ requires: [code-review, audit-uat, secure-phase, eval-review, ui-review, validate-phase, debug, forensics, audit-fix, review, ui-phase]
9
9
  ---
10
10
 
11
11
  Route to the appropriate quality / review skill based on the user's intent.
@@ -22,5 +22,8 @@ Route to the appropriate quality / review skill based on the user's intent.
22
22
  | Validate phase outputs | gsd-validate-phase |
23
23
  | Debug a failing feature or error | gsd-debug |
24
24
  | Forensic investigation of a broken system | gsd-forensics |
25
+ | Autonomous audit-to-fix pipeline | gsd-audit-fix |
26
+ | Cross-AI peer review of plans | gsd-review |
27
+ | Generate a UI design contract | gsd-ui-phase |
25
28
 
26
29
  Invoke the matched skill directly using the Skill tool.
@@ -5,7 +5,7 @@ argument-hint: ""
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Skill
8
- requires: [discuss-phase, spec-phase, plan-phase, execute-phase, verify-work, phase, progress, ultraplan-phase, plan-review-convergence]
8
+ requires: [discuss-phase, spec-phase, plan-phase, execute-phase, verify-work, phase, progress, ultraplan-phase, plan-review-convergence, add-tests, ai-integration-phase, autonomous, fast, mvp-phase, quick]
9
9
  ---
10
10
 
11
11
  Route to the appropriate phase-pipeline skill based on the user's intent.
@@ -24,5 +24,11 @@ absorbs the former next/do commands.
24
24
  | Advance to the next logical step | gsd-progress |
25
25
  | Offload planning to the ultraplan cloud | gsd-ultraplan-phase |
26
26
  | Cross-AI plan review convergence loop | gsd-plan-review-convergence |
27
+ | Generate tests for a completed phase | gsd-add-tests |
28
+ | Design an AI-integration phase | gsd-ai-integration-phase |
29
+ | Run all remaining phases autonomously | gsd-autonomous |
30
+ | Execute a trivial task inline | gsd-fast |
31
+ | Plan a phase as a vertical MVP slice | gsd-mvp-phase |
32
+ | Execute a quick task with GSD guarantees | gsd-quick |
27
33
 
28
34
  Invoke the matched skill directly using the Skill tool.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gsd:plan-review-convergence
3
- description: "Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain."
3
+ description: "Cross-AI plan convergence - replan until review concerns are resolved."
4
4
  argument-hint: "<phase> [--codex] [--gemini] [--claude] [--opencode] [--ollama] [--lm-studio] [--llama-cpp] [--text] [--ws <name>] [--all] [--max-cycles N]"
5
5
  allowed-tools:
6
6
  - Read
@@ -9,19 +9,20 @@ allowed-tools:
9
9
  - Glob
10
10
  - Grep
11
11
  - Agent
12
+ - Skill
12
13
  - AskUserQuestion
13
14
  requires: [phase, review]
14
15
  ---
15
16
 
16
17
  <objective>
17
18
  Cross-AI plan convergence loop — an outer revision gate around gsd-review and gsd-planner.
18
- Repeatedly: review plans with external AI CLIs → if HIGH concerns found → replan with --reviews feedback → re-review. Stops when no HIGH concerns remain or max cycles reached.
19
+ Repeatedly: review plans with external AI CLIs → if HIGH or actionable non-HIGH concerns remain → replan with --reviews feedback → re-review. Stops when no unresolved HIGH concerns or actionable MEDIUM/LOW findings remain outside PLAN.md, or when max cycles is reached.
19
20
 
20
- **Flow:** Skill("gsd-plan-phase") → Agent→Skill("gsd-review") → check HIGHs → Skill("gsd-plan-phase --reviews") → Agent→Skill("gsd-review") → ... → Converge or escalate
21
+ **Flow:** Skill("gsd-plan-phase") → Agent→Skill("gsd-review") → check unresolved HIGH + actionable non-HIGH → Skill("gsd-plan-phase --reviews") → Agent→Skill("gsd-review") → ... → Converge or escalate
21
22
 
22
23
  Replaces gsd-plan-phase's internal gsd-plan-checker with external AI reviewers (codex, gemini, etc.). Plan-phase runs **inline** (bare Skill at depth 0) so it can spawn gsd-planner/gsd-plan-checker at depth 1. Review runs inside an isolated Agent (gsd-review is a Bash leaf — no sub-agents needed). Orchestrator only does loop control.
23
24
 
24
- **Orchestrator role:** Parse arguments, validate phase, run plan-phase inline (Skill at depth 0), spawn an Agent for gsd-review, check HIGHs, stall detection, escalation gate.
25
+ **Orchestrator role:** Parse arguments, validate phase, run plan-phase inline (Skill at depth 0), spawn an Agent for gsd-review, check unresolved HIGH and actionable non-HIGH counts, stall detection, escalation gate.
25
26
  </objective>
26
27
 
27
28
  <execution_context>
@@ -36,8 +36,12 @@ Parse the first token of $ARGUMENTS:
36
36
 
37
37
  ## list / status
38
38
 
39
- Call `listSurface(runtimeConfigDir, manifest, CLUSTERS)` from
40
- `gsd-core/bin/lib/surface.cjs`. Display:
39
+ Load the capability registry and call `listSurface(runtimeConfigDir, manifest, CLUSTERS, registry)` from
40
+ `gsd-core/bin/lib/surface.cjs`. The registry is loaded via:
41
+ ```js
42
+ const registry = require('gsd-core/bin/lib/capability-registry.cjs');
43
+ ```
44
+ Display:
41
45
 
42
46
  ```
43
47
  Enabled (N skills, ~T tokens):
@@ -67,8 +71,9 @@ Install profile: standard (from .gsd-profile)
67
71
  3. `writeSurface(runtimeConfigDir, surfaceState)`.
68
72
  4. Resolve and re-apply:
69
73
  ```js
74
+ const registry = require('gsd-core/bin/lib/capability-registry.cjs');
70
75
  const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
71
- applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
76
+ applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
72
77
  ```
73
78
  5. Confirm: "Surface updated to profile `<name>`. N skills enabled."
74
79
 
@@ -84,8 +89,9 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
84
89
  3. Add cluster to `surfaceState.disabledClusters` (deduplicate).
85
90
  4. `writeSurface` → resolve layout → `applySurface`:
86
91
  ```js
92
+ const registry = require('gsd-core/bin/lib/capability-registry.cjs');
87
93
  const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
88
- applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
94
+ applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
89
95
  ```
90
96
  5. Confirm: "Disabled cluster `<cluster>`. N skills removed from surface."
91
97
 
@@ -97,8 +103,9 @@ Valid cluster names: `core_loop`, `audit_review`, `milestone`, `research_ideate`
97
103
  2. Remove cluster from `surfaceState.disabledClusters`.
98
104
  3. `writeSurface` → resolve layout → `applySurface`:
99
105
  ```js
106
+ const registry = require('gsd-core/bin/lib/capability-registry.cjs');
100
107
  const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope);
101
- applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
108
+ applySurface(runtimeConfigDir, layout, manifest, CLUSTERS, registry);
102
109
  ```
103
110
  4. Confirm: "Enabled cluster `<cluster>`. N skills added back to surface."
104
111
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.4.3",
3
+ "version": "1.5.0-rc.1",
4
4
  "description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -163,6 +163,17 @@
163
163
  * learnings prune --older-than <dur> Remove entries older than duration (e.g. 90d)
164
164
  * learnings delete <id> Delete a learning by ID
165
165
  *
166
+ * Loop Extension Point Queries (ADR-857 phase 3c):
167
+ * loop render-hooks <point> Resolve + render active Capability hooks at a loop point
168
+ * Returns JSON envelope { point, activeHooks, rendered }
169
+ * Valid points: discuss:pre/post, plan:pre/post,
170
+ * execute:pre/wave:pre/wave:post/post, verify:pre/post, ship:pre/post
171
+ *
172
+ * Capability State (ADR-857 phase 4b):
173
+ * capability state [--config-dir <path>] Resolve per-capability install/surface/hook-activation state
174
+ * Returns JSON envelope { runtimeConfigDir, capabilities[] }
175
+ * --config-dir: runtime config dir (default: auto-detect current runtime)
176
+ *
166
177
  * GSD-2 Migration:
167
178
  * from-gsd2 [--path <dir>] [--force] [--dry-run]
168
179
  * Import a GSD-2 (.gsd/) project back to GSD v1 (.planning/) format
@@ -201,6 +212,8 @@ const { routeVerifyCommand } = require('./lib/verify-command-router.cjs');
201
212
  const { routeVerificationCommand } = require('./lib/verification-command-router.cjs');
202
213
  const verification = require('./lib/verification.cjs');
203
214
  const { routeInitCommand } = require('./lib/init-command-router.cjs');
215
+ const loopResolver = require('./lib/loop-resolver.cjs');
216
+ const capabilityState = require('./lib/capability-state.cjs');
204
217
  const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
205
218
  const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
206
219
  const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
@@ -243,6 +256,122 @@ function _dispatchNonFamily({ registryCommand, registryArgs, legacyCommand, lega
243
256
  return false;
244
257
  }
245
258
 
259
+ // ─── ADR-959: Capability Command Dispatch ─────────────────────────────────────
260
+
261
+ /**
262
+ * Dispatch a command via the capability registry's commandFamilies index.
263
+ *
264
+ * Consulted in the `default` case of `runCommand` BEFORE the unknown-command
265
+ * error is emitted. Returns:
266
+ * true — command was "consumed" (found in registry, or a dispatch error was
267
+ * emitted); "Unknown command" is suppressed in all consumed cases.
268
+ * false — command not found in the registry (including prototype-pollution
269
+ * guard hits and missing/empty commandFamilies); caller falls through
270
+ * to the existing unknown-command error path.
271
+ * Behavior-preserving when commandFamilies is empty ({}).
272
+ *
273
+ * Injectable for tests:
274
+ * - `registry` defaults to require('./lib/capability-registry.cjs')
275
+ * - `requireModule` defaults to a confinement-checked loader that resolves the
276
+ * module path relative to bin/lib/ and asserts it stays within that directory
277
+ * before requiring — defense-in-depth against corrupted/hand-edited registry entries.
278
+ *
279
+ * @param {object} opts
280
+ * @param {string} opts.command The command name (top-level gsd-tools command)
281
+ * @param {string[]} opts.args Remaining args passed to the router
282
+ * @param {string} opts.cwd Project working directory
283
+ * @param {boolean} opts.raw Raw output mode flag
284
+ * @param {Function} opts.error Error reporter (core.error)
285
+ * @param {object} [opts.registry] Injectable registry (for tests)
286
+ * @param {Function} [opts.requireModule] Injectable module loader (for tests)
287
+ * @returns {boolean} true if the command was dispatched, false otherwise
288
+ */
289
+ function dispatchCapabilityCommand({ command, args, cwd, raw, error, registry, requireModule }) {
290
+ // Prototype-pollution guard: reject reserved property names as command keys
291
+ if (command === '__proto__' || command === 'constructor' || command === 'prototype') {
292
+ return false;
293
+ }
294
+
295
+ // Resolve defaults (injectable for tests)
296
+ const reg = registry !== undefined ? registry : require('./lib/capability-registry.cjs');
297
+
298
+ // Default requireModule: confined to bin/lib/ — validate the module name is a
299
+ // safe bare .cjs basename (no path separators, no directory traversal), then
300
+ // resolve and assert confinement, then require the RESOLVED absolute path so
301
+ // the checked representation and the required representation are identical.
302
+ const libDir = path.join(__dirname, 'lib');
303
+ const defaultRequireModule = function (m) {
304
+ // Step 1: validate m is a bare .cjs basename — same conservative pattern the
305
+ // generator uses. Rejects any value with path separators (/, \, ..) or
306
+ // missing the .cjs extension before we even touch the filesystem.
307
+ if (typeof m !== 'string' || !/^[A-Za-z0-9._-]+\.cjs$/.test(m)) {
308
+ throw new Error('capability module must be a bare .cjs basename: ' + JSON.stringify(m));
309
+ }
310
+ // Step 2: confinement check — belt-and-suspenders even after the basename
311
+ // validation above. Resolved path must be inside libDir (not equal to it,
312
+ // and must start with libDir + sep so "libDir-suffix" can't sneak through).
313
+ const resolved = path.resolve(libDir, m);
314
+ if (resolved === libDir || !resolved.startsWith(libDir + path.sep)) {
315
+ throw new Error('capability module path escapes bin/lib/: ' + JSON.stringify(m));
316
+ }
317
+ // Step 3: require the resolved absolute path — the SAME representation that
318
+ // was checked above, not the concatenated './lib/' + m string.
319
+ return require(resolved);
320
+ };
321
+ const loadModule = requireModule !== undefined ? requireModule : defaultRequireModule;
322
+
323
+ // Look up the command family in the registry
324
+ const families = reg && reg.commandFamilies;
325
+ if (!families || typeof families !== 'object') return false;
326
+
327
+ const entry = families[command];
328
+ if (!entry || typeof entry !== 'object') return false;
329
+
330
+ // Resolve and call the router
331
+ let mod;
332
+ try {
333
+ mod = loadModule(entry.module);
334
+ } catch (_) {
335
+ // Module not found, load error, or confinement violation — surface a
336
+ // diagnostic and return true (consumed) so "Unknown command" is suppressed.
337
+ error('capability command "' + command + '" module "' + entry.module + '" failed to load');
338
+ return true; // consumed — don't emit "Unknown command"
339
+ }
340
+
341
+ // Own-property guard: prevent invoking inherited prototype methods
342
+ // (constructor, toString, hasOwnProperty, etc.) as a router when the registry
343
+ // entry names one of those. Must come before the typeof check.
344
+ if (!mod || !Object.prototype.hasOwnProperty.call(mod, entry.router)) {
345
+ error('capability command "' + command + '" router "' + entry.router + '" is not an own export of module "' + entry.module + '"');
346
+ return true; // consumed — don't emit "Unknown command"
347
+ }
348
+ const fn = mod[entry.router];
349
+ if (typeof fn !== 'function') {
350
+ // Router export not found — surface a diagnostic and return true (consumed)
351
+ // so "Unknown command" is suppressed.
352
+ error('capability command "' + command + '" router "' + entry.router + '" is not a function in module "' + entry.module + '"');
353
+ return true; // consumed — don't emit "Unknown command"
354
+ }
355
+
356
+ let _result;
357
+ try {
358
+ _result = fn({ args, cwd, raw, error });
359
+ } catch (e) {
360
+ if (e instanceof ExitError) throw e; // intentional structured error from the router (honors --json-errors) — propagate untouched
361
+ error(
362
+ 'capability command "' + command + '" router "' + entry.router + '" in module "' + entry.module + '" threw: ' + (e && e.message ? e.message : String(e)),
363
+ ERROR_REASON.SDK_FAIL_FAST,
364
+ );
365
+ }
366
+ if (_result && typeof _result.then === 'function') {
367
+ error(
368
+ 'capability command "' + command + '" router "' + entry.router + '" in module "' + entry.module + '" must be synchronous (returned a Promise); async capability routers are not supported.',
369
+ ERROR_REASON.SDK_FAIL_FAST,
370
+ );
371
+ }
372
+ return true;
373
+ }
374
+
246
375
  // ─── Arg parsing helpers ──────────────────────────────────────────────────────
247
376
 
248
377
  // ─── CLI Router ───────────────────────────────────────────────────────────────
@@ -379,7 +508,7 @@ async function main() {
379
508
  'current-timestamp, detect-custom-files, docs-init, effort, extract-messages, find-phase, ' +
380
509
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
381
510
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
382
- 'classify-confidence, learnings, list-todos, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
511
+ 'capability, classify-confidence, learnings, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
383
512
  'profile-sample, progress, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
384
513
  'task, template, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
385
514
  'Global flags:\n' +
@@ -420,6 +549,11 @@ async function main() {
420
549
  // Multi-repo guard: resolve project root for commands that read/write .planning/.
421
550
  // Skip for pure-utility commands that don't touch .planning/ to avoid unnecessary
422
551
  // filesystem traversal on every invocation.
552
+ // 'loop' and 'capability' are intentionally NOT in SKIP_ROOT_RESOLUTION.
553
+ // Both are registry/config queries that resolve activation via
554
+ // .planning/config.json; they need the project root (cwd) for correct
555
+ // `when` key resolution. If one is ever moved to SKIP_ROOT_RESOLUTION,
556
+ // move the other at the same time (keep them consistent).
423
557
  const SKIP_ROOT_RESOLUTION = new Set([
424
558
  'generate-slug', 'current-timestamp', 'verify-path-exists',
425
559
  'verify-summary', 'template', 'frontmatter', 'detect-custom-files',
@@ -1021,7 +1155,8 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1021
1155
  if (subcommand === 'complete') {
1022
1156
  const milestoneName = parseMultiwordArg(args, 'name');
1023
1157
  const archivePhases = args.includes('--archive-phases');
1024
- milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases }, raw);
1158
+ const force = args.includes('--force');
1159
+ milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force }, raw);
1025
1160
  } else {
1026
1161
  error('Unknown milestone subcommand. Available: complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1027
1162
  }
@@ -1046,27 +1181,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1046
1181
  break;
1047
1182
  }
1048
1183
 
1049
- case 'audit-uat': {
1050
- const uat = require('./lib/uat.cjs');
1051
- uat.cmdAuditUat(cwd, raw);
1052
- break;
1053
- }
1054
-
1055
- case 'audit-open': {
1056
- const { auditOpenArtifacts, formatAuditReport } = require('./lib/audit.cjs');
1057
- const wantJson = args.includes('--json');
1058
- const result = auditOpenArtifacts(cwd);
1059
- if (wantJson) {
1060
- // core.output JSON-stringifies its first arg; pass the object directly.
1061
- core.output(result, raw);
1062
- } else {
1063
- // Human-readable report must bypass JSON encoding — use the rawValue
1064
- // form (third arg) which core.output emits verbatim.
1065
- core.output(null, true, formatAuditReport(result));
1066
- }
1067
- break;
1068
- }
1069
-
1070
1184
  case 'uat': {
1071
1185
  const subcommand = args[1];
1072
1186
  const uat = require('./lib/uat.cjs');
@@ -1118,6 +1232,50 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1118
1232
  break;
1119
1233
  }
1120
1234
 
1235
+ case 'loop': {
1236
+ // loop render-hooks <point>
1237
+ const loopSubcommand = args[1];
1238
+ if (loopSubcommand === 'render-hooks') {
1239
+ loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {});
1240
+ } else {
1241
+ error(
1242
+ `Unknown loop subcommand: ${loopSubcommand}. Available: render-hooks`,
1243
+ core.ERROR_REASON ? core.ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
1244
+ );
1245
+ }
1246
+ break;
1247
+ }
1248
+
1249
+ case 'capability': {
1250
+ // capability state [--config-dir <path>]
1251
+ // Root resolution: 'capability' is NOT in SKIP_ROOT_RESOLUTION for the
1252
+ // same reason 'loop' is not: both are registry/config queries that need
1253
+ // the project root (cwd) for .planning/config.json activation resolution.
1254
+ // If 'loop' were ever added to SKIP_ROOT_RESOLUTION, 'capability' should
1255
+ // be added at the same time to keep them consistent.
1256
+ const capSubcommand = args[1];
1257
+ if (capSubcommand === 'state') {
1258
+ const configDirIdx = args.indexOf('--config-dir');
1259
+ let configDir = null;
1260
+ if (configDirIdx !== -1) {
1261
+ const configDirVal = args[configDirIdx + 1];
1262
+ // Validate that --config-dir has a following non-flag value.
1263
+ if (!configDirVal || configDirVal.startsWith('--')) {
1264
+ error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1265
+ }
1266
+ configDir = configDirVal;
1267
+ }
1268
+ const resolvedConfigDir = configDir ? path.resolve(configDir) : null;
1269
+ capabilityState.cmdCapabilityState(cwd, resolvedConfigDir, raw, {});
1270
+ } else {
1271
+ error(
1272
+ `Unknown capability subcommand: ${capSubcommand}. Available: state`,
1273
+ core.ERROR_REASON ? core.ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
1274
+ );
1275
+ }
1276
+ break;
1277
+ }
1278
+
1121
1279
  case 'phase-plan-index': {
1122
1280
  phase.cmdPhasePlanIndex(cwd, args[1], raw);
1123
1281
  break;
@@ -1279,83 +1437,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1279
1437
  break;
1280
1438
  }
1281
1439
 
1282
- // ─── Intel ────────────────────────────────────────────────────────────
1283
-
1284
- case 'intel': {
1285
- const intel = require('./lib/intel.cjs');
1286
- const subcommand = args[1];
1287
- if (subcommand === 'query') {
1288
- const term = args[2];
1289
- if (!term) error('Usage: gsd-tools intel query <term>', ERROR_REASON.USAGE);
1290
- const planningDir = path.join(cwd, '.planning');
1291
- core.output(intel.intelQuery(term, planningDir), raw);
1292
- } else if (subcommand === 'status') {
1293
- const planningDir = path.join(cwd, '.planning');
1294
- const status = intel.intelStatus(planningDir);
1295
- if (!raw && status.files) {
1296
- for (const file of Object.values(status.files)) {
1297
- if (file.updated_at) {
1298
- file.updated_at = core.timeAgo(new Date(file.updated_at));
1299
- }
1300
- }
1301
- }
1302
- core.output(status, raw);
1303
- } else if (subcommand === 'diff') {
1304
- const planningDir = path.join(cwd, '.planning');
1305
- core.output(intel.intelDiff(planningDir), raw);
1306
- } else if (subcommand === 'snapshot') {
1307
- const planningDir = path.join(cwd, '.planning');
1308
- core.output(intel.intelSnapshot(planningDir), raw);
1309
- } else if (subcommand === 'patch-meta') {
1310
- const filePath = args[2];
1311
- if (!filePath) error('Usage: gsd-tools intel patch-meta <file-path>', ERROR_REASON.USAGE);
1312
- core.output(intel.intelPatchMeta(path.resolve(cwd, filePath)), raw);
1313
- } else if (subcommand === 'validate') {
1314
- const planningDir = path.join(cwd, '.planning');
1315
- core.output(intel.intelValidate(planningDir), raw);
1316
- } else if (subcommand === 'extract-exports') {
1317
- const filePath = args[2];
1318
- if (!filePath) error('Usage: gsd-tools intel extract-exports <file-path>', ERROR_REASON.USAGE);
1319
- core.output(intel.intelExtractExports(path.resolve(cwd, filePath)), raw);
1320
- } else if (subcommand === 'update') {
1321
- const planningDir = path.join(cwd, '.planning');
1322
- core.output(intel.intelUpdate(planningDir), raw);
1323
- } else if (subcommand === 'api-surface') {
1324
- const planningDir = path.join(cwd, '.planning');
1325
- core.output(intel.intelApiSurface(planningDir), raw);
1326
- } else {
1327
- error('Unknown intel subcommand. Available: query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1328
- }
1329
- break;
1330
- }
1331
-
1332
- // ─── Graphify ──────────────────────────────────────────────────────────
1333
-
1334
- case 'graphify': {
1335
- const graphify = require('./lib/graphify.cjs');
1336
- const subcommand = args[1];
1337
- if (subcommand === 'query') {
1338
- const term = args[2];
1339
- if (!term) error('Usage: gsd-tools graphify query <term>', ERROR_REASON.USAGE);
1340
- const budgetIdx = args.indexOf('--budget');
1341
- const budget = budgetIdx !== -1 ? parseInt(args[budgetIdx + 1], 10) : null;
1342
- core.output(graphify.graphifyQuery(cwd, term, { budget }), raw);
1343
- } else if (subcommand === 'status') {
1344
- core.output(graphify.graphifyStatus(cwd), raw);
1345
- } else if (subcommand === 'diff') {
1346
- core.output(graphify.graphifyDiff(cwd), raw);
1347
- } else if (subcommand === 'build') {
1348
- if (args[2] === 'snapshot') {
1349
- core.output(graphify.writeSnapshot(cwd), raw);
1350
- } else {
1351
- core.output(graphify.graphifyBuild(cwd), raw);
1352
- }
1353
- } else {
1354
- error('Unknown graphify subcommand. Available: build, query, status, diff', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1355
- }
1356
- break;
1357
- }
1358
-
1359
1440
  // ─── Documentation ────────────────────────────────────────────────────
1360
1441
 
1361
1442
  case 'docs-init': {
@@ -1904,6 +1985,14 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1904
1985
  }
1905
1986
 
1906
1987
  default: {
1988
+ // ADR-959: try capability-registry dispatch before emitting the unknown-command error.
1989
+ // An unmigrated command still hits its hardcoded `case` above — untouched.
1990
+ // A migrated command's `case` is removed at cutover, so it reaches here and
1991
+ // dispatchCapabilityCommand routes it to the capability's registered router.
1992
+ // commandFamilies now includes migrated capabilities (e.g. graphify → graphify-command-router.cjs);
1993
+ // this returns true when a registered capability owns the command, false otherwise.
1994
+ if (dispatchCapabilityCommand({ command, args, cwd, raw, error })) break;
1995
+
1907
1996
  // #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
1908
1997
  // above split it so `command` here is the head ("foo"). Use
1909
1998
  // originalCommand to reconstruct the original dotted form and suggest
@@ -1925,4 +2014,12 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1925
2014
  }
1926
2015
  }
1927
2016
 
1928
- runMain(main);
2017
+ // ─── CLI entry point ──────────────────────────────────────────────────────────
2018
+ if (require.main === module) {
2019
+ runMain(main);
2020
+ }
2021
+
2022
+ // ─── Exports (for tests) ──────────────────────────────────────────────────────
2023
+ // ADR-959: export dispatchCapabilityCommand so tests can exercise it with
2024
+ // synthetic registry + requireModule injections.
2025
+ module.exports = { dispatchCapabilityCommand };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+ /**
3
+ * Audit command routers — CLI dispatchers for `gsd-tools audit-uat` and
4
+ * `gsd-tools audit-open`.
5
+ *
6
+ * ADR-959 (phase 4d-impl-3): audit command family cutover.
7
+ * Extracted from the hardcoded `case 'audit-uat':` and `case 'audit-open':`
8
+ * arms in gsd-tools.cjs. Behaviour is preserved byte-for-behaviour from the
9
+ * prior inline cases; the dispatch path now flows:
10
+ * default → dispatchCapabilityCommand →
11
+ * require(audit-command-router.cjs) → routeAuditUat | routeAuditOpen.
12
+ *
13
+ * Router signatures: { args, cwd, raw, error } — identical to the existing
14
+ * host routers. No new handler/arg convention; the capability registry
15
+ * discovers these routers by name.
16
+ *
17
+ * Test seam: pass `_uat` / `_audit` / `_core` in the options object to inject
18
+ * recording mocks instead of the real modules. The `_`-prefix follows the
19
+ * repo's established seam convention (see graphify-command-router.cts).
20
+ * Production callers omit them.
21
+ *
22
+ * Lazy requires: uat.cjs and audit.cjs are required INSIDE each route function
23
+ * so the unneeded module is never loaded (preserves equivalence with the old
24
+ * inline case arms which each required only their own module).
25
+ */
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ const core = require("./core.cjs");
28
+ // ─── routeAuditUat ────────────────────────────────────────────────────────────
29
+ function routeAuditUat({ args, cwd, raw, error, _uat }) {
30
+ // Suppress unused-variable warnings for args/error — this command has no
31
+ // subcommands and passes raw through directly to the uat module.
32
+ void args;
33
+ void error;
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
35
+ const u = _uat ?? require('./uat.cjs');
36
+ u.cmdAuditUat(cwd, raw);
37
+ }
38
+ // ─── routeAuditOpen ──────────────────────────────────────────────────────────
39
+ function routeAuditOpen({ args, cwd, raw, error, _audit, _core }) {
40
+ // Suppress unused-variable warning for error — audit-open has no subcommand
41
+ // dispatch that would call error(); only flag parsing occurs here.
42
+ void error;
43
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
44
+ const a = _audit ?? require('./audit.cjs');
45
+ const c = _core ?? core;
46
+ const wantJson = args.includes('--json');
47
+ const result = a.auditOpenArtifacts(cwd);
48
+ if (wantJson) {
49
+ // core.output JSON-stringifies its first arg; pass the object directly.
50
+ c.output(result, raw);
51
+ }
52
+ else {
53
+ // Human-readable report must bypass JSON encoding — use the rawValue
54
+ // form (third arg) which core.output emits verbatim.
55
+ c.output(null, true, a.formatAuditReport(result));
56
+ }
57
+ }
58
+ module.exports = {
59
+ routeAuditUat,
60
+ routeAuditOpen,
61
+ };