@pareto4/pp-core 1.5.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 (618) hide show
  1. package/.claude-plugin/plugin.json +23 -0
  2. package/GEMINI.md +53 -0
  3. package/LICENSE +22 -0
  4. package/README.md +100 -0
  5. package/agents/pp-advisor-researcher.md +108 -0
  6. package/agents/pp-ai-researcher.md +114 -0
  7. package/agents/pp-assumptions-analyzer.md +105 -0
  8. package/agents/pp-code-fixer.md +669 -0
  9. package/agents/pp-code-reviewer.md +387 -0
  10. package/agents/pp-codebase-mapper.md +853 -0
  11. package/agents/pp-debug-session-manager.md +315 -0
  12. package/agents/pp-debugger.md +1453 -0
  13. package/agents/pp-doc-classifier.md +168 -0
  14. package/agents/pp-doc-synthesizer.md +204 -0
  15. package/agents/pp-doc-verifier.md +217 -0
  16. package/agents/pp-doc-writer.md +616 -0
  17. package/agents/pp-domain-researcher.md +147 -0
  18. package/agents/pp-eval-auditor.md +191 -0
  19. package/agents/pp-eval-planner.md +154 -0
  20. package/agents/pp-executor.md +808 -0
  21. package/agents/pp-framework-selector.md +160 -0
  22. package/agents/pp-integration-checker.md +470 -0
  23. package/agents/pp-intel-updater.md +339 -0
  24. package/agents/pp-mempalace-curator.md +47 -0
  25. package/agents/pp-nyquist-auditor.md +204 -0
  26. package/agents/pp-pattern-mapper.md +335 -0
  27. package/agents/pp-phase-researcher.md +867 -0
  28. package/agents/pp-plan-checker.md +988 -0
  29. package/agents/pp-planner.md +1052 -0
  30. package/agents/pp-project-researcher.md +612 -0
  31. package/agents/pp-research-synthesizer.md +260 -0
  32. package/agents/pp-roadmapper.md +741 -0
  33. package/agents/pp-security-auditor.md +156 -0
  34. package/agents/pp-ui-auditor.md +495 -0
  35. package/agents/pp-ui-checker.md +309 -0
  36. package/agents/pp-ui-researcher.md +375 -0
  37. package/agents/pp-user-profiler.md +171 -0
  38. package/agents/pp-verifier.md +967 -0
  39. package/assets/gsd-logo-2000-transparent.png +0 -0
  40. package/assets/gsd-logo-2000-transparent.svg +17 -0
  41. package/assets/gsd-logo-2000.png +0 -0
  42. package/assets/gsd-logo-2000.svg +21 -0
  43. package/assets/terminal.svg +68 -0
  44. package/bin/install.js +12370 -0
  45. package/bin/lib/ui-safety-gate.cjs +107 -0
  46. package/commands/pp-add-tests.md +42 -0
  47. package/commands/pp-ai-integration-phase.md +37 -0
  48. package/commands/pp-audit-fix.md +34 -0
  49. package/commands/pp-audit-milestone.md +37 -0
  50. package/commands/pp-audit-uat.md +24 -0
  51. package/commands/pp-autonomous.md +51 -0
  52. package/commands/pp-capture.md +62 -0
  53. package/commands/pp-cleanup.md +24 -0
  54. package/commands/pp-code-review.md +59 -0
  55. package/commands/pp-complete-milestone.md +143 -0
  56. package/commands/pp-config.md +56 -0
  57. package/commands/pp-debug.md +52 -0
  58. package/commands/pp-discovery.md +34 -0
  59. package/commands/pp-discuss-phase.md +77 -0
  60. package/commands/pp-docs-update.md +49 -0
  61. package/commands/pp-eval-review.md +33 -0
  62. package/commands/pp-execute-phase.md +65 -0
  63. package/commands/pp-explore.md +27 -0
  64. package/commands/pp-extract-learnings.md +23 -0
  65. package/commands/pp-fast.md +31 -0
  66. package/commands/pp-forensics.md +57 -0
  67. package/commands/pp-graphify.md +204 -0
  68. package/commands/pp-health.md +31 -0
  69. package/commands/pp-help.md +28 -0
  70. package/commands/pp-import.md +45 -0
  71. package/commands/pp-inbox.md +39 -0
  72. package/commands/pp-ingest-docs.md +42 -0
  73. package/commands/pp-manager.md +45 -0
  74. package/commands/pp-map-codebase.md +83 -0
  75. package/commands/pp-mempalace-capture.md +71 -0
  76. package/commands/pp-mempalace-recall.md +102 -0
  77. package/commands/pp-milestone-summary.md +51 -0
  78. package/commands/pp-mvp-phase.md +45 -0
  79. package/commands/pp-new-milestone.md +45 -0
  80. package/commands/pp-new-project.md +47 -0
  81. package/commands/pp-ns-context.md +25 -0
  82. package/commands/pp-ns-ideate.md +24 -0
  83. package/commands/pp-ns-manage.md +36 -0
  84. package/commands/pp-ns-project.md +27 -0
  85. package/commands/pp-ns-review.md +29 -0
  86. package/commands/pp-ns-workflow.md +35 -0
  87. package/commands/pp-pause-work.md +43 -0
  88. package/commands/pp-phase.md +56 -0
  89. package/commands/pp-plan-phase.md +63 -0
  90. package/commands/pp-plan-review-convergence.md +60 -0
  91. package/commands/pp-pr-branch.md +26 -0
  92. package/commands/pp-profile-user.md +46 -0
  93. package/commands/pp-progress.md +49 -0
  94. package/commands/pp-quick.md +174 -0
  95. package/commands/pp-resume-work.md +30 -0
  96. package/commands/pp-review-backlog.md +63 -0
  97. package/commands/pp-review.md +42 -0
  98. package/commands/pp-secure-phase.md +36 -0
  99. package/commands/pp-settings.md +29 -0
  100. package/commands/pp-ship.md +24 -0
  101. package/commands/pp-sketch.md +60 -0
  102. package/commands/pp-spec-phase.md +63 -0
  103. package/commands/pp-spike.md +57 -0
  104. package/commands/pp-stats.md +20 -0
  105. package/commands/pp-surface.md +162 -0
  106. package/commands/pp-thread.md +24 -0
  107. package/commands/pp-ui-phase.md +35 -0
  108. package/commands/pp-ui-review.md +33 -0
  109. package/commands/pp-ultraplan-phase.md +34 -0
  110. package/commands/pp-undo.md +35 -0
  111. package/commands/pp-update.md +49 -0
  112. package/commands/pp-validate-phase.md +36 -0
  113. package/commands/pp-verify-work.md +39 -0
  114. package/commands/pp-workspace.md +52 -0
  115. package/commands/pp-workstreams.md +70 -0
  116. package/gemini-extension.json +6 -0
  117. package/hooks/dist/lib/git-cmd.js +150 -0
  118. package/hooks/dist/lib/pp-graphify-rebuild.sh +65 -0
  119. package/hooks/dist/managed-hooks-registry.cjs +39 -0
  120. package/hooks/dist/pp-check-update-worker.js +108 -0
  121. package/hooks/dist/pp-check-update.js +66 -0
  122. package/hooks/dist/pp-config-reload.js +133 -0
  123. package/hooks/dist/pp-context-monitor.js +196 -0
  124. package/hooks/dist/pp-cursor-post-tool.js +75 -0
  125. package/hooks/dist/pp-cursor-session-start.js +52 -0
  126. package/hooks/dist/pp-ensure-canonical-path.js +305 -0
  127. package/hooks/dist/pp-graphify-update.sh +158 -0
  128. package/hooks/dist/pp-phase-boundary.sh +47 -0
  129. package/hooks/dist/pp-prompt-guard.js +97 -0
  130. package/hooks/dist/pp-read-guard.js +101 -0
  131. package/hooks/dist/pp-read-injection-scanner.js +203 -0
  132. package/hooks/dist/pp-session-state.sh +59 -0
  133. package/hooks/dist/pp-statusline.js +566 -0
  134. package/hooks/dist/pp-update-banner.js +138 -0
  135. package/hooks/dist/pp-validate-commit.sh +57 -0
  136. package/hooks/dist/pp-workflow-guard.js +167 -0
  137. package/hooks/dist/pp-worktree-path-guard.js +185 -0
  138. package/hooks/hooks.json +70 -0
  139. package/hooks/lib/git-cmd.js +150 -0
  140. package/hooks/lib/pp-graphify-rebuild.sh +65 -0
  141. package/hooks/managed-hooks-registry.cjs +39 -0
  142. package/hooks/pp-check-update-worker.js +108 -0
  143. package/hooks/pp-check-update.js +66 -0
  144. package/hooks/pp-config-reload.js +133 -0
  145. package/hooks/pp-context-monitor.js +196 -0
  146. package/hooks/pp-cursor-post-tool.js +75 -0
  147. package/hooks/pp-cursor-session-start.js +52 -0
  148. package/hooks/pp-ensure-canonical-path.js +305 -0
  149. package/hooks/pp-graphify-update.sh +158 -0
  150. package/hooks/pp-phase-boundary.sh +47 -0
  151. package/hooks/pp-prompt-guard.js +97 -0
  152. package/hooks/pp-read-guard.js +101 -0
  153. package/hooks/pp-read-injection-scanner.js +203 -0
  154. package/hooks/pp-session-state.sh +59 -0
  155. package/hooks/pp-statusline.js +566 -0
  156. package/hooks/pp-update-banner.js +138 -0
  157. package/hooks/pp-validate-commit.sh +57 -0
  158. package/hooks/pp-workflow-guard.js +167 -0
  159. package/hooks/pp-worktree-path-guard.js +185 -0
  160. package/package.json +125 -0
  161. package/pp-core/bin/check-latest-version.cjs +161 -0
  162. package/pp-core/bin/lib/active-workstream-store.cjs +297 -0
  163. package/pp-core/bin/lib/adr-parser.cjs +399 -0
  164. package/pp-core/bin/lib/agent-command-router.cjs +68 -0
  165. package/pp-core/bin/lib/agent-install-check.cjs +143 -0
  166. package/pp-core/bin/lib/artifacts.cjs +51 -0
  167. package/pp-core/bin/lib/audit-command-router.cjs +61 -0
  168. package/pp-core/bin/lib/audit.cjs +743 -0
  169. package/pp-core/bin/lib/capability-activation.cjs +113 -0
  170. package/pp-core/bin/lib/capability-registry.cjs +3727 -0
  171. package/pp-core/bin/lib/capability-state.cjs +497 -0
  172. package/pp-core/bin/lib/capability-writer.cjs +355 -0
  173. package/pp-core/bin/lib/check-command-router.cjs +800 -0
  174. package/pp-core/bin/lib/cjs-command-router-adapter.cjs +81 -0
  175. package/pp-core/bin/lib/cli-exit.cjs +61 -0
  176. package/pp-core/bin/lib/clock.cjs +95 -0
  177. package/pp-core/bin/lib/clusters.cjs +131 -0
  178. package/pp-core/bin/lib/code-review-flags.cjs +59 -0
  179. package/pp-core/bin/lib/command-aliases.cjs +809 -0
  180. package/pp-core/bin/lib/command-arg-projection.cjs +55 -0
  181. package/pp-core/bin/lib/command-roster.cjs +19 -0
  182. package/pp-core/bin/lib/command-routing-hub.cjs +300 -0
  183. package/pp-core/bin/lib/commands.cjs +1244 -0
  184. package/pp-core/bin/lib/config-loader.cjs +714 -0
  185. package/pp-core/bin/lib/config-schema.cjs +58 -0
  186. package/pp-core/bin/lib/config-types.cjs +20 -0
  187. package/pp-core/bin/lib/config.cjs +796 -0
  188. package/pp-core/bin/lib/configuration.cjs +209 -0
  189. package/pp-core/bin/lib/context-utilization.cjs +48 -0
  190. package/pp-core/bin/lib/core-utils.cjs +198 -0
  191. package/pp-core/bin/lib/decisions.cjs +136 -0
  192. package/pp-core/bin/lib/docs.cjs +264 -0
  193. package/pp-core/bin/lib/drift.cjs +364 -0
  194. package/pp-core/bin/lib/edge-probe.cjs +196 -0
  195. package/pp-core/bin/lib/fallow-runner.cjs +153 -0
  196. package/pp-core/bin/lib/federated-config.cjs +182 -0
  197. package/pp-core/bin/lib/frontmatter.cjs +494 -0
  198. package/pp-core/bin/lib/gap-checker.cjs +260 -0
  199. package/pp-core/bin/lib/git-base-branch.cjs +220 -0
  200. package/pp-core/bin/lib/graphify-command-router.cjs +72 -0
  201. package/pp-core/bin/lib/graphify.cjs +472 -0
  202. package/pp-core/bin/lib/gsd2-import.cjs +456 -0
  203. package/pp-core/bin/lib/init-command-router.cjs +62 -0
  204. package/pp-core/bin/lib/init.cjs +1912 -0
  205. package/pp-core/bin/lib/install-profiles.cjs +814 -0
  206. package/pp-core/bin/lib/installer-migration-authoring.cjs +122 -0
  207. package/pp-core/bin/lib/installer-migration-report.cjs +351 -0
  208. package/pp-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +225 -0
  209. package/pp-core/bin/lib/installer-migrations/001-legacy-orphan-files.cjs +48 -0
  210. package/pp-core/bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs +94 -0
  211. package/pp-core/bin/lib/installer-migrations/003-rename-get-shit-done-to-gsd-core.cjs +108 -0
  212. package/pp-core/bin/lib/installer-migrations/004-prune-stale-pristine-snapshots.cjs +123 -0
  213. package/pp-core/bin/lib/installer-migrations/005-rename-gsd-core-to-pp-core.cjs +109 -0
  214. package/pp-core/bin/lib/installer-migrations/006-rename-gsd-command-namespace.cjs +135 -0
  215. package/pp-core/bin/lib/installer-migrations.cjs +823 -0
  216. package/pp-core/bin/lib/intel-command-router.cjs +119 -0
  217. package/pp-core/bin/lib/intel.cjs +584 -0
  218. package/pp-core/bin/lib/io.cjs +220 -0
  219. package/pp-core/bin/lib/learnings.cjs +270 -0
  220. package/pp-core/bin/lib/legacy-cleanup.cjs +253 -0
  221. package/pp-core/bin/lib/loop-host-contract.cjs +105 -0
  222. package/pp-core/bin/lib/loop-resolver.cjs +479 -0
  223. package/pp-core/bin/lib/milestone.cjs +381 -0
  224. package/pp-core/bin/lib/model-catalog.cjs +154 -0
  225. package/pp-core/bin/lib/model-profiles.cjs +24 -0
  226. package/pp-core/bin/lib/model-resolver.cjs +467 -0
  227. package/pp-core/bin/lib/observability/event.cjs +51 -0
  228. package/pp-core/bin/lib/observability/logger.cjs +146 -0
  229. package/pp-core/bin/lib/observability/redaction.cjs +48 -0
  230. package/pp-core/bin/lib/package-identity.cjs +35 -0
  231. package/pp-core/bin/lib/package-legitimacy.cjs +368 -0
  232. package/pp-core/bin/lib/phase-command-router.cjs +209 -0
  233. package/pp-core/bin/lib/phase-id.cjs +213 -0
  234. package/pp-core/bin/lib/phase-lifecycle.cjs +74 -0
  235. package/pp-core/bin/lib/phase-locator.cjs +145 -0
  236. package/pp-core/bin/lib/phase.cjs +1446 -0
  237. package/pp-core/bin/lib/phases-command-router.cjs +43 -0
  238. package/pp-core/bin/lib/plan-drift-guard.cjs +117 -0
  239. package/pp-core/bin/lib/plan-scan.cjs +91 -0
  240. package/pp-core/bin/lib/planning-workspace.cjs +245 -0
  241. package/pp-core/bin/lib/probe-core.cjs +402 -0
  242. package/pp-core/bin/lib/profile-output.cjs +1164 -0
  243. package/pp-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
  244. package/pp-core/bin/lib/profile-pipeline.cjs +519 -0
  245. package/pp-core/bin/lib/prohibition-enforcement.cjs +660 -0
  246. package/pp-core/bin/lib/project-root.cjs +119 -0
  247. package/pp-core/bin/lib/prompt-budget.cjs +305 -0
  248. package/pp-core/bin/lib/research-provider.cjs +137 -0
  249. package/pp-core/bin/lib/research-store.cjs +167 -0
  250. package/pp-core/bin/lib/review-reviewer-selection.cjs +121 -0
  251. package/pp-core/bin/lib/roadmap-command-router.cjs +166 -0
  252. package/pp-core/bin/lib/roadmap-parser.cjs +456 -0
  253. package/pp-core/bin/lib/roadmap-upgrade.cjs +476 -0
  254. package/pp-core/bin/lib/roadmap.cjs +738 -0
  255. package/pp-core/bin/lib/runtime-artifact-conversion.cjs +1966 -0
  256. package/pp-core/bin/lib/runtime-artifact-layout.cjs +342 -0
  257. package/pp-core/bin/lib/runtime-config-adapter-registry.cjs +103 -0
  258. package/pp-core/bin/lib/runtime-homes.cjs +272 -0
  259. package/pp-core/bin/lib/runtime-hooks-surface.cjs +1460 -0
  260. package/pp-core/bin/lib/runtime-name-policy.cjs +97 -0
  261. package/pp-core/bin/lib/runtime-slash.cjs +137 -0
  262. package/pp-core/bin/lib/schema-detect.cjs +159 -0
  263. package/pp-core/bin/lib/secrets.cjs +34 -0
  264. package/pp-core/bin/lib/security.cjs +480 -0
  265. package/pp-core/bin/lib/semver-compare.cjs +42 -0
  266. package/pp-core/bin/lib/shell-command-projection.cjs +550 -0
  267. package/pp-core/bin/lib/state-command-router.cjs +172 -0
  268. package/pp-core/bin/lib/state-document.cjs +312 -0
  269. package/pp-core/bin/lib/state.cjs +2587 -0
  270. package/pp-core/bin/lib/surface.cjs +505 -0
  271. package/pp-core/bin/lib/task-command-router.cjs +85 -0
  272. package/pp-core/bin/lib/teams-status.cjs +74 -0
  273. package/pp-core/bin/lib/template.cjs +246 -0
  274. package/pp-core/bin/lib/uat-predicate.cjs +329 -0
  275. package/pp-core/bin/lib/uat.cjs +359 -0
  276. package/pp-core/bin/lib/ui-safety-gate.cjs +98 -0
  277. package/pp-core/bin/lib/update-context.cjs +221 -0
  278. package/pp-core/bin/lib/validate-command-router.cjs +91 -0
  279. package/pp-core/bin/lib/validate.cjs +122 -0
  280. package/pp-core/bin/lib/verification-command-router.cjs +31 -0
  281. package/pp-core/bin/lib/verification.cjs +196 -0
  282. package/pp-core/bin/lib/verify-command-router.cjs +44 -0
  283. package/pp-core/bin/lib/verify.cjs +1932 -0
  284. package/pp-core/bin/lib/workstream-inventory-builder.cjs +81 -0
  285. package/pp-core/bin/lib/workstream-inventory.cjs +147 -0
  286. package/pp-core/bin/lib/workstream-name-policy.cjs +91 -0
  287. package/pp-core/bin/lib/workstream.cjs +386 -0
  288. package/pp-core/bin/lib/worktree-base-ref.cjs +350 -0
  289. package/pp-core/bin/lib/worktree-safety.cjs +985 -0
  290. package/pp-core/bin/pp-tools.cjs +2239 -0
  291. package/pp-core/bin/pp_run +20 -0
  292. package/pp-core/bin/shared/config-defaults.manifest.json +97 -0
  293. package/pp-core/bin/shared/config-schema.manifest.json +176 -0
  294. package/pp-core/bin/shared/model-catalog.json +160 -0
  295. package/pp-core/bin/shared/runtime-aliases.manifest.json +79 -0
  296. package/pp-core/bin/verify-reapply-patches.cjs +400 -0
  297. package/pp-core/contexts/dev.md +21 -0
  298. package/pp-core/contexts/research.md +22 -0
  299. package/pp-core/contexts/review.md +23 -0
  300. package/pp-core/references/agent-contracts.md +79 -0
  301. package/pp-core/references/ai-evals.md +156 -0
  302. package/pp-core/references/ai-frameworks.md +186 -0
  303. package/pp-core/references/artifact-types.md +131 -0
  304. package/pp-core/references/autonomous-smart-discuss.md +277 -0
  305. package/pp-core/references/checkpoints.md +814 -0
  306. package/pp-core/references/common-bug-patterns.md +114 -0
  307. package/pp-core/references/context-budget.md +85 -0
  308. package/pp-core/references/continuation-format.md +253 -0
  309. package/pp-core/references/debugger-philosophy.md +76 -0
  310. package/pp-core/references/decimal-phase-calculation.md +64 -0
  311. package/pp-core/references/doc-conflict-engine.md +91 -0
  312. package/pp-core/references/domain-probes.md +125 -0
  313. package/pp-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
  314. package/pp-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
  315. package/pp-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
  316. package/pp-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
  317. package/pp-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
  318. package/pp-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
  319. package/pp-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
  320. package/pp-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
  321. package/pp-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
  322. package/pp-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
  323. package/pp-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
  324. package/pp-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
  325. package/pp-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
  326. package/pp-core/references/edge-probe.md +272 -0
  327. package/pp-core/references/execute-mvp-tdd.md +81 -0
  328. package/pp-core/references/executor-examples.md +110 -0
  329. package/pp-core/references/few-shot-examples/plan-checker.md +73 -0
  330. package/pp-core/references/few-shot-examples/verifier.md +109 -0
  331. package/pp-core/references/gate-prompts.md +100 -0
  332. package/pp-core/references/gates.md +70 -0
  333. package/pp-core/references/git-integration.md +298 -0
  334. package/pp-core/references/git-planning-commit.md +40 -0
  335. package/pp-core/references/ios-scaffold.md +123 -0
  336. package/pp-core/references/loop-hook-dispatch.md +61 -0
  337. package/pp-core/references/mandatory-initial-read.md +2 -0
  338. package/pp-core/references/model-profile-resolution.md +38 -0
  339. package/pp-core/references/model-profiles.md +245 -0
  340. package/pp-core/references/mvp-concepts.md +49 -0
  341. package/pp-core/references/phase-argument-parsing.md +61 -0
  342. package/pp-core/references/planner-antipatterns.md +176 -0
  343. package/pp-core/references/planner-chunked.md +49 -0
  344. package/pp-core/references/planner-gap-closure.md +62 -0
  345. package/pp-core/references/planner-graphify-auto-update.md +67 -0
  346. package/pp-core/references/planner-guidance.md +186 -0
  347. package/pp-core/references/planner-human-verify-mode.md +57 -0
  348. package/pp-core/references/planner-interface-context.md +62 -0
  349. package/pp-core/references/planner-load-graph-context.md +36 -0
  350. package/pp-core/references/planner-mvp-mode.md +53 -0
  351. package/pp-core/references/planner-reviews.md +42 -0
  352. package/pp-core/references/planner-revision.md +87 -0
  353. package/pp-core/references/planner-source-audit.md +73 -0
  354. package/pp-core/references/planning-config.md +477 -0
  355. package/pp-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
  356. package/pp-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
  357. package/pp-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
  358. package/pp-core/references/prohibition-probe.md +326 -0
  359. package/pp-core/references/project-skills-discovery.md +19 -0
  360. package/pp-core/references/questioning.md +162 -0
  361. package/pp-core/references/research-documentation-lookup.md +29 -0
  362. package/pp-core/references/research-philosophy.md +29 -0
  363. package/pp-core/references/research-verification-protocol.md +27 -0
  364. package/pp-core/references/revision-loop.md +97 -0
  365. package/pp-core/references/scout-codebase.md +51 -0
  366. package/pp-core/references/skeleton-template.md +48 -0
  367. package/pp-core/references/sketch-interactivity.md +41 -0
  368. package/pp-core/references/sketch-theme-system.md +94 -0
  369. package/pp-core/references/sketch-tooling.md +45 -0
  370. package/pp-core/references/sketch-variant-patterns.md +81 -0
  371. package/pp-core/references/spidr-splitting.md +69 -0
  372. package/pp-core/references/tdd.md +330 -0
  373. package/pp-core/references/thinking-models-debug.md +44 -0
  374. package/pp-core/references/thinking-models-execution.md +50 -0
  375. package/pp-core/references/thinking-models-planning.md +62 -0
  376. package/pp-core/references/thinking-models-research.md +50 -0
  377. package/pp-core/references/thinking-models-verification.md +55 -0
  378. package/pp-core/references/thinking-partner.md +96 -0
  379. package/pp-core/references/ui-brand.md +162 -0
  380. package/pp-core/references/universal-anti-patterns.md +63 -0
  381. package/pp-core/references/user-profiling.md +681 -0
  382. package/pp-core/references/user-story-template.md +58 -0
  383. package/pp-core/references/verification-overrides.md +227 -0
  384. package/pp-core/references/verification-patterns.md +612 -0
  385. package/pp-core/references/verify-mvp-mode.md +85 -0
  386. package/pp-core/references/workstream-flag.md +111 -0
  387. package/pp-core/references/worktree-branch-check.md +44 -0
  388. package/pp-core/references/worktree-path-safety.md +67 -0
  389. package/pp-core/templates/AI-SPEC.md +246 -0
  390. package/pp-core/templates/DEBUG.md +169 -0
  391. package/pp-core/templates/README.md +77 -0
  392. package/pp-core/templates/SECURITY.md +61 -0
  393. package/pp-core/templates/UAT.md +265 -0
  394. package/pp-core/templates/UI-SPEC.md +100 -0
  395. package/pp-core/templates/VALIDATION.md +76 -0
  396. package/pp-core/templates/claude-md.md +145 -0
  397. package/pp-core/templates/codebase/architecture.md +255 -0
  398. package/pp-core/templates/codebase/concerns.md +310 -0
  399. package/pp-core/templates/codebase/conventions.md +307 -0
  400. package/pp-core/templates/codebase/integrations.md +280 -0
  401. package/pp-core/templates/codebase/stack.md +186 -0
  402. package/pp-core/templates/codebase/structure.md +285 -0
  403. package/pp-core/templates/codebase/testing.md +480 -0
  404. package/pp-core/templates/config.json +62 -0
  405. package/pp-core/templates/context.md +352 -0
  406. package/pp-core/templates/continue-here.md +78 -0
  407. package/pp-core/templates/copilot-instructions.md +7 -0
  408. package/pp-core/templates/debug-subagent-prompt.md +91 -0
  409. package/pp-core/templates/dev-preferences.md +21 -0
  410. package/pp-core/templates/discovery.md +146 -0
  411. package/pp-core/templates/discussion-log.md +63 -0
  412. package/pp-core/templates/milestone-archive.md +123 -0
  413. package/pp-core/templates/milestone.md +115 -0
  414. package/pp-core/templates/phase-prompt.md +610 -0
  415. package/pp-core/templates/planner-subagent-prompt.md +117 -0
  416. package/pp-core/templates/project.md +203 -0
  417. package/pp-core/templates/requirements.md +231 -0
  418. package/pp-core/templates/research-project/ARCHITECTURE.md +204 -0
  419. package/pp-core/templates/research-project/FEATURES.md +147 -0
  420. package/pp-core/templates/research-project/PITFALLS.md +200 -0
  421. package/pp-core/templates/research-project/STACK.md +120 -0
  422. package/pp-core/templates/research-project/SUMMARY.md +170 -0
  423. package/pp-core/templates/research.md +592 -0
  424. package/pp-core/templates/retrospective.md +54 -0
  425. package/pp-core/templates/roadmap.md +202 -0
  426. package/pp-core/templates/spec.md +333 -0
  427. package/pp-core/templates/state.md +195 -0
  428. package/pp-core/templates/summary-complex.md +60 -0
  429. package/pp-core/templates/summary-minimal.md +42 -0
  430. package/pp-core/templates/summary-standard.md +49 -0
  431. package/pp-core/templates/summary.md +249 -0
  432. package/pp-core/templates/user-profile.md +146 -0
  433. package/pp-core/templates/user-setup.md +311 -0
  434. package/pp-core/templates/verification-report.md +335 -0
  435. package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -0
  436. package/pp-core/workflows/add-backlog.md +91 -0
  437. package/pp-core/workflows/add-phase.md +113 -0
  438. package/pp-core/workflows/add-tests.md +355 -0
  439. package/pp-core/workflows/add-todo.md +161 -0
  440. package/pp-core/workflows/ai-integration-phase.md +295 -0
  441. package/pp-core/workflows/analyze-dependencies.md +96 -0
  442. package/pp-core/workflows/audit-fix.md +178 -0
  443. package/pp-core/workflows/audit-milestone.md +362 -0
  444. package/pp-core/workflows/audit-uat.md +110 -0
  445. package/pp-core/workflows/autonomous.md +886 -0
  446. package/pp-core/workflows/check-todos.md +180 -0
  447. package/pp-core/workflows/cleanup.md +195 -0
  448. package/pp-core/workflows/code-review-fix.md +506 -0
  449. package/pp-core/workflows/code-review.md +696 -0
  450. package/pp-core/workflows/complete-milestone.md +857 -0
  451. package/pp-core/workflows/debug.md +237 -0
  452. package/pp-core/workflows/diagnose-issues.md +245 -0
  453. package/pp-core/workflows/discovery-phase.md +291 -0
  454. package/pp-core/workflows/discovery-sprint.md +1546 -0
  455. package/pp-core/workflows/discuss-phase/modes/advisor.md +176 -0
  456. package/pp-core/workflows/discuss-phase/modes/all.md +28 -0
  457. package/pp-core/workflows/discuss-phase/modes/analyze.md +44 -0
  458. package/pp-core/workflows/discuss-phase/modes/auto.md +57 -0
  459. package/pp-core/workflows/discuss-phase/modes/batch.md +52 -0
  460. package/pp-core/workflows/discuss-phase/modes/chain.md +98 -0
  461. package/pp-core/workflows/discuss-phase/modes/default.md +141 -0
  462. package/pp-core/workflows/discuss-phase/modes/power.md +44 -0
  463. package/pp-core/workflows/discuss-phase/modes/text.md +55 -0
  464. package/pp-core/workflows/discuss-phase/templates/checkpoint.json +18 -0
  465. package/pp-core/workflows/discuss-phase/templates/context.md +136 -0
  466. package/pp-core/workflows/discuss-phase/templates/discussion-log.md +50 -0
  467. package/pp-core/workflows/discuss-phase-assumptions.md +675 -0
  468. package/pp-core/workflows/discuss-phase-power.md +291 -0
  469. package/pp-core/workflows/discuss-phase.md +520 -0
  470. package/pp-core/workflows/do.md +111 -0
  471. package/pp-core/workflows/docs-update.md +1168 -0
  472. package/pp-core/workflows/edit-phase.md +295 -0
  473. package/pp-core/workflows/eval-review.md +159 -0
  474. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +95 -0
  475. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  476. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +117 -0
  477. package/pp-core/workflows/execute-phase/steps/worktree-recovery-policy.md +9 -0
  478. package/pp-core/workflows/execute-phase.md +1707 -0
  479. package/pp-core/workflows/execute-plan.md +541 -0
  480. package/pp-core/workflows/explore.md +146 -0
  481. package/pp-core/workflows/extract-learnings.md +243 -0
  482. package/pp-core/workflows/fast.md +124 -0
  483. package/pp-core/workflows/forensics.md +279 -0
  484. package/pp-core/workflows/graduation.md +196 -0
  485. package/pp-core/workflows/health.md +224 -0
  486. package/pp-core/workflows/help/modes/brief.md +22 -0
  487. package/pp-core/workflows/help/modes/default.md +50 -0
  488. package/pp-core/workflows/help/modes/full.md +791 -0
  489. package/pp-core/workflows/help/modes/topic.md +74 -0
  490. package/pp-core/workflows/help.md +24 -0
  491. package/pp-core/workflows/import.md +256 -0
  492. package/pp-core/workflows/inbox.md +387 -0
  493. package/pp-core/workflows/ingest-docs.md +340 -0
  494. package/pp-core/workflows/insert-phase.md +152 -0
  495. package/pp-core/workflows/list-phase-assumptions.md +178 -0
  496. package/pp-core/workflows/list-workspaces.md +57 -0
  497. package/pp-core/workflows/manager.md +435 -0
  498. package/pp-core/workflows/map-codebase.md +444 -0
  499. package/pp-core/workflows/milestone-summary.md +224 -0
  500. package/pp-core/workflows/mvp-phase.md +222 -0
  501. package/pp-core/workflows/new-milestone.md +643 -0
  502. package/pp-core/workflows/new-project.md +1563 -0
  503. package/pp-core/workflows/new-workspace.md +240 -0
  504. package/pp-core/workflows/next.md +347 -0
  505. package/pp-core/workflows/node-repair.md +92 -0
  506. package/pp-core/workflows/note.md +158 -0
  507. package/pp-core/workflows/pause-work.md +250 -0
  508. package/pp-core/workflows/plan-milestone-gaps.md +281 -0
  509. package/pp-core/workflows/plan-phase.md +1755 -0
  510. package/pp-core/workflows/plan-review-convergence.md +369 -0
  511. package/pp-core/workflows/plant-seed.md +230 -0
  512. package/pp-core/workflows/pr-branch.md +159 -0
  513. package/pp-core/workflows/profile-user.md +455 -0
  514. package/pp-core/workflows/progress.md +756 -0
  515. package/pp-core/workflows/quick.md +1045 -0
  516. package/pp-core/workflows/reapply-patches.md +443 -0
  517. package/pp-core/workflows/remove-phase.md +156 -0
  518. package/pp-core/workflows/remove-workspace.md +108 -0
  519. package/pp-core/workflows/resume-project.md +348 -0
  520. package/pp-core/workflows/review.md +765 -0
  521. package/pp-core/workflows/scan.md +107 -0
  522. package/pp-core/workflows/secure-phase.md +184 -0
  523. package/pp-core/workflows/session-report.md +146 -0
  524. package/pp-core/workflows/settings-advanced.md +816 -0
  525. package/pp-core/workflows/settings-integrations.md +312 -0
  526. package/pp-core/workflows/settings.md +592 -0
  527. package/pp-core/workflows/ship.md +465 -0
  528. package/pp-core/workflows/sketch-wrap-up.md +286 -0
  529. package/pp-core/workflows/sketch.md +361 -0
  530. package/pp-core/workflows/spec-phase.md +495 -0
  531. package/pp-core/workflows/spike-wrap-up.md +307 -0
  532. package/pp-core/workflows/spike.md +453 -0
  533. package/pp-core/workflows/stats.md +80 -0
  534. package/pp-core/workflows/sync-skills.md +182 -0
  535. package/pp-core/workflows/thread.md +222 -0
  536. package/pp-core/workflows/transition.md +694 -0
  537. package/pp-core/workflows/ui-phase.md +328 -0
  538. package/pp-core/workflows/ui-review.md +193 -0
  539. package/pp-core/workflows/ultraplan-phase.md +199 -0
  540. package/pp-core/workflows/undo.md +314 -0
  541. package/pp-core/workflows/update.md +501 -0
  542. package/pp-core/workflows/validate-phase.md +183 -0
  543. package/pp-core/workflows/verify-phase.md +569 -0
  544. package/pp-core/workflows/verify-work.md +804 -0
  545. package/scripts/affected-tests-lib.cjs +554 -0
  546. package/scripts/audit-workflow-script-paths.cjs +73 -0
  547. package/scripts/base64-scan.sh +351 -0
  548. package/scripts/build-hooks.js +254 -0
  549. package/scripts/changeset/README.md +129 -0
  550. package/scripts/changeset/cli.cjs +597 -0
  551. package/scripts/changeset/github-release-notes.cjs +199 -0
  552. package/scripts/changeset/lint.cjs +147 -0
  553. package/scripts/changeset/new.cjs +151 -0
  554. package/scripts/changeset/parse.cjs +114 -0
  555. package/scripts/changeset/render.cjs +34 -0
  556. package/scripts/changeset/serialize.cjs +130 -0
  557. package/scripts/check-alias-drift.cjs +114 -0
  558. package/scripts/check-env.cjs +312 -0
  559. package/scripts/check-npm-integrity.cjs +215 -0
  560. package/scripts/ci-guard-runner.cjs +22 -0
  561. package/scripts/ci-prepare-test-scope.cjs +93 -0
  562. package/scripts/ci-rebase-check.cjs +86 -0
  563. package/scripts/ci-test-scope.cjs +441 -0
  564. package/scripts/command-contract-helpers.cjs +64 -0
  565. package/scripts/diff-touches-shipped-paths.cjs +149 -0
  566. package/scripts/fix-slash-commands.cjs +161 -0
  567. package/scripts/gen-capability-registry.cjs +2646 -0
  568. package/scripts/gen-commands-reference.cjs +139 -0
  569. package/scripts/gen-inventory-manifest.cjs +114 -0
  570. package/scripts/gen-loop-host-contract.cjs +526 -0
  571. package/scripts/gen-research-agents.cjs +276 -0
  572. package/scripts/generate-package-identity.cjs +134 -0
  573. package/scripts/issue-dedupe.cjs +278 -0
  574. package/scripts/issue-version-gate.cjs +140 -0
  575. package/scripts/lib/allowlist-ratchet.cjs +236 -0
  576. package/scripts/lib/cli-exit.cjs +56 -0
  577. package/scripts/lint-allow-test-rule-refs.allowlist.json +328 -0
  578. package/scripts/lint-allow-test-rule-refs.cjs +162 -0
  579. package/scripts/lint-command-contract.cjs +118 -0
  580. package/scripts/lint-descriptions.cjs +87 -0
  581. package/scripts/lint-docs-required.cjs +222 -0
  582. package/scripts/lint-legacy-dir-name.cjs +166 -0
  583. package/scripts/lint-package-identity-drift.cjs +141 -0
  584. package/scripts/lint-pr-check-project-dir.cjs +99 -0
  585. package/scripts/lint-regression-test-names.allowlist.json +269 -0
  586. package/scripts/lint-regression-test-names.cjs +117 -0
  587. package/scripts/lint-shell-command-projection-drift.cjs +62 -0
  588. package/scripts/lint-skill-deps.cjs +185 -0
  589. package/scripts/lint-test-file-count.allowlist.json +181 -0
  590. package/scripts/lint-test-file-count.cjs +246 -0
  591. package/scripts/lint-windows-test-portability.cjs +178 -0
  592. package/scripts/mutation-matrix.cjs +323 -0
  593. package/scripts/pr-target-policy.cjs +63 -0
  594. package/scripts/pr-template-policy.cjs +268 -0
  595. package/scripts/prompt-injection-scan.sh +207 -0
  596. package/scripts/registry-smoke.cjs +337 -0
  597. package/scripts/release-notes/discord-release-summary.cjs +373 -0
  598. package/scripts/release-notes/format-github-release-notes.cjs +261 -0
  599. package/scripts/release-tarball-smoke.cjs +763 -0
  600. package/scripts/research-profiles.cjs +149 -0
  601. package/scripts/run-affected-tests.cjs +7 -0
  602. package/scripts/run-tests.cjs +589 -0
  603. package/scripts/secret-scan-lint.sh +231 -0
  604. package/scripts/secret-scan.sh +358 -0
  605. package/scripts/setup-branch-protection.sh +234 -0
  606. package/scripts/strip-prose-atrefs.cjs +106 -0
  607. package/scripts/sync-manifest-versions.cjs +119 -0
  608. package/scripts/sync-next-version.cjs +133 -0
  609. package/scripts/sync-rulesets.sh +34 -0
  610. package/scripts/sync-runtime-launcher.cjs +425 -0
  611. package/scripts/test-failure-reasons.cjs +34 -0
  612. package/scripts/update-size-baseline.cjs +68 -0
  613. package/scripts/verify-npm-publish.cjs +240 -0
  614. package/scripts/verify-opengsd-sweep.cjs +288 -0
  615. package/scripts/verify-phase-02-rename.sh +258 -0
  616. package/scripts/verify-phase-03-rename.sh +376 -0
  617. package/scripts/workflow-policy.cjs +483 -0
  618. package/scripts/workflow-size.cjs +90 -0
@@ -0,0 +1,2239 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GSD Tools — CLI utility for GSD workflow operations.
5
+ *
6
+ * Replaces repetitive inline bash patterns across ~50 GSD command/workflow/agent files.
7
+ * Centralizes: config parsing, model resolution, phase lookup, git commits, summary verification.
8
+ *
9
+ * Usage: node gsd-tools.cjs <command> [args] [--raw] [--pick <field>]
10
+ *
11
+ * Atomic Commands:
12
+ * state load Load project config + state
13
+ * state json Output STATE.md frontmatter as JSON
14
+ * state update <field> <value> Update a STATE.md field
15
+ * state get [section] Get STATE.md content or section
16
+ * state patch --field val ... Batch update STATE.md fields
17
+ * state begin-phase --phase N --name S --plans C Update STATE.md for new phase start
18
+ * state signal-waiting --type T --question Q --options "A|B" --phase P Write WAITING.json signal
19
+ * state signal-resume Remove WAITING.json signal
20
+ * resolve-model <agent-type> Get model for agent based on profile
21
+ * find-phase <phase> Find phase directory by number
22
+ * commit <message> [--files f1 f2] [--no-verify] Commit planning docs
23
+ * commit-to-subrepo <msg> --files f1 f2 Route commits to sub-repos
24
+ * verify-summary <path> Verify a SUMMARY.md file
25
+ * generate-slug <text> Convert text to URL-safe slug
26
+ * current-timestamp [format] Get timestamp (full|date|filename)
27
+ * list-todos [area] Count and enumerate pending todos
28
+ * verify-path-exists <path> Check file/directory existence
29
+ * config-ensure-section Initialize .planning/config.json
30
+ * history-digest Aggregate all SUMMARY.md data
31
+ * summary-extract <path> [--fields] Extract structured data from SUMMARY.md
32
+ * state-snapshot Structured parse of STATE.md
33
+ * phase-plan-index <phase> Index plans with waves and status
34
+ * websearch <query> Search web via Brave API (if configured)
35
+ * [--limit N] [--freshness day|week|month]
36
+ *
37
+ * Phase Operations:
38
+ * phase next-decimal <phase> Calculate next decimal phase number
39
+ * phase add <description> [--id ID] Append new phase to roadmap + create dir
40
+ * phase insert <after> <description> Insert decimal phase after existing
41
+ * phase remove <phase> [--force] Remove phase, renumber all subsequent
42
+ * phase complete <phase> Mark phase done, update state + roadmap
43
+ *
44
+ * Roadmap Operations:
45
+ * roadmap get-phase <phase> Extract phase section from ROADMAP.md
46
+ * roadmap analyze Full roadmap parse with disk status
47
+ * roadmap update-plan-progress <N> Update progress table row from disk (PLAN vs SUMMARY counts)
48
+ * roadmap annotate-dependencies <N> Add wave dependency notes + cross-cutting constraints to ROADMAP.md
49
+ * roadmap validate Validate phase ID convention compliance
50
+ * roadmap upgrade [--apply] --convention milestone-prefixed Migrate phase IDs to M-NN convention
51
+ *
52
+ * Requirements Operations:
53
+ * requirements mark-complete <ids> Mark requirement IDs as complete in REQUIREMENTS.md
54
+ * Accepts: REQ-01,REQ-02 or REQ-01 REQ-02 or [REQ-01, REQ-02]
55
+ *
56
+ * Milestone Operations:
57
+ * milestone complete <version> Archive milestone, create MILESTONES.md
58
+ * [--name <name>]
59
+ * [--archive-phases] Move phase dirs to milestones/vX.Y-phases/
60
+ *
61
+ * User Story Validation:
62
+ * user-story validate --story "..." Validate "As a / I want to / so that" format
63
+ * Returns JSON { valid, errors[], slots: {role,capability,outcome} | null }
64
+ * --pick valid Emit bare boolean (for workflow boolean checks)
65
+ *
66
+ * Drift Guard (ADR-22):
67
+ * drift-guard authority Resolve effective source-grounding authority
68
+ * (reads plan_review.source_grounding_authority + intel.enabled from config)
69
+ * drift-guard severity --status <S> Classify a symbol verdict into { severity, hardBlock }
70
+ * [--authority <A>] Status: VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE
71
+ * Authority: grep|intel|treesitter|lsp|scip (default: config-resolved)
72
+ *
73
+ * Validation:
74
+ * validate consistency Check phase numbering, disk/roadmap sync
75
+ * validate health [--repair] Check .planning/ integrity, optionally repair
76
+ * validate agents Check GSD agent installation status
77
+ *
78
+ * Progress:
79
+ * progress [json|table|bar] Render progress in various formats
80
+ *
81
+ * Todos:
82
+ * todo complete <filename> Move todo from pending to completed
83
+ *
84
+ * UAT Audit:
85
+ * audit-uat Scan all phases for unresolved UAT/verification items
86
+ * uat render-checkpoint --file <path> Render the current UAT checkpoint block
87
+ *
88
+ * Open Artifact Audit:
89
+ * audit-open [--json] Scan all .planning/ artifact types for unresolved items
90
+ *
91
+ * Intel:
92
+ * intel query <term> Query intel files for a term
93
+ * intel status Show intel file freshness
94
+ * intel update Trigger intel refresh (returns agent spawn hint)
95
+ * intel diff Show changed intel entries since last snapshot
96
+ * intel snapshot Save current intel state as diff baseline
97
+ * intel patch-meta <file> Update _meta.updated_at in an intel file
98
+ * intel validate Validate intel file structure
99
+ * intel extract-exports <file> Extract exported symbols from a source file
100
+ * intel api-surface Render api-map.json into API-SURFACE.md
101
+ *
102
+ * Scaffolding:
103
+ * scaffold context --phase <N> Create CONTEXT.md template
104
+ * scaffold uat --phase <N> Create UAT.md template
105
+ * scaffold verification --phase <N> Create VERIFICATION.md template
106
+ * scaffold phase-dir --phase <N> Create phase directory
107
+ * --name <name>
108
+ *
109
+ * Frontmatter CRUD:
110
+ * frontmatter get <file> [--field k] Extract frontmatter as JSON
111
+ * frontmatter set <file> --field k Update single frontmatter field
112
+ * --value jsonVal
113
+ * frontmatter merge <file> Merge JSON into frontmatter
114
+ * --data '{json}'
115
+ * frontmatter validate <file> Validate required fields
116
+ * --schema plan|summary|verification
117
+ *
118
+ * Verification Suite:
119
+ * verify plan-structure <file> Check PLAN.md structure + tasks
120
+ * verify phase-completeness <phase> Check all plans have summaries
121
+ * verify references <file> Check @-refs + paths resolve
122
+ * verify commits <h1> [h2] ... Batch verify commit hashes
123
+ * verify artifacts <plan-file> Check must_haves.artifacts
124
+ * verify key-links <plan-file> Check must_haves.key_links
125
+ * verify schema-drift <phase> [--skip] Detect schema file changes without push
126
+ * verify codebase-drift Detect structural drift since last codebase map (#2003)
127
+ *
128
+ * Template Fill:
129
+ * template fill summary --phase N Create pre-filled SUMMARY.md
130
+ * [--plan M] [--name "..."]
131
+ * [--fields '{json}']
132
+ * template fill plan --phase N Create pre-filled PLAN.md
133
+ * [--plan M] [--type execute|tdd]
134
+ * [--wave N] [--fields '{json}']
135
+ * template fill verification Create pre-filled VERIFICATION.md
136
+ * --phase N [--fields '{json}']
137
+ *
138
+ * State Progression:
139
+ * state advance-plan Increment plan counter
140
+ * state record-metric --phase N Record execution metrics
141
+ * --plan M --duration Xmin
142
+ * [--tasks N] [--files N]
143
+ * state update-progress Recalculate progress bar
144
+ * state add-decision --summary "..." Add decision to STATE.md
145
+ * [--phase N] [--rationale "..."]
146
+ * [--summary-file path] [--rationale-file path]
147
+ * state add-blocker --text "..." Add blocker
148
+ * [--text-file path]
149
+ * state resolve-blocker --text "..." Remove blocker
150
+ * state record-session Update session continuity
151
+ * --stopped-at "..."
152
+ * [--resume-file path]
153
+ *
154
+ * Compound Commands (workflow-specific initialization):
155
+ * init execute-phase <phase> All context for execute-phase workflow
156
+ * init plan-phase <phase> All context for plan-phase workflow
157
+ * init new-project All context for new-project workflow
158
+ * init new-milestone All context for new-milestone workflow
159
+ * init quick <description> All context for quick workflow
160
+ * init resume All context for resume-project workflow
161
+ * init verify-work <phase> All context for verify-work workflow
162
+ * init phase-op <phase> Generic phase operation context
163
+ * init todos [area] All context for todo workflows
164
+ * init milestone-op All context for milestone operations
165
+ * init map-codebase All context for map-codebase workflow
166
+ * init progress All context for progress workflow
167
+ *
168
+ * Documentation:
169
+ * docs-init Project context for docs-update workflow
170
+ *
171
+ * Learnings:
172
+ * learnings list List all global learnings (JSON)
173
+ * learnings query --tag <tag> Query learnings by tag
174
+ * learnings copy Copy from current project's LEARNINGS.md
175
+ * learnings prune --older-than <dur> Remove entries older than duration (e.g. 90d)
176
+ * learnings delete <id> Delete a learning by ID
177
+ *
178
+ * Loop Extension Point Queries (ADR-857 phase 3c):
179
+ * loop render-hooks <point> Resolve + render active Capability hooks at a loop point
180
+ * Returns JSON envelope { point, activeHooks, rendered }
181
+ * Valid points: discuss:pre/post, plan:pre/post,
182
+ * execute:pre/wave:pre/wave:post/post, verify:pre/post, ship:pre/post
183
+ *
184
+ * Capability State (ADR-857 phase 4b):
185
+ * capability state [--config-dir <path>] Resolve per-capability install/surface/hook-activation state
186
+ * Returns JSON envelope { runtimeConfigDir, capabilities[] }
187
+ * --config-dir: runtime config dir (default: auto-detect current runtime)
188
+ *
189
+ * GSD-2 Migration:
190
+ * from-gsd2 [--path <dir>] [--force] [--dry-run]
191
+ * Import a GSD-2 (.gsd/) project back to GSD v1 (.planning/) format
192
+ */
193
+
194
+ const fs = require('fs');
195
+ const path = require('path');
196
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
197
+ const io = require('./lib/io.cjs');
198
+ const { error, ERROR_REASON, setJsonErrorMode, output } = io;
199
+ const projectRoot = require('./lib/project-root.cjs');
200
+ // Resolve findProjectRoot lazily at call time rather than binding it at module
201
+ // load. It is sourced from project-root.cjs; a call-time lookup is robust
202
+ // against any require/load-ordering edge where the export isn't bound yet
203
+ // when this entrypoint is first required (#604).
204
+ const findProjectRoot = (...args) => projectRoot.findProjectRoot(...args);
205
+ const { getActiveWorkstream } = require('./lib/planning-workspace.cjs');
206
+ const { resolveActiveWorkstream, applyResolvedWorkstreamEnv } = require('./lib/active-workstream-store.cjs');
207
+ const state = require('./lib/state.cjs');
208
+ const phase = require('./lib/phase.cjs');
209
+ const roadmap = require('./lib/roadmap.cjs');
210
+ const verify = require('./lib/verify.cjs');
211
+ const config = require('./lib/config.cjs');
212
+ const template = require('./lib/template.cjs');
213
+ const milestone = require('./lib/milestone.cjs');
214
+ const commands = require('./lib/commands.cjs');
215
+ const init = require('./lib/init.cjs');
216
+ const frontmatter = require('./lib/frontmatter.cjs');
217
+ const workstream = require('./lib/workstream.cjs');
218
+ const docs = require('./lib/docs.cjs');
219
+ const learnings = require('./lib/learnings.cjs');
220
+ const gapChecker = require('./lib/gap-checker.cjs');
221
+ const { routeStateCommand } = require('./lib/state-command-router.cjs');
222
+ const { routeVerifyCommand } = require('./lib/verify-command-router.cjs');
223
+ const { routeVerificationCommand } = require('./lib/verification-command-router.cjs');
224
+ const verification = require('./lib/verification.cjs');
225
+ const { routeInitCommand } = require('./lib/init-command-router.cjs');
226
+ const loopResolver = require('./lib/loop-resolver.cjs');
227
+ const capabilityState = require('./lib/capability-state.cjs');
228
+ const capabilityWriter = require('./lib/capability-writer.cjs');
229
+ const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
230
+ const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
231
+ const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
232
+ const { routeRoadmapCommand } = require('./lib/roadmap-command-router.cjs');
233
+ const { routeAgentCommand } = require('./lib/agent-command-router.cjs');
234
+ const { routeCheckCommand } = require('./lib/check-command-router.cjs');
235
+ const { routeTaskCommand } = require('./lib/task-command-router.cjs');
236
+ const { parseNamedArgs, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
237
+ const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
238
+ const { getEffectiveAuthority, classifyDriftSeverity } = require('./lib/plan-drift-guard.cjs');
239
+
240
+ // ─── Bridge collapsed (Phase 4) ────────────────────────────────────────────────
241
+ // Non-family commands now run through their CJS handlers directly. Keep the
242
+ // helper contract so existing call sites remain unchanged during the phase
243
+ // sequence; it always returns false so callers fall through to CJS.
244
+
245
+ /**
246
+ * Retired bridge-era shim for non-family dispatch.
247
+ *
248
+ * Always returns false so command handlers continue down the CJS path.
249
+ * Kept only to avoid churn while legacy call sites are being deleted.
250
+ *
251
+ * @param {object} opts
252
+ * @param {string} opts.registryCommand - legacy bridge placeholder
253
+ * @param {string[]} opts.registryArgs - legacy bridge placeholder
254
+ * @param {string} opts.legacyCommand - original gsd-tools command name
255
+ * @param {string[]} opts.legacyArgs - original args
256
+ * @param {string} opts.cwd - project dir
257
+ * @param {boolean} opts.raw - raw output mode
258
+ * @param {Function} opts.error - error reporter
259
+ * @param {Function} opts.output - output emitter (output)
260
+ */
261
+ function _dispatchNonFamily({ registryCommand, registryArgs, legacyCommand, legacyArgs, cwd, raw, error, output }) {
262
+ void registryCommand;
263
+ void registryArgs;
264
+ void legacyCommand;
265
+ void legacyArgs;
266
+ void cwd;
267
+ void raw;
268
+ void error;
269
+ void output;
270
+ return false;
271
+ }
272
+
273
+ // ─── ADR-959: Capability Command Dispatch ─────────────────────────────────────
274
+
275
+ /**
276
+ * Dispatch a command via the capability registry's commandFamilies index.
277
+ *
278
+ * Consulted in the `default` case of `runCommand` BEFORE the unknown-command
279
+ * error is emitted. Returns:
280
+ * true — command was "consumed" (found in registry, or a dispatch error was
281
+ * emitted); "Unknown command" is suppressed in all consumed cases.
282
+ * false — command not found in the registry (including prototype-pollution
283
+ * guard hits and missing/empty commandFamilies); caller falls through
284
+ * to the existing unknown-command error path.
285
+ * Behavior-preserving when commandFamilies is empty ({}).
286
+ *
287
+ * Injectable for tests:
288
+ * - `registry` defaults to require('./lib/capability-registry.cjs')
289
+ * - `requireModule` defaults to a confinement-checked loader that resolves the
290
+ * module path relative to bin/lib/ and asserts it stays within that directory
291
+ * before requiring — defense-in-depth against corrupted/hand-edited registry entries.
292
+ *
293
+ * @param {object} opts
294
+ * @param {string} opts.command The command name (top-level gsd-tools command)
295
+ * @param {string[]} opts.args Remaining args passed to the router
296
+ * @param {string} opts.cwd Project working directory
297
+ * @param {boolean} opts.raw Raw output mode flag
298
+ * @param {Function} opts.error Error reporter (io.error)
299
+ * @param {object} [opts.registry] Injectable registry (for tests)
300
+ * @param {Function} [opts.requireModule] Injectable module loader (for tests)
301
+ * @returns {boolean} true if the command was dispatched, false otherwise
302
+ */
303
+ function dispatchCapabilityCommand({ command, args, cwd, raw, error, registry, requireModule }) {
304
+ // Prototype-pollution guard: reject reserved property names as command keys
305
+ if (command === '__proto__' || command === 'constructor' || command === 'prototype') {
306
+ return false;
307
+ }
308
+
309
+ // Resolve defaults (injectable for tests)
310
+ const reg = registry !== undefined ? registry : require('./lib/capability-registry.cjs');
311
+
312
+ // Default requireModule: confined to bin/lib/ — validate the module name is a
313
+ // safe bare .cjs basename (no path separators, no directory traversal), then
314
+ // resolve and assert confinement, then require the RESOLVED absolute path so
315
+ // the checked representation and the required representation are identical.
316
+ const libDir = path.join(__dirname, 'lib');
317
+ const defaultRequireModule = function (m) {
318
+ // Step 1: validate m is a bare .cjs basename — same conservative pattern the
319
+ // generator uses. Rejects any value with path separators (/, \, ..) or
320
+ // missing the .cjs extension before we even touch the filesystem.
321
+ if (typeof m !== 'string' || !/^[A-Za-z0-9._-]+\.cjs$/.test(m)) {
322
+ throw new Error('capability module must be a bare .cjs basename: ' + JSON.stringify(m));
323
+ }
324
+ // Step 2: confinement check — belt-and-suspenders even after the basename
325
+ // validation above. Resolved path must be inside libDir (not equal to it,
326
+ // and must start with libDir + sep so "libDir-suffix" can't sneak through).
327
+ const resolved = path.resolve(libDir, m);
328
+ if (resolved === libDir || !resolved.startsWith(libDir + path.sep)) {
329
+ throw new Error('capability module path escapes bin/lib/: ' + JSON.stringify(m));
330
+ }
331
+ // Step 3: require the resolved absolute path — the SAME representation that
332
+ // was checked above, not the concatenated './lib/' + m string.
333
+ return require(resolved);
334
+ };
335
+ const loadModule = requireModule !== undefined ? requireModule : defaultRequireModule;
336
+
337
+ // Look up the command family in the registry
338
+ const families = reg && reg.commandFamilies;
339
+ if (!families || typeof families !== 'object') return false;
340
+
341
+ const entry = families[command];
342
+ if (!entry || typeof entry !== 'object') return false;
343
+
344
+ // Resolve and call the router
345
+ let mod;
346
+ try {
347
+ mod = loadModule(entry.module);
348
+ } catch (_) {
349
+ // Module not found, load error, or confinement violation — surface a
350
+ // diagnostic and return true (consumed) so "Unknown command" is suppressed.
351
+ error('capability command "' + command + '" module "' + entry.module + '" failed to load');
352
+ return true; // consumed — don't emit "Unknown command"
353
+ }
354
+
355
+ // Own-property guard: prevent invoking inherited prototype methods
356
+ // (constructor, toString, hasOwnProperty, etc.) as a router when the registry
357
+ // entry names one of those. Must come before the typeof check.
358
+ if (!mod || !Object.prototype.hasOwnProperty.call(mod, entry.router)) {
359
+ error('capability command "' + command + '" router "' + entry.router + '" is not an own export of module "' + entry.module + '"');
360
+ return true; // consumed — don't emit "Unknown command"
361
+ }
362
+ const fn = mod[entry.router];
363
+ if (typeof fn !== 'function') {
364
+ // Router export not found — surface a diagnostic and return true (consumed)
365
+ // so "Unknown command" is suppressed.
366
+ error('capability command "' + command + '" router "' + entry.router + '" is not a function in module "' + entry.module + '"');
367
+ return true; // consumed — don't emit "Unknown command"
368
+ }
369
+
370
+ let _result;
371
+ try {
372
+ _result = fn({ args, cwd, raw, error });
373
+ } catch (e) {
374
+ if (e instanceof ExitError) throw e; // intentional structured error from the router (honors --json-errors) — propagate untouched
375
+ error(
376
+ 'capability command "' + command + '" router "' + entry.router + '" in module "' + entry.module + '" threw: ' + (e && e.message ? e.message : String(e)),
377
+ ERROR_REASON.SDK_FAIL_FAST,
378
+ );
379
+ }
380
+ if (_result && typeof _result.then === 'function') {
381
+ error(
382
+ 'capability command "' + command + '" router "' + entry.router + '" in module "' + entry.module + '" must be synchronous (returned a Promise); async capability routers are not supported.',
383
+ ERROR_REASON.SDK_FAIL_FAST,
384
+ );
385
+ }
386
+ return true;
387
+ }
388
+
389
+ // ─── Arg parsing helpers ──────────────────────────────────────────────────────
390
+
391
+ // ─── CLI Router ───────────────────────────────────────────────────────────────
392
+
393
+ async function main() {
394
+ let args = process.argv.slice(2);
395
+
396
+ // --json-errors / GSD_JSON_ERRORS=1: when active, error() emits structured
397
+ // JSON ({ ok: false, reason: <ERROR_REASON code>, message }) to stderr
398
+ // instead of "Error: <text>". Lets test suites assert on typed reason codes
399
+ // per CONTRIBUTING.md "Prohibited: Raw Text Matching" (#2974).
400
+ //
401
+ // Detect early — before any flag parsing that can fire error() — so even
402
+ // --cwd and workstream-resolution failures emit structured stderr (#3310).
403
+ // The argv splice must happen here too, otherwise the dispatcher below sees
404
+ // "--json-errors" as an unknown command. Default off — human operators keep
405
+ // their plain-text diagnostic.
406
+ const jsonErrorsIdx = args.indexOf('--json-errors');
407
+ if (jsonErrorsIdx !== -1) {
408
+ setJsonErrorMode(true);
409
+ args.splice(jsonErrorsIdx, 1);
410
+ } else if (process.env.GSD_JSON_ERRORS === '1') {
411
+ setJsonErrorMode(true);
412
+ }
413
+
414
+ // Optional cwd override for sandboxed subagents running outside project root.
415
+ let cwd = process.cwd();
416
+ const cwdEqArg = args.find(arg => arg.startsWith('--cwd='));
417
+ const cwdIdx = args.indexOf('--cwd');
418
+ if (cwdEqArg) {
419
+ const value = cwdEqArg.slice('--cwd='.length).trim();
420
+ if (!value) error('Missing value for --cwd', ERROR_REASON.USAGE);
421
+ args.splice(args.indexOf(cwdEqArg), 1);
422
+ cwd = path.resolve(value);
423
+ } else if (cwdIdx !== -1) {
424
+ const value = args[cwdIdx + 1];
425
+ if (!value || value.startsWith('--')) error('Missing value for --cwd', ERROR_REASON.USAGE);
426
+ args.splice(cwdIdx, 2);
427
+ cwd = path.resolve(value);
428
+ }
429
+
430
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
431
+ error(`Invalid --cwd: ${cwd}`, ERROR_REASON.USAGE);
432
+ }
433
+
434
+ // Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
435
+ // However, in monorepo worktrees where the subdirectory itself owns .planning/,
436
+ // skip worktree resolution — the CWD is already the correct project root.
437
+ const { resolveWorktreeRoot } = require('./lib/worktree-safety.cjs');
438
+ if (!fs.existsSync(path.join(cwd, '.planning'))) {
439
+ const worktreeRoot = resolveWorktreeRoot(cwd);
440
+ if (worktreeRoot !== cwd) {
441
+ cwd = worktreeRoot;
442
+ }
443
+ }
444
+
445
+ // Optional workstream override for parallel milestone work.
446
+ // Priority: --ws flag > GSD_WORKSTREAM env var > session/shared pointer > null.
447
+ let workstreamContext = null;
448
+ try {
449
+ workstreamContext = resolveActiveWorkstream(cwd, args, process.env, {
450
+ getStored: getActiveWorkstream,
451
+ });
452
+ args = workstreamContext.args;
453
+ // Set env var so all modules (planningDir, planningPaths) auto-resolve workstream paths.
454
+ applyResolvedWorkstreamEnv(workstreamContext, process.env);
455
+ } catch (err) {
456
+ error(err.message || String(err));
457
+ }
458
+
459
+ const rawIndex = args.indexOf('--raw');
460
+ const raw = rawIndex !== -1;
461
+ if (rawIndex !== -1) args.splice(rawIndex, 1);
462
+
463
+ // --pick <name>: extract a single field from JSON output (replaces jq dependency).
464
+ // Supports dot-notation (e.g., --pick workflow.research) and bracket notation
465
+ // for arrays (e.g., --pick directories[-1]).
466
+ const pickIdx = args.indexOf('--pick');
467
+ let pickField = null;
468
+ if (pickIdx !== -1) {
469
+ pickField = args[pickIdx + 1];
470
+ if (!pickField || pickField.startsWith('--')) error('Missing value for --pick', ERROR_REASON.USAGE);
471
+ args.splice(pickIdx, 2);
472
+ }
473
+
474
+ // --default <value>: for config-get, return this value instead of erroring
475
+ // when the key is absent. Allows workflows to express optional config reads
476
+ // without defensive `2>/dev/null || true` boilerplate (#1893).
477
+ const defaultIdx = args.indexOf('--default');
478
+ let defaultValue = undefined;
479
+ if (defaultIdx !== -1) {
480
+ defaultValue = args[defaultIdx + 1];
481
+ if (defaultValue === undefined) defaultValue = '';
482
+ args.splice(defaultIdx, 2);
483
+ }
484
+
485
+ let command = args[0];
486
+
487
+ // Accept `query` as a meta-prefix for canonical dotted/spaced commands.
488
+ // Workflows may call `node gsd-tools.cjs query <command>` directly.
489
+ if (command === 'query') {
490
+ args.shift();
491
+ command = args[0];
492
+ }
493
+
494
+ // #3243: accept dotted canonical form (e.g. `state.update`) as well as the
495
+ // spaced form (`state update`). Some workflow callers pass the dotted
496
+ // canonical form directly; this normalization keeps both forms valid.
497
+ //
498
+ // Split on the FIRST dot only — `check.decision-coverage-plan` becomes
499
+ // command='check', args=['check','decision-coverage-plan',...rest].
500
+ // Guard: head and rest must both be non-empty (rejects leading-dot args like
501
+ // ".hidden" and bare-dot ".").
502
+ const originalCommand = command; // preserved for "Unknown command" suggestion
503
+ if (typeof command === 'string' && command.includes('.')) {
504
+ const dotIdx = command.indexOf('.');
505
+ const head = command.slice(0, dotIdx);
506
+ const rest = command.slice(dotIdx + 1);
507
+ if (head && rest) {
508
+ command = head;
509
+ args = [head, rest, ...args.slice(1)];
510
+ }
511
+ }
512
+
513
+ // Top-level usage string — emitted by `gsd-tools` (no args) and by
514
+ // `gsd-tools --help` / any `--help` request below.
515
+ // CR feedback: the command list must enumerate every top-level command
516
+ // supported by the dispatcher so `--help` is actually useful for
517
+ // discovery; previously it was a partial subset that didn't include
518
+ // phase / roadmap / milestone / progress / etc.
519
+ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
520
+ 'Commands: agent, agent-skills, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, ' +
521
+ 'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, ' +
522
+ 'current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
523
+ 'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
524
+ 'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
525
+ 'capability, classify-confidence, git, learnings, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
526
+ 'profile-sample, progress, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
527
+ 'task, template, user-story, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
528
+ 'Global flags:\n' +
529
+ ' --raw Emit raw output without post-processing\n' +
530
+ ' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
531
+ ' --cwd <path> Override working directory for project-root resolution\n' +
532
+ ' --ws <name> Override active workstream (or set GSD_WORKSTREAM)\n' +
533
+ ' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n\n' +
534
+ 'For command-specific argument requirements, invoke the command without args ' +
535
+ '(e.g. `gsd-tools phase add`) — the resulting error lists what is required.';
536
+
537
+ if (!command) {
538
+ error(TOP_LEVEL_USAGE);
539
+ }
540
+
541
+ // #3019: a `--help` / `-h` flag in argv must render the top-level usage
542
+ // and exit 0 — not error out with "Unknown flag". The previous shape
543
+ // erred on agent-hallucinated flags, but it also blocked humans from
544
+ // discovering the command surface via subcommand help requests routed
545
+ // through this dispatcher. Rendering top-level usage on --help is strictly
546
+ // better UX than the old short-circuit that printed unrelated usage text.
547
+ const HELP_FLAGS = new Set(['-h', '--help', '-?', '--h', '--usage']);
548
+ if (args.some((a) => HELP_FLAGS.has(a))) {
549
+ process.stdout.write(TOP_LEVEL_USAGE + '\n');
550
+ return;
551
+ }
552
+
553
+ // Reject version flags. AI agents sometimes hallucinate --version on tool
554
+ // invocations; silently ignoring it can cause destructive operations to
555
+ // proceed unchecked. (Help flags are handled above.)
556
+ const NEVER_VALID_FLAGS = new Set(['--version', '-v']);
557
+ for (const arg of args) {
558
+ if (NEVER_VALID_FLAGS.has(arg)) {
559
+ error(`Unknown flag: ${arg}\ngsd-tools does not accept version flags. Run "gsd-tools" with no arguments for usage.`, ERROR_REASON.USAGE);
560
+ }
561
+ }
562
+
563
+ // Multi-repo guard: resolve project root for commands that read/write .planning/.
564
+ // Skip for pure-utility commands that don't touch .planning/ to avoid unnecessary
565
+ // filesystem traversal on every invocation.
566
+ // 'loop' and 'capability' are intentionally NOT in SKIP_ROOT_RESOLUTION.
567
+ // Both are registry/config queries that resolve activation via
568
+ // .planning/config.json; they need the project root (cwd) for correct
569
+ // `when` key resolution. If one is ever moved to SKIP_ROOT_RESOLUTION,
570
+ // move the other at the same time (keep them consistent).
571
+ const SKIP_ROOT_RESOLUTION = new Set([
572
+ 'generate-slug', 'current-timestamp', 'verify-path-exists',
573
+ 'verify-summary', 'template', 'frontmatter', 'detect-custom-files',
574
+ 'worktree', 'prompt-budget',
575
+ 'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
576
+ 'user-story', // pure string validation — no .planning/ access needed
577
+ ]);
578
+ if (!SKIP_ROOT_RESOLUTION.has(command)) {
579
+ cwd = findProjectRoot(cwd);
580
+ }
581
+
582
+ // When --pick is active, capture stdout and extract the requested field.
583
+ if (pickField) {
584
+ const captured = await captureStdoutSyncWrites(async () => {
585
+ await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
586
+ });
587
+ const resolved = resolveAtFileOutput(captured);
588
+ try {
589
+ const obj = JSON.parse(resolved);
590
+ const value = extractField(obj, pickField);
591
+ const result = value === null || value === undefined ? '' : String(value);
592
+ fs.writeSync(1, result);
593
+ } catch {
594
+ fs.writeSync(1, captured);
595
+ }
596
+ return;
597
+ }
598
+
599
+ // Intercept stdout to transparently resolve @file: references (#1891).
600
+ // io.cjs output() writes @file:<path> when JSON > 50KB. The --pick path
601
+ // already resolves this, but the normal path wrote @file: to stdout, forcing
602
+ // every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
603
+ // that breaks on PowerShell and other non-bash shells.
604
+ const captured = await captureStdoutSyncWrites(async () => {
605
+ await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
606
+ });
607
+ fs.writeSync(1, resolveAtFileOutput(captured));
608
+ }
609
+
610
+ function captureStdoutSyncWrites(run) {
611
+ const originalWriteSync = fs.writeSync;
612
+ let captured = '';
613
+
614
+ fs.writeSync = function patchedWriteSync(fd, data, ...rest) {
615
+ if (fd === 1) {
616
+ if (Buffer.isBuffer(data)) {
617
+ captured += data.toString('utf-8');
618
+ return data.length;
619
+ }
620
+ const text = String(data);
621
+ captured += text;
622
+ let encoding = 'utf-8';
623
+ if (typeof rest[1] === 'string') encoding = rest[1];
624
+ return Buffer.byteLength(text, encoding);
625
+ }
626
+ return originalWriteSync.call(fs, fd, data, ...rest);
627
+ };
628
+
629
+ const restore = () => {
630
+ fs.writeSync = originalWriteSync;
631
+ };
632
+
633
+ return Promise.resolve()
634
+ .then(() => run())
635
+ .then(() => {
636
+ restore();
637
+ return captured;
638
+ }, (err) => {
639
+ restore();
640
+ throw err;
641
+ });
642
+ }
643
+
644
+ function resolveAtFileOutput(captured) {
645
+ if (!captured.startsWith('@file:')) return captured;
646
+ return fs.readFileSync(captured.slice(6), 'utf-8');
647
+ }
648
+
649
+ /**
650
+ * Extract a field from an object using dot-notation and bracket syntax.
651
+ * Supports: 'field', 'parent.child', 'arr[-1]', 'arr[0]'
652
+ */
653
+ function extractField(obj, fieldPath) {
654
+ const parts = fieldPath.split('.');
655
+ let current = obj;
656
+ for (const part of parts) {
657
+ if (current === null || current === undefined) return undefined;
658
+ const bracketMatch = part.match(/^(.+?)\[(-?\d+)]$/);
659
+ if (bracketMatch) {
660
+ const key = bracketMatch[1];
661
+ const index = parseInt(bracketMatch[2], 10);
662
+ current = current[key];
663
+ if (!Array.isArray(current)) return undefined;
664
+ current = index < 0 ? current[current.length + index] : current[index];
665
+ } else {
666
+ current = current[part];
667
+ }
668
+ }
669
+ return current;
670
+ }
671
+
672
+ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) {
673
+ switch (command) {
674
+ case 'agent': {
675
+ routeAgentCommand({ args, raw });
676
+ break;
677
+ }
678
+
679
+ case 'check': {
680
+ routeCheckCommand({ args, cwd, raw });
681
+ break;
682
+ }
683
+
684
+ case 'state': {
685
+ routeStateCommand({
686
+ state,
687
+ args,
688
+ cwd,
689
+ raw,
690
+ error,
691
+ });
692
+ break;
693
+ }
694
+
695
+ case 'resolve-model': {
696
+ commands.cmdResolveModel(cwd, args[1], raw);
697
+ break;
698
+ }
699
+
700
+ case 'resolve-granularity': {
701
+ // Parse optional --granularity <val> flag (space form only); positional is phase-type.
702
+ // The =form (--granularity=<val>) is intentionally not supported: parseNamedArgs and
703
+ // the /gsd:plan-phase + init plan-phase paths accept only the space form, so supporting
704
+ // = here alone would create an inconsistency (#703).
705
+ const granArgs = args.slice(1);
706
+ let granOverride;
707
+ const granPositionals = [];
708
+ for (let i = 0; i < granArgs.length; i++) {
709
+ const a = granArgs[i];
710
+ if (a === '--granularity' && granArgs[i + 1] !== undefined && !granArgs[i + 1].startsWith('--')) {
711
+ if (granOverride === undefined) { granOverride = granArgs[++i]; } else { ++i; }
712
+ } else {
713
+ granPositionals.push(a);
714
+ }
715
+ }
716
+ commands.cmdResolveGranularity(cwd, granPositionals[0], raw, granOverride);
717
+ break;
718
+ }
719
+
720
+ case 'resolve-execution': {
721
+ // Deterministic flag parsing: consume --flag <value> pairs first,
722
+ // then the AGENT is the single remaining positional.
723
+ // Supports both orderings: <agent> --flag val AND --flag val <agent>.
724
+ // Also supports --flag=value form (same convention as --cwd= above).
725
+ const execArgs = args.slice(1);
726
+ let effortOverride;
727
+ let fastModeOverride;
728
+ let attempt;
729
+ const positionals = [];
730
+ for (let i = 0; i < execArgs.length; i++) {
731
+ const a = execArgs[i];
732
+ // --effort=<val> form
733
+ if (a.startsWith('--effort=')) {
734
+ effortOverride = a.slice('--effort='.length);
735
+ continue;
736
+ }
737
+ // --fast-mode=<val> form
738
+ if (a.startsWith('--fast-mode=')) {
739
+ const v = a.slice('--fast-mode='.length);
740
+ fastModeOverride = v === 'true' ? true : v === 'false' ? false : undefined;
741
+ continue;
742
+ }
743
+ // --attempt=<val> form
744
+ if (a.startsWith('--attempt=')) {
745
+ const v = a.slice('--attempt='.length);
746
+ const n = parseInt(v, 10);
747
+ if (!Number.isInteger(n) || n < 0) error('--attempt requires a non-negative integer', ERROR_REASON.USAGE);
748
+ attempt = n;
749
+ continue;
750
+ }
751
+ // --effort <val>
752
+ if (a === '--effort') {
753
+ const val = execArgs[i + 1];
754
+ if (val === undefined || val.startsWith('--')) error('Missing value for --effort', ERROR_REASON.USAGE);
755
+ effortOverride = val;
756
+ i++;
757
+ continue;
758
+ }
759
+ // --fast-mode <val>
760
+ if (a === '--fast-mode') {
761
+ const val = execArgs[i + 1];
762
+ if (val === undefined || val.startsWith('--')) error('Missing value for --fast-mode', ERROR_REASON.USAGE);
763
+ fastModeOverride = val === 'true' ? true : val === 'false' ? false : undefined;
764
+ i++;
765
+ continue;
766
+ }
767
+ // --attempt <val>
768
+ if (a === '--attempt') {
769
+ const val = execArgs[i + 1];
770
+ if (val === undefined || val.startsWith('--')) error('Missing value for --attempt', ERROR_REASON.USAGE);
771
+ const n = parseInt(val, 10);
772
+ if (!Number.isInteger(n) || n < 0) error('--attempt requires a non-negative integer', ERROR_REASON.USAGE);
773
+ attempt = n;
774
+ i++;
775
+ continue;
776
+ }
777
+ // --raw is handled by top-level arg processing; skip it here
778
+ if (a === '--raw') continue;
779
+ // Unknown flag
780
+ if (a.startsWith('-')) error(`Unknown flag for resolve-execution: ${a}`, ERROR_REASON.USAGE);
781
+ // Positional
782
+ positionals.push(a);
783
+ }
784
+ if (positionals.length === 0) error('agent-type required', ERROR_REASON.USAGE);
785
+ if (positionals.length > 1) error(`resolve-execution requires exactly one agent-type argument; got: ${positionals.join(', ')}`, ERROR_REASON.USAGE);
786
+ const agentTypeArg = positionals[0];
787
+ commands.cmdResolveExecution(cwd, agentTypeArg, raw, {
788
+ effortOverride,
789
+ fastModeOverride,
790
+ attempt,
791
+ });
792
+ break;
793
+ }
794
+
795
+ case 'find-phase': {
796
+ // Phase 6 (#3575): dispatch via SDK executeForCjs when available.
797
+ // SDK handler: findPhase in sdk/src/query/phase.ts.
798
+ const handled = _dispatchNonFamily({
799
+ registryCommand: 'find-phase',
800
+ registryArgs: args.slice(1),
801
+ legacyCommand: 'find-phase',
802
+ legacyArgs: args.slice(1),
803
+ cwd,
804
+ raw,
805
+ error,
806
+ output: output,
807
+ });
808
+ if (!handled) phase.cmdFindPhase(cwd, args[1], raw);
809
+ break;
810
+ }
811
+
812
+ case 'commit': {
813
+ const amend = args.includes('--amend');
814
+ const noVerify = args.includes('--no-verify');
815
+ const filesIndex = args.indexOf('--files');
816
+ // Collect all positional args between command name and first flag,
817
+ // then join them — handles both quoted ("multi word msg") and
818
+ // unquoted (multi word msg) invocations from different shells
819
+ const endIndex = filesIndex !== -1 ? filesIndex : args.length;
820
+ const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
821
+ const message = messageArgs.join(' ') || undefined;
822
+ const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
823
+ commands.cmdCommit(cwd, message, files, raw, amend, noVerify);
824
+ break;
825
+ }
826
+
827
+ case 'check-commit': {
828
+ commands.cmdCheckCommit(cwd, raw);
829
+ break;
830
+ }
831
+
832
+ case 'commit-to-subrepo': {
833
+ const message = args[1];
834
+ const filesIndex = args.indexOf('--files');
835
+ const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
836
+ commands.cmdCommitToSubrepo(cwd, message, files, raw);
837
+ break;
838
+ }
839
+
840
+ case 'verify-summary': {
841
+ const summaryPath = args[1];
842
+ const countIndex = args.indexOf('--check-count');
843
+ const checkCount = countIndex !== -1 ? parseInt(args[countIndex + 1], 10) : 2;
844
+ verify.cmdVerifySummary(cwd, summaryPath, checkCount, raw);
845
+ break;
846
+ }
847
+
848
+ case 'template': {
849
+ const subcommand = args[1];
850
+ if (subcommand === 'select') {
851
+ template.cmdTemplateSelect(cwd, args[2], raw);
852
+ } else if (subcommand === 'fill') {
853
+ const templateType = args[2];
854
+ const { phase, plan, name, type, wave, fields: fieldsRaw } = parseNamedArgs(args, ['phase', 'plan', 'name', 'type', 'wave', 'fields']);
855
+ let fields = {};
856
+ if (fieldsRaw) {
857
+ const { safeJsonParse } = require('./lib/security.cjs');
858
+ const result = safeJsonParse(fieldsRaw, { label: '--fields' });
859
+ if (!result.ok) error(result.error);
860
+ fields = result.value;
861
+ }
862
+ template.cmdTemplateFill(cwd, templateType, {
863
+ phase, plan, name, fields,
864
+ type: type || 'execute',
865
+ wave: wave || '1',
866
+ }, raw);
867
+ } else {
868
+ error('Unknown template subcommand. Available: select, fill', ERROR_REASON.SDK_UNKNOWN_COMMAND);
869
+ }
870
+ break;
871
+ }
872
+
873
+ case 'task': {
874
+ routeTaskCommand({ args, cwd, raw });
875
+ break;
876
+ }
877
+
878
+ case 'frontmatter': {
879
+ // Phase 6 (#3575): dispatch via SDK executeForCjs when available.
880
+ // SDK handler: sdk/src/query/frontmatter.ts + frontmatter-mutation.ts.
881
+ // CJS fallback: frontmatter.cjs (cooperating sibling).
882
+ const subcommand = args[1];
883
+ const file = args[2];
884
+ const FRONTMATTER_SDK_MAP = {
885
+ get: 'frontmatter.get',
886
+ set: 'frontmatter.set',
887
+ merge: 'frontmatter.merge',
888
+ validate: 'frontmatter.validate',
889
+ };
890
+ if (subcommand in FRONTMATTER_SDK_MAP) {
891
+ const handled = _dispatchNonFamily({
892
+ registryCommand: FRONTMATTER_SDK_MAP[subcommand],
893
+ registryArgs: args.slice(2),
894
+ legacyCommand: 'frontmatter',
895
+ legacyArgs: args.slice(1),
896
+ cwd,
897
+ raw,
898
+ error,
899
+ output: output,
900
+ });
901
+ if (handled) break;
902
+ }
903
+ // CJS fallback (SDK unavailable or unknown subcommand)
904
+ if (subcommand === 'get') {
905
+ frontmatter.cmdFrontmatterGet(cwd, file, parseNamedArgs(args, ['field']).field, raw);
906
+ } else if (subcommand === 'set') {
907
+ const { field, value } = parseNamedArgs(args, ['field', 'value']);
908
+ frontmatter.cmdFrontmatterSet(cwd, file, field, value !== null ? value : undefined, raw);
909
+ } else if (subcommand === 'merge') {
910
+ frontmatter.cmdFrontmatterMerge(cwd, file, parseNamedArgs(args, ['data']).data, raw);
911
+ } else if (subcommand === 'validate') {
912
+ frontmatter.cmdFrontmatterValidate(cwd, file, parseNamedArgs(args, ['schema']).schema, raw);
913
+ } else {
914
+ error('Unknown frontmatter subcommand. Available: get, set, merge, validate', ERROR_REASON.SDK_UNKNOWN_COMMAND);
915
+ }
916
+ break;
917
+ }
918
+
919
+ case 'verify': {
920
+ routeVerifyCommand({
921
+ verify,
922
+ args,
923
+ cwd,
924
+ raw,
925
+ error,
926
+ });
927
+ break;
928
+ }
929
+
930
+ // ─── Verification Status ───────────────────────────────────────────────
931
+ //
932
+ // verification status <phaseDir>
933
+ // Read the first *-VERIFICATION.md in phaseDir and return
934
+ // { status, next_action, next_command } routing result.
935
+ //
936
+ // Note: `verification` (reads verifier-emitted status) is distinct from
937
+ // `verify` (runs verification checks like plan-structure/artifacts).
938
+
939
+ case 'verification': {
940
+ routeVerificationCommand({
941
+ verification,
942
+ args,
943
+ cwd,
944
+ raw,
945
+ error,
946
+ });
947
+ break;
948
+ }
949
+
950
+ case 'generate-slug': {
951
+ // Phase 6 (#3575): dispatch via SDK executeForCjs when available.
952
+ // SDK handler: generateSlug in sdk/src/query/utils.ts.
953
+ const handled = _dispatchNonFamily({
954
+ registryCommand: 'generate-slug',
955
+ registryArgs: args.slice(1),
956
+ legacyCommand: 'generate-slug',
957
+ legacyArgs: args.slice(1),
958
+ cwd,
959
+ raw,
960
+ error,
961
+ output: output,
962
+ });
963
+ if (!handled) commands.cmdGenerateSlug(args[1], raw);
964
+ break;
965
+ }
966
+
967
+ case 'current-timestamp': {
968
+ // Keep this command on the CJS fast path.
969
+ // Rationale: it is a pure local formatter and avoids SDK bridge startup
970
+ // in tight subprocess loops where Windows CI has shown intermittent
971
+ // native crashes (0xC0000005 / 3221225477).
972
+ commands.cmdCurrentTimestamp(args[1] || 'full', raw);
973
+ break;
974
+ }
975
+
976
+ case 'list-todos': {
977
+ commands.cmdListTodos(cwd, args[1], raw);
978
+ break;
979
+ }
980
+
981
+ case 'verify-path-exists': {
982
+ commands.cmdVerifyPathExists(cwd, args[1], raw);
983
+ break;
984
+ }
985
+
986
+ case 'config-ensure-section': {
987
+ // Phase 6 (#3575): dispatch via SDK executeForCjs. The catalog rebinds
988
+ // 'config-ensure-section' to configNewProject in
989
+ // sdk/src/query/command-static-catalog-foundation.ts, restoring the
990
+ // legacy "no-arg full default init" contract on the SDK path
991
+ // (configEnsureSection itself stays available as an unbound single-
992
+ // section helper for future SDK callers).
993
+ const handled = _dispatchNonFamily({
994
+ registryCommand: 'config-ensure-section',
995
+ registryArgs: args.slice(1),
996
+ legacyCommand: 'config-ensure-section',
997
+ legacyArgs: args.slice(1),
998
+ cwd,
999
+ raw,
1000
+ error,
1001
+ output: output,
1002
+ });
1003
+ if (!handled) config.cmdConfigEnsureSection(cwd, raw);
1004
+ break;
1005
+ }
1006
+
1007
+ case 'config-set': {
1008
+ // Phase 6 (#3575): dispatch via SDK executeForCjs when available.
1009
+ const handled = _dispatchNonFamily({
1010
+ registryCommand: 'config-set',
1011
+ registryArgs: args.slice(1),
1012
+ legacyCommand: 'config-set',
1013
+ legacyArgs: args.slice(1),
1014
+ cwd,
1015
+ raw,
1016
+ error,
1017
+ output: output,
1018
+ });
1019
+ if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw);
1020
+ break;
1021
+ }
1022
+
1023
+ case "config-set-model-profile": {
1024
+ // Phase 6 (#3575): dispatch via SDK executeForCjs when available.
1025
+ const handled = _dispatchNonFamily({
1026
+ registryCommand: 'config-set-model-profile',
1027
+ registryArgs: args.slice(1),
1028
+ legacyCommand: 'config-set-model-profile',
1029
+ legacyArgs: args.slice(1),
1030
+ cwd,
1031
+ raw,
1032
+ error,
1033
+ output: output,
1034
+ });
1035
+ if (!handled) config.cmdConfigSetModelProfile(cwd, args[1], raw);
1036
+ break;
1037
+ }
1038
+
1039
+ case 'config-get': {
1040
+ // Phase 6 (#3575): dispatch via SDK executeForCjs when available.
1041
+ // The SDK handler supports --default via the registry args (args.slice(1)
1042
+ // contains the key; defaultValue is handled by the SDK via the --default
1043
+ // flag which was already stripped from args and held in defaultValue).
1044
+ // Pass the full original args.slice(1) so the SDK sees the key; the
1045
+ // defaultValue from the flag is in the global defaultValue variable above.
1046
+ // Since the SDK handler reads --default from registryArgs, re-inject it.
1047
+ const configGetSdkArgs = defaultValue !== undefined
1048
+ ? [args[1], '--default', defaultValue]
1049
+ : args.slice(1);
1050
+ const handled = _dispatchNonFamily({
1051
+ registryCommand: 'config-get',
1052
+ registryArgs: configGetSdkArgs,
1053
+ legacyCommand: 'config-get',
1054
+ legacyArgs: args.slice(1),
1055
+ cwd,
1056
+ raw,
1057
+ error,
1058
+ output: output,
1059
+ });
1060
+ if (!handled) config.cmdConfigGet(cwd, args[1], raw, defaultValue);
1061
+ break;
1062
+ }
1063
+
1064
+ case 'config-new-project': {
1065
+ // Phase 6 (#3575): dispatch via SDK executeForCjs when available.
1066
+ const handled = _dispatchNonFamily({
1067
+ registryCommand: 'config-new-project',
1068
+ registryArgs: args.slice(1),
1069
+ legacyCommand: 'config-new-project',
1070
+ legacyArgs: args.slice(1),
1071
+ cwd,
1072
+ raw,
1073
+ error,
1074
+ output: output,
1075
+ });
1076
+ if (!handled) config.cmdConfigNewProject(cwd, args[1], raw);
1077
+ break;
1078
+ }
1079
+
1080
+ case 'config-path': {
1081
+ // CJS-native: config-path returns the filesystem path to config.json.
1082
+ // The SDK handler (configPath) also exists but requires a projectDir that
1083
+ // is already resolved. Both produce identical output; keeping CJS here is
1084
+ // simpler and avoids sync-bridge overhead for a trivial path lookup.
1085
+ config.cmdConfigPath(cwd, raw, workstreamContext);
1086
+ break;
1087
+ }
1088
+
1089
+ case 'migrate-config': {
1090
+ // CJS-native: migrate-config wraps the Configuration Module migrateOnDisk()
1091
+ // which is async and mutates the filesystem. No SDK counterpart exists in
1092
+ // the command registry (it's a one-shot migration utility). Must await.
1093
+ await config.cmdMigrateConfig(cwd, raw);
1094
+ break;
1095
+ }
1096
+
1097
+ case 'agent-skills': {
1098
+ // --json emits typed IR { agent_type, block, skills_count } for test assertions
1099
+ // (#455). Default (no flag) outputs raw XML so workflow shell expansions work.
1100
+ const jsonIdx = args.indexOf('--json');
1101
+ const agentSkillsJsonMode = jsonIdx !== -1;
1102
+ if (agentSkillsJsonMode) args.splice(jsonIdx, 1);
1103
+ init.cmdAgentSkills(cwd, args[1], raw, agentSkillsJsonMode);
1104
+ break;
1105
+ }
1106
+
1107
+ case 'skill-manifest': {
1108
+ init.cmdSkillManifest(cwd, args, raw);
1109
+ break;
1110
+ }
1111
+
1112
+ case 'history-digest': {
1113
+ commands.cmdHistoryDigest(cwd, raw);
1114
+ break;
1115
+ }
1116
+
1117
+ case 'phases': {
1118
+ routePhasesCommand({
1119
+ phase,
1120
+ milestone,
1121
+ args,
1122
+ cwd,
1123
+ raw,
1124
+ error,
1125
+ });
1126
+ break;
1127
+ }
1128
+
1129
+ case 'roadmap': {
1130
+ routeRoadmapCommand({
1131
+ roadmap,
1132
+ args,
1133
+ cwd,
1134
+ raw,
1135
+ error,
1136
+ });
1137
+ break;
1138
+ }
1139
+
1140
+ case 'requirements': {
1141
+ const subcommand = args[1];
1142
+ if (subcommand === 'mark-complete') {
1143
+ milestone.cmdRequirementsMarkComplete(cwd, args.slice(2), raw);
1144
+ } else {
1145
+ error('Unknown requirements subcommand. Available: mark-complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1146
+ }
1147
+ break;
1148
+ }
1149
+
1150
+ case 'gap-analysis': {
1151
+ // Post-planning gap checker (#2493) — unified REQUIREMENTS.md +
1152
+ // CONTEXT.md <decisions> coverage report against PLAN.md files.
1153
+ gapChecker.cmdGapAnalysis(cwd, args.slice(1), raw);
1154
+ break;
1155
+ }
1156
+
1157
+ case 'phase': {
1158
+ routePhaseCommand({
1159
+ phase,
1160
+ args,
1161
+ cwd,
1162
+ raw,
1163
+ error,
1164
+ });
1165
+ break;
1166
+ }
1167
+
1168
+ case 'milestone': {
1169
+ const subcommand = args[1];
1170
+ if (subcommand === 'complete') {
1171
+ const milestoneName = parseMultiwordArg(args, 'name');
1172
+ const archivePhases = args.includes('--archive-phases');
1173
+ const force = args.includes('--force');
1174
+ milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force }, raw);
1175
+ } else {
1176
+ error('Unknown milestone subcommand. Available: complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1177
+ }
1178
+ break;
1179
+ }
1180
+
1181
+ case 'validate': {
1182
+ routeValidateCommand({
1183
+ verify,
1184
+ args,
1185
+ cwd,
1186
+ raw,
1187
+ output: output,
1188
+ error,
1189
+ });
1190
+ break;
1191
+ }
1192
+
1193
+ case 'progress': {
1194
+ const subcommand = args[1] || 'json';
1195
+ commands.cmdProgressRender(cwd, subcommand, raw);
1196
+ break;
1197
+ }
1198
+
1199
+ case 'uat': {
1200
+ const subcommand = args[1];
1201
+ const uat = require('./lib/uat.cjs');
1202
+ if (subcommand === 'render-checkpoint') {
1203
+ const options = parseNamedArgs(args, ['file']);
1204
+ uat.cmdRenderCheckpoint(cwd, options, raw);
1205
+ } else {
1206
+ error('Unknown uat subcommand. Available: render-checkpoint', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1207
+ }
1208
+ break;
1209
+ }
1210
+
1211
+ case 'stats': {
1212
+ const subcommand = args[1] || 'json';
1213
+ commands.cmdStats(cwd, subcommand, raw);
1214
+ break;
1215
+ }
1216
+
1217
+ case 'todo': {
1218
+ const subcommand = args[1];
1219
+ if (subcommand === 'complete') {
1220
+ commands.cmdTodoComplete(cwd, args[2], raw);
1221
+ } else if (subcommand === 'match-phase') {
1222
+ commands.cmdTodoMatchPhase(cwd, args[2], raw);
1223
+ } else {
1224
+ error('Unknown todo subcommand. Available: complete, match-phase', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1225
+ }
1226
+ break;
1227
+ }
1228
+
1229
+ case 'scaffold': {
1230
+ const scaffoldType = args[1];
1231
+ const scaffoldOptions = {
1232
+ phase: parseNamedArgs(args, ['phase']).phase,
1233
+ name: parseMultiwordArg(args, 'name'),
1234
+ };
1235
+ commands.cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
1236
+ break;
1237
+ }
1238
+
1239
+ case 'init': {
1240
+ routeInitCommand({
1241
+ init,
1242
+ args,
1243
+ cwd,
1244
+ raw,
1245
+ error,
1246
+ });
1247
+ break;
1248
+ }
1249
+
1250
+ case 'loop': {
1251
+ // loop render-hooks <point>
1252
+ const loopSubcommand = args[1];
1253
+ if (loopSubcommand === 'render-hooks') {
1254
+ let loopConfigDir = null;
1255
+ const configDirEqArg = args.find(arg => arg.startsWith('--config-dir='));
1256
+ const configDirIdx = args.indexOf('--config-dir');
1257
+ if (configDirEqArg) {
1258
+ const value = configDirEqArg.slice('--config-dir='.length).trim();
1259
+ if (!value) error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1260
+ loopConfigDir = value;
1261
+ } else if (configDirIdx !== -1) {
1262
+ const value = args[configDirIdx + 1];
1263
+ if (!value || value.startsWith('--')) {
1264
+ error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1265
+ }
1266
+ loopConfigDir = value;
1267
+ }
1268
+ // --active-cap <capId>: parse and validate before delegating
1269
+ let loopActiveCap = undefined;
1270
+ const activeCapEqArg = args.find(arg => arg.startsWith('--active-cap='));
1271
+ const activeCapIdx = args.indexOf('--active-cap');
1272
+ if (activeCapEqArg) {
1273
+ const value = activeCapEqArg.slice('--active-cap='.length).trim();
1274
+ if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1275
+ loopActiveCap = value;
1276
+ } else if (activeCapIdx !== -1) {
1277
+ const value = args[activeCapIdx + 1];
1278
+ if (!value || value.startsWith('--')) {
1279
+ error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1280
+ }
1281
+ loopActiveCap = value;
1282
+ }
1283
+ loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {
1284
+ configDir: loopConfigDir ? path.resolve(loopConfigDir) : undefined,
1285
+ activeCap: loopActiveCap,
1286
+ });
1287
+ } else {
1288
+ error(
1289
+ `Unknown loop subcommand: ${loopSubcommand}. Available: render-hooks`,
1290
+ ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
1291
+ );
1292
+ }
1293
+ break;
1294
+ }
1295
+
1296
+ case 'capability': {
1297
+ // capability state [--config-dir <path>]
1298
+ // Root resolution: 'capability' is NOT in SKIP_ROOT_RESOLUTION for the
1299
+ // same reason 'loop' is not: both are registry/config queries that need
1300
+ // the project root (cwd) for .planning/config.json activation resolution.
1301
+ // If 'loop' were ever added to SKIP_ROOT_RESOLUTION, 'capability' should
1302
+ // be added at the same time to keep them consistent.
1303
+ const capSubcommand = args[1];
1304
+ if (capSubcommand === 'state') {
1305
+ const configDirIdx = args.indexOf('--config-dir');
1306
+ let configDir = null;
1307
+ if (configDirIdx !== -1) {
1308
+ const configDirVal = args[configDirIdx + 1];
1309
+ // Validate that --config-dir has a following non-flag value.
1310
+ if (!configDirVal || configDirVal.startsWith('--')) {
1311
+ error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1312
+ }
1313
+ configDir = configDirVal;
1314
+ }
1315
+ const resolvedConfigDir = configDir ? path.resolve(configDir) : null;
1316
+ capabilityState.cmdCapabilityState(cwd, resolvedConfigDir, raw, {});
1317
+ } else if (capSubcommand === 'set') {
1318
+ // capability set <id> [--on|--off|--enable|--disable] [--gate <key>=<bool>]... [--config-dir <dir>] [--runtime <r>] [--scope <s>]
1319
+ const capId = args[2];
1320
+ if (!capId || capId.startsWith('--')) {
1321
+ error('Missing capability id for: capability set <id>', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1322
+ }
1323
+ // Parse --config-dir
1324
+ const setConfigDirIdx = args.indexOf('--config-dir');
1325
+ let setConfigDir = null;
1326
+ if (setConfigDirIdx !== -1) {
1327
+ const setConfigDirVal = args[setConfigDirIdx + 1];
1328
+ if (!setConfigDirVal || setConfigDirVal.startsWith('--')) {
1329
+ error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1330
+ }
1331
+ setConfigDir = setConfigDirVal;
1332
+ }
1333
+ const resolvedSetConfigDir = setConfigDir ? path.resolve(setConfigDir) : null;
1334
+ // Parse --on/--enable and --off/--disable (mutually exclusive)
1335
+ const hasOn = args.includes('--on') || args.includes('--enable');
1336
+ const hasOff = args.includes('--off') || args.includes('--disable');
1337
+ if (hasOn && hasOff) {
1338
+ error('Conflicting flags: --on/--enable and --off/--disable cannot both be present', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1339
+ }
1340
+ let setEnabled;
1341
+ if (hasOn) {
1342
+ setEnabled = true;
1343
+ } else if (hasOff) {
1344
+ setEnabled = false;
1345
+ }
1346
+ // Parse --gate <key>=<bool> (repeatable)
1347
+ const setGates = {};
1348
+ for (let gi = 0; gi < args.length; gi++) {
1349
+ if (args[gi] === '--gate') {
1350
+ const gateVal = args[gi + 1];
1351
+ if (!gateVal || gateVal.startsWith('--')) {
1352
+ error('Missing value for --gate (expected <key>=<true|false>)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1353
+ }
1354
+ const eqIdx = gateVal.indexOf('=');
1355
+ if (eqIdx === -1) {
1356
+ error(`Malformed --gate value "${gateVal}": expected <key>=<true|false>`, ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1357
+ }
1358
+ const gateKey = gateVal.slice(0, eqIdx);
1359
+ const gateBoolStr = gateVal.slice(eqIdx + 1);
1360
+ if (gateBoolStr !== 'true' && gateBoolStr !== 'false') {
1361
+ error(`Malformed --gate value "${gateVal}": bool must be true or false`, ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1362
+ }
1363
+ setGates[gateKey] = gateBoolStr === 'true';
1364
+ gi++; // skip consumed value
1365
+ }
1366
+ }
1367
+ // Parse --runtime and --scope (validate that values are present and not flags)
1368
+ const runtimeIdx = args.indexOf('--runtime');
1369
+ let setRuntime;
1370
+ if (runtimeIdx !== -1) {
1371
+ const runtimeVal = args[runtimeIdx + 1];
1372
+ if (!runtimeVal || runtimeVal.startsWith('--')) {
1373
+ error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1374
+ }
1375
+ setRuntime = runtimeVal;
1376
+ }
1377
+ const scopeIdx = args.indexOf('--scope');
1378
+ let setScope;
1379
+ if (scopeIdx !== -1) {
1380
+ const scopeVal = args[scopeIdx + 1];
1381
+ if (!scopeVal || scopeVal.startsWith('--')) {
1382
+ error('Missing value for --scope', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1383
+ }
1384
+ setScope = scopeVal;
1385
+ }
1386
+ capabilityWriter.cmdCapabilitySet(
1387
+ cwd,
1388
+ resolvedSetConfigDir,
1389
+ capId,
1390
+ { enabled: setEnabled, gates: Object.keys(setGates).length > 0 ? setGates : undefined, runtime: setRuntime, scope: setScope },
1391
+ raw,
1392
+ );
1393
+ } else {
1394
+ error(
1395
+ `Unknown capability subcommand: ${capSubcommand}. Available: state, set`,
1396
+ ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
1397
+ );
1398
+ }
1399
+ break;
1400
+ }
1401
+
1402
+ case 'phase-plan-index': {
1403
+ phase.cmdPhasePlanIndex(cwd, args[1], raw);
1404
+ break;
1405
+ }
1406
+
1407
+ case 'state-snapshot': {
1408
+ state.cmdStateSnapshot(cwd, raw);
1409
+ break;
1410
+ }
1411
+
1412
+ case 'summary-extract': {
1413
+ const summaryPath = args[1];
1414
+ const fieldsIndex = args.indexOf('--fields');
1415
+ const fields = fieldsIndex !== -1 ? args[fieldsIndex + 1].split(',') : null;
1416
+ commands.cmdSummaryExtract(cwd, summaryPath, fields, raw);
1417
+ break;
1418
+ }
1419
+
1420
+ case 'websearch': {
1421
+ const query = args[1];
1422
+ const limitIdx = args.indexOf('--limit');
1423
+ const freshnessIdx = args.indexOf('--freshness');
1424
+ await commands.cmdWebsearch(query, {
1425
+ limit: limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 10,
1426
+ freshness: freshnessIdx !== -1 ? args[freshnessIdx + 1] : null,
1427
+ }, raw);
1428
+ break;
1429
+ }
1430
+
1431
+ case 'workstream': {
1432
+ const subcommand = args[1];
1433
+ if (subcommand === 'create') {
1434
+ const migrateNameIdx = args.indexOf('--migrate-name');
1435
+ const noMigrate = args.includes('--no-migrate');
1436
+ workstream.cmdWorkstreamCreate(cwd, args[2], {
1437
+ migrate: !noMigrate,
1438
+ migrateName: migrateNameIdx !== -1 ? args[migrateNameIdx + 1] : null,
1439
+ }, raw);
1440
+ } else if (subcommand === 'list') {
1441
+ workstream.cmdWorkstreamList(cwd, raw);
1442
+ } else if (subcommand === 'status') {
1443
+ workstream.cmdWorkstreamStatus(cwd, args[2], raw);
1444
+ } else if (subcommand === 'complete') {
1445
+ workstream.cmdWorkstreamComplete(cwd, args[2], {}, raw);
1446
+ } else if (subcommand === 'set') {
1447
+ workstream.cmdWorkstreamSet(cwd, args[2], raw);
1448
+ } else if (subcommand === 'get') {
1449
+ workstream.cmdWorkstreamGet(cwd, raw);
1450
+ } else if (subcommand === 'progress') {
1451
+ workstream.cmdWorkstreamProgress(cwd, raw);
1452
+ } else {
1453
+ error('Unknown workstream subcommand. Available: create, list, status, complete, set, get, progress', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1454
+ }
1455
+ break;
1456
+ }
1457
+
1458
+ case 'worktree': {
1459
+ const subcommand = args[1];
1460
+ const worktreeSafety = require('./lib/worktree-safety.cjs');
1461
+ if (subcommand === 'cleanup-wave') {
1462
+ worktreeSafety.cmdWorktreeCleanupWave(cwd, args.slice(2));
1463
+ } else if (subcommand === 'reap-orphans') {
1464
+ worktreeSafety.cmdWorktreeReapOrphans(cwd);
1465
+ } else if (subcommand === 'base-check') {
1466
+ require('./lib/worktree-base-ref.cjs').cmdWorktreeBaseCheck(cwd, args.slice(2));
1467
+ } else if (subcommand === 'set-baseref') {
1468
+ require('./lib/worktree-base-ref.cjs').cmdWorktreeSetBaseRef(cwd, args.slice(2));
1469
+ } else {
1470
+ error('Unknown worktree subcommand. Available: cleanup-wave, reap-orphans, base-check, set-baseref', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1471
+ }
1472
+ break;
1473
+ }
1474
+
1475
+ // ─── Documentation ────────────────────────────────────────────────────
1476
+
1477
+ case 'docs-init': {
1478
+ // Phase 6 (#3575): dispatch via SDK executeForCjs when available.
1479
+ // SDK handler: docsInit in sdk/src/query/docs-init.ts.
1480
+ const handled = _dispatchNonFamily({
1481
+ registryCommand: 'docs-init',
1482
+ registryArgs: args.slice(1),
1483
+ legacyCommand: 'docs-init',
1484
+ legacyArgs: args.slice(1),
1485
+ cwd,
1486
+ raw,
1487
+ error,
1488
+ output: output,
1489
+ });
1490
+ if (!handled) docs.cmdDocsInit(cwd, raw);
1491
+ break;
1492
+ }
1493
+
1494
+ // ─── Learnings ─────────────────────────────────────────────────────────
1495
+
1496
+ case 'learnings': {
1497
+ const subcommand = args[1];
1498
+ if (subcommand === 'list') {
1499
+ learnings.cmdLearningsList(raw);
1500
+ } else if (subcommand === 'query') {
1501
+ const tagIdx = args.indexOf('--tag');
1502
+ const tag = tagIdx !== -1 ? args[tagIdx + 1] : null;
1503
+ if (!tag) error('Usage: gsd-tools learnings query --tag <tag>', ERROR_REASON.USAGE);
1504
+ learnings.cmdLearningsQuery(tag, raw);
1505
+ } else if (subcommand === 'copy') {
1506
+ learnings.cmdLearningsCopy(cwd, raw);
1507
+ } else if (subcommand === 'prune') {
1508
+ const olderIdx = args.indexOf('--older-than');
1509
+ const olderThan = olderIdx !== -1 ? args[olderIdx + 1] : null;
1510
+ if (!olderThan) error('Usage: gsd-tools learnings prune --older-than <duration>', ERROR_REASON.USAGE);
1511
+ learnings.cmdLearningsPrune(olderThan, raw);
1512
+ } else if (subcommand === 'delete') {
1513
+ const id = args[2];
1514
+ if (!id) error('Usage: gsd-tools learnings delete <id>', ERROR_REASON.USAGE);
1515
+ learnings.cmdLearningsDelete(id, raw);
1516
+ } else {
1517
+ error('Unknown learnings subcommand. Available: list, query, copy, prune, delete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1518
+ }
1519
+ break;
1520
+ }
1521
+
1522
+ // ─── teams-status ──────────────────────────────────────────────────────
1523
+ // Read-only detector for claude-code's experimental agent-teams feature.
1524
+ // issue #1355: stop gsd-core hanging silently under claude-code agent-teams.
1525
+ // No capability registration needed — this is a diagnostic query command,
1526
+ // not a feature capability.
1527
+ case 'teams-status': {
1528
+ const teamsStatus = require('./lib/teams-status.cjs');
1529
+ teamsStatus.cmdTeamsStatus(cwd, { active: args.includes('--active') });
1530
+ break;
1531
+ }
1532
+
1533
+ // ─── detect-custom-files ───────────────────────────────────────────────
1534
+ // CJS-native: no SDK counterpart exists in the command registry.
1535
+ // detect-custom-files reads a pp-file-manifest.json against the
1536
+ // live filesystem to identify user-added files. It is installer-specific
1537
+ // logic that has no async query equivalent in the SDK.
1538
+ //
1539
+ // Detect user-added files inside GSD-managed directories that are not
1540
+ // tracked in pp-file-manifest.json. Used by the update workflow to back
1541
+ // up custom files before the installer wipes those directories.
1542
+ //
1543
+ // This replaces the fragile bash pattern:
1544
+ // MANIFEST_FILES=$(node -e "require('$RUNTIME_DIR/...')" 2>/dev/null)
1545
+ // ${filepath#$RUNTIME_DIR/} # unreliable path stripping
1546
+ // which silently returns CUSTOM_COUNT=0 when $RUNTIME_DIR is unset or
1547
+ // when the stripped path does not match the manifest key format (#1997).
1548
+
1549
+ case 'detect-custom-files': {
1550
+ const configDirIdx = args.indexOf('--config-dir');
1551
+ const configDir = configDirIdx !== -1 ? args[configDirIdx + 1] : null;
1552
+ if (!configDir) {
1553
+ error('Usage: gsd-tools detect-custom-files --config-dir <path>', ERROR_REASON.USAGE);
1554
+ }
1555
+ const resolvedConfigDir = path.resolve(configDir);
1556
+ if (!fs.existsSync(resolvedConfigDir)) {
1557
+ error(`Config directory not found: ${resolvedConfigDir}`, ERROR_REASON.USAGE);
1558
+ }
1559
+
1560
+ const manifestPath = path.join(resolvedConfigDir, 'pp-file-manifest.json');
1561
+ if (!fs.existsSync(manifestPath)) {
1562
+ // No manifest — cannot determine what is custom. Return empty list
1563
+ // (same behaviour as saveLocalPatches in install.js when no manifest).
1564
+ const out = { custom_files: [], custom_count: 0, manifest_found: false };
1565
+ process.stdout.write(JSON.stringify(out, null, 2));
1566
+ break;
1567
+ }
1568
+
1569
+ let manifest;
1570
+ try {
1571
+ manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
1572
+ } catch {
1573
+ const out = { custom_files: [], custom_count: 0, manifest_found: false, error: 'manifest parse error' };
1574
+ process.stdout.write(JSON.stringify(out, null, 2));
1575
+ break;
1576
+ }
1577
+
1578
+ const manifestKeys = new Set(Object.keys(manifest.files || {}));
1579
+
1580
+ // GSD-managed directories to scan for user-added files. Whole-owned
1581
+ // roots are wiped recursively; shared runtime roots are pruned by the
1582
+ // same gsd-* top-level prefix used by install.js _removeGsdEntries.
1583
+ const GSD_WHOLE_MANAGED_DIRS = [
1584
+ 'pp-core',
1585
+ path.join('commands', 'gsd'),
1586
+ ];
1587
+ const GSD_PREFIX_MANAGED_DIRS = [
1588
+ 'agents',
1589
+ 'hooks',
1590
+ 'skills',
1591
+ ];
1592
+
1593
+ function collectCustomFiles(dir, baseDir, manifestKeys, out) {
1594
+ if (!fs.existsSync(dir)) return;
1595
+ const stat = fs.statSync(dir);
1596
+ if (stat.isFile()) {
1597
+ const relPath = path.relative(baseDir, dir).replace(/\\/g, '/');
1598
+ if (!manifestKeys.has(relPath)) {
1599
+ out.push(relPath);
1600
+ }
1601
+ return;
1602
+ }
1603
+ if (!stat.isDirectory()) return;
1604
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1605
+ const fullPath = path.join(dir, entry.name);
1606
+ if (entry.isDirectory()) {
1607
+ collectCustomFiles(fullPath, baseDir, manifestKeys, out);
1608
+ continue;
1609
+ }
1610
+ // Use forward slashes for cross-platform manifest key compatibility
1611
+ const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
1612
+ if (!manifestKeys.has(relPath)) {
1613
+ out.push(relPath);
1614
+ }
1615
+ }
1616
+ }
1617
+
1618
+ const customFiles = [];
1619
+ for (const managedDir of GSD_WHOLE_MANAGED_DIRS) {
1620
+ const absDir = path.join(resolvedConfigDir, managedDir);
1621
+ if (!fs.existsSync(absDir)) continue;
1622
+ collectCustomFiles(absDir, resolvedConfigDir, manifestKeys, customFiles);
1623
+ }
1624
+ for (const managedDir of GSD_PREFIX_MANAGED_DIRS) {
1625
+ const absDir = path.join(resolvedConfigDir, managedDir);
1626
+ if (!fs.existsSync(absDir)) continue;
1627
+ for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
1628
+ if (!entry.name.startsWith('gsd-')) continue;
1629
+ collectCustomFiles(path.join(absDir, entry.name), resolvedConfigDir, manifestKeys, customFiles);
1630
+ }
1631
+ }
1632
+
1633
+ const out = {
1634
+ custom_files: customFiles,
1635
+ custom_count: customFiles.length,
1636
+ manifest_found: true,
1637
+ manifest_version: manifest.version || null,
1638
+ };
1639
+ process.stdout.write(JSON.stringify(out, null, 2));
1640
+ break;
1641
+ }
1642
+
1643
+ // ─── GSD-2 Reverse Migration ───────────────────────────────────────────
1644
+
1645
+ case 'from-gsd2': {
1646
+ const gsd2Import = require('./lib/gsd2-import.cjs');
1647
+ gsd2Import.cmdFromGsd2(args.slice(1), cwd, raw);
1648
+ break;
1649
+ }
1650
+
1651
+ // ─── Prompt Budget ────────────────────────────────────────────────────
1652
+ //
1653
+ // Assemble and deterministically trim review prompt sections to fit a
1654
+ // token budget. Used by the /gsd-review workflow before dispatching to
1655
+ // small-context local model servers (Ollama, llama.cpp, LM Studio).
1656
+ //
1657
+ // Required flags:
1658
+ // --budget <N> Token budget (integer > 0)
1659
+ // --instructions-file <path> Review instructions
1660
+ // --roadmap-file <path> Roadmap section
1661
+ // --plan-file <path> Plan file (may be repeated)
1662
+ // --output-prompt <path> Write trimmed prompt here
1663
+ // --output-metadata <path> Write metadata JSON here
1664
+ //
1665
+ // Optional flags:
1666
+ // --safety-margin-pct <N> Default 10
1667
+ // --project-md-head-lines <N> Default 40
1668
+ // --project-file <path>
1669
+ // --context-file <path>
1670
+ // --research-file <path>
1671
+ // --requirements-file <path>
1672
+ //
1673
+ // Exit codes:
1674
+ // 0 success (trim or no-trim)
1675
+ // 1 invocation error (missing required arg, missing file, invalid budget)
1676
+ // 2 hardFailed: prompt cannot fit effective budget after trim policy
1677
+
1678
+ case 'prompt-budget': {
1679
+ const promptBudget = require('./lib/prompt-budget.cjs');
1680
+
1681
+ // ── Collect multi-value --plan-file flags ──────────────────────────
1682
+ const planFiles = [];
1683
+ for (let i = 1; i < args.length; i++) {
1684
+ if (args[i] === '--plan-file' && args[i + 1] && !args[i + 1].startsWith('--')) {
1685
+ planFiles.push(args[i + 1]);
1686
+ i++;
1687
+ }
1688
+ }
1689
+
1690
+ // ── Parse single-value flags ───────────────────────────────────────
1691
+ const flagMap = new Map();
1692
+ for (let i = 1; i < args.length; i++) {
1693
+ const current = args[i];
1694
+ const next = args[i + 1];
1695
+ if (!current.startsWith('--')) continue;
1696
+ if (!next || next.startsWith('--')) {
1697
+ if (!flagMap.has(current)) flagMap.set(current, null);
1698
+ continue;
1699
+ }
1700
+ if (!flagMap.has(current)) flagMap.set(current, next);
1701
+ i++;
1702
+ }
1703
+ const getFlag = (flag) => flagMap.get(flag) ?? null;
1704
+
1705
+ const budgetStr = getFlag('--budget');
1706
+ const instructionsFile = getFlag('--instructions-file');
1707
+ const roadmapFile = getFlag('--roadmap-file');
1708
+ const outputPromptFile = getFlag('--output-prompt');
1709
+ const outputMetadataFile = getFlag('--output-metadata');
1710
+ const safetyMarginStr = getFlag('--safety-margin-pct');
1711
+ const projectMdHeadLinesStr = getFlag('--project-md-head-lines');
1712
+ const projectFile = getFlag('--project-file');
1713
+ const contextFile = getFlag('--context-file');
1714
+ const researchFile = getFlag('--research-file');
1715
+ const requirementsFile = getFlag('--requirements-file');
1716
+
1717
+ // ── Validate required args ─────────────────────────────────────────
1718
+ if (!budgetStr) {
1719
+ throw new ExitError(1, 'Error: --budget <N> is required');
1720
+ }
1721
+ const budget = parseInt(budgetStr, 10);
1722
+ if (!Number.isFinite(budget) || budget <= 0) {
1723
+ throw new ExitError(1, 'Error: --budget must be a positive integer');
1724
+ }
1725
+ if (!instructionsFile) {
1726
+ throw new ExitError(1, 'Error: --instructions-file <path> is required');
1727
+ }
1728
+ if (!roadmapFile) {
1729
+ throw new ExitError(1, 'Error: --roadmap-file <path> is required');
1730
+ }
1731
+ if (planFiles.length === 0) {
1732
+ throw new ExitError(1, 'Error: at least one --plan-file <path> is required');
1733
+ }
1734
+ if (!outputPromptFile) {
1735
+ throw new ExitError(1, 'Error: --output-prompt <path> is required');
1736
+ }
1737
+ if (!outputMetadataFile) {
1738
+ throw new ExitError(1, 'Error: --output-metadata <path> is required');
1739
+ }
1740
+
1741
+ // ── Validate and read required files ──────────────────────────────
1742
+ async function readRequired(filePath, flagName) {
1743
+ const resolved = path.resolve(filePath);
1744
+ try {
1745
+ return await fs.promises.readFile(resolved, 'utf8');
1746
+ } catch (err) {
1747
+ if (err && err.code === 'ENOENT') {
1748
+ throw new ExitError(1, `Error: file not found for ${flagName}: ${resolved}`);
1749
+ }
1750
+ throw new ExitError(1, `Error: cannot read file for ${flagName}: ${resolved}`);
1751
+ }
1752
+ }
1753
+
1754
+ async function readOptional(filePath) {
1755
+ if (!filePath) return null;
1756
+ const resolved = path.resolve(filePath);
1757
+ try {
1758
+ return await fs.promises.readFile(resolved, 'utf8');
1759
+ } catch (err) {
1760
+ if (err && err.code === 'ENOENT') return null;
1761
+ throw new ExitError(1, `Error: cannot read optional file: ${resolved}`);
1762
+ }
1763
+ }
1764
+
1765
+ const instructions = await readRequired(instructionsFile, '--instructions-file');
1766
+ const roadmap = await readRequired(roadmapFile, '--roadmap-file');
1767
+ const plans = await Promise.all(planFiles.map(async (p) => {
1768
+ const resolved = path.resolve(p);
1769
+ try {
1770
+ const content = await fs.promises.readFile(resolved, 'utf8');
1771
+ return { file: path.basename(p), content };
1772
+ } catch (err) {
1773
+ if (err && err.code === 'ENOENT') {
1774
+ throw new ExitError(1, `Error: plan file not found: ${resolved}`);
1775
+ }
1776
+ throw new ExitError(1, `Error: cannot read plan file: ${resolved}`);
1777
+ }
1778
+ }));
1779
+
1780
+ const projectMd = await readOptional(projectFile);
1781
+ const context = await readOptional(contextFile);
1782
+ const research = await readOptional(researchFile);
1783
+ const requirements = await readOptional(requirementsFile);
1784
+
1785
+ // ── Build options ─────────────────────────────────────────────────
1786
+ const options = {};
1787
+ if (safetyMarginStr !== null) {
1788
+ const pct = parseInt(safetyMarginStr, 10);
1789
+ if (Number.isFinite(pct)) options.safetyMarginPct = pct;
1790
+ }
1791
+ if (projectMdHeadLinesStr !== null) {
1792
+ const lines = parseInt(projectMdHeadLinesStr, 10);
1793
+ if (Number.isFinite(lines)) options.projectMdHeadLines = lines;
1794
+ }
1795
+
1796
+ // ── Call applyBudget ──────────────────────────────────────────────
1797
+ const sections = { instructions, roadmap, plans, projectMd, context, research, requirements };
1798
+ const { prompt, metadata } = promptBudget.applyBudget({ sections, budget, options });
1799
+
1800
+ // ── Write outputs ─────────────────────────────────────────────────
1801
+ await fs.promises.writeFile(path.resolve(outputMetadataFile), JSON.stringify(metadata, null, 2));
1802
+ await fs.promises.writeFile(path.resolve(outputPromptFile), prompt);
1803
+
1804
+ if (metadata.hardFailed) {
1805
+ throw new ExitError(2);
1806
+ }
1807
+ break;
1808
+ }
1809
+
1810
+ case 'update-context': {
1811
+ // #498: resolve the installed GSD version, scope, runtime, and config dir
1812
+ // for /gsd:update. Replaces ~280 lines of inline bash in update.md with a
1813
+ // tested projection. Emits the contract as JSON: { installedVersion,
1814
+ // scope, runtime, gsdDir }. Optional --config-dir / --runtime carry the
1815
+ // workflow's execution_context hints (the one thing only it can know).
1816
+ const { loadUpdateContext } = require('./lib/update-context.cjs');
1817
+ const ucArgs = args.slice(1);
1818
+ let preferredConfigDir = '';
1819
+ let preferredRuntime = '';
1820
+ for (let i = 0; i < ucArgs.length; i++) {
1821
+ const a = ucArgs[i];
1822
+ if (a.startsWith('--config-dir=')) { preferredConfigDir = a.slice('--config-dir='.length); continue; }
1823
+ if (a.startsWith('--runtime=')) { preferredRuntime = a.slice('--runtime='.length); continue; }
1824
+ if (a === '--config-dir') {
1825
+ const v = ucArgs[i + 1];
1826
+ if (v === undefined || v.startsWith('--')) error('Missing value for --config-dir', ERROR_REASON.USAGE);
1827
+ preferredConfigDir = v; i++; continue;
1828
+ }
1829
+ if (a === '--runtime') {
1830
+ const v = ucArgs[i + 1];
1831
+ if (v === undefined || v.startsWith('--')) error('Missing value for --runtime', ERROR_REASON.USAGE);
1832
+ preferredRuntime = v; i++; continue;
1833
+ }
1834
+ if (a === '--json') continue; // JSON is the only output; accepted for symmetry
1835
+ if (a.startsWith('-')) error(`Unknown flag for update-context: ${a}`, ERROR_REASON.USAGE);
1836
+ }
1837
+ const ctx = loadUpdateContext({ preferredConfigDir, preferredRuntime });
1838
+ process.stdout.write(JSON.stringify(ctx) + '\n');
1839
+ break;
1840
+ }
1841
+
1842
+ // ─── Research Store ────────────────────────────────────────────────────
1843
+ //
1844
+ // research-store get <key> [--kind <k>]
1845
+ // -> getResearch(cwd, key, { homeDir }); searches both tiers; output(result, raw)
1846
+ // (--kind is accepted for backward compatibility but no longer drives tier selection)
1847
+ // research-store put <key> --content <str> --source <s> --provider <p>
1848
+ // --confidence <c> --kind <k>
1849
+ // -> putResearch(cwd, key, { content, source, provider, confidence, kind })
1850
+ //
1851
+ // Tier is derived from source: 'curated' source writes to process.env.HOME/.gsd/research-cache;
1852
+ // all other sources write to cwd/.planning/research/.cache.
1853
+ // Tests may override the home directory by setting the HOME env var.
1854
+
1855
+ case 'research-store': {
1856
+ const researchStore = require('./lib/research-store.cjs');
1857
+ const subcommand = args[1];
1858
+ const homeDir = process.env.HOME || require('os').homedir();
1859
+ if (subcommand === 'get') {
1860
+ const key = args[2];
1861
+ if (!key || key.startsWith('--')) {
1862
+ error('Usage: gsd-tools research-store get <key> [--kind <k>]', ERROR_REASON.USAGE);
1863
+ }
1864
+ if (!researchStore.isValidResearchKey(key)) {
1865
+ error('research-store: <key> must be a 64-char sha256 hex (use research-plan to obtain keys)', ERROR_REASON.USAGE);
1866
+ }
1867
+ // --kind is accepted but no longer drives tier selection; getResearch searches both tiers
1868
+ const result = researchStore.getResearch(cwd, key, { homeDir });
1869
+ output(result, raw);
1870
+ } else if (subcommand === 'put') {
1871
+ const key = args[2];
1872
+ if (!key || key.startsWith('--')) {
1873
+ error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
1874
+ }
1875
+ if (!researchStore.isValidResearchKey(key)) {
1876
+ error('research-store: <key> must be a 64-char sha256 hex (use research-plan to obtain keys)', ERROR_REASON.USAGE);
1877
+ }
1878
+ const contentIdx = args.indexOf('--content');
1879
+ const sourceIdx = args.indexOf('--source');
1880
+ const providerIdx = args.indexOf('--provider');
1881
+ const confidenceIdx = args.indexOf('--confidence');
1882
+ const kindIdx = args.indexOf('--kind');
1883
+ // For each flag, if the following value is missing or itself starts with '--', reject.
1884
+ function getFlagValue(idx, flagName) {
1885
+ if (idx === -1) return null;
1886
+ const val = args[idx + 1];
1887
+ if (val === undefined || val.startsWith('--')) {
1888
+ error(`research-store put: missing value for ${flagName}`, ERROR_REASON.USAGE);
1889
+ }
1890
+ return val;
1891
+ }
1892
+ const content = getFlagValue(contentIdx, '--content');
1893
+ const source = getFlagValue(sourceIdx, '--source');
1894
+ const provider = getFlagValue(providerIdx, '--provider');
1895
+ const confidence = getFlagValue(confidenceIdx, '--confidence');
1896
+ const kind = getFlagValue(kindIdx, '--kind');
1897
+ if (!content || !source || !provider || !confidence || !kind) {
1898
+ error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
1899
+ }
1900
+ const entry = researchStore.putResearch(cwd, key, { content, source, provider, confidence, kind }, { homeDir });
1901
+ output(entry, raw);
1902
+ } else {
1903
+ error('Unknown research-store subcommand. Available: get, put', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1904
+ }
1905
+ break;
1906
+ }
1907
+
1908
+ // ─── Research Plan ─────────────────────────────────────────────────────
1909
+ //
1910
+ // research-plan --input <path>
1911
+ // Read+JSON.parse file; call planResearch({ questions, ecosystem, config, cwd })
1912
+ // { ecosystem, config, questions: [{ text, kind, library?, version? }] }
1913
+
1914
+ case 'research-plan': {
1915
+ const researchProvider = require('./lib/research-provider.cjs');
1916
+ const inputIdx = args.indexOf('--input');
1917
+ const inputPath = inputIdx !== -1 ? args[inputIdx + 1] : null;
1918
+ if (!inputPath || inputPath.startsWith('--')) {
1919
+ error('Usage: gsd-tools research-plan --input <path>', ERROR_REASON.USAGE);
1920
+ }
1921
+ let planInput;
1922
+ try {
1923
+ const raw_ = fs.readFileSync(path.resolve(inputPath), 'utf8');
1924
+ planInput = JSON.parse(raw_);
1925
+ } catch (readErr) {
1926
+ error(`research-plan: cannot read/parse --input file: ${inputPath}`, ERROR_REASON.USAGE);
1927
+ }
1928
+ if (planInput === null || typeof planInput !== 'object' || Array.isArray(planInput)) {
1929
+ error('research-plan: --input must be an object with a questions array', ERROR_REASON.USAGE);
1930
+ }
1931
+ if (!Array.isArray(planInput.questions)) {
1932
+ error('research-plan: --input must be an object with a questions array', ERROR_REASON.USAGE);
1933
+ }
1934
+ const { ecosystem = '', config: planConfig = {}, questions } = planInput;
1935
+ const homeDir = process.env.HOME || require('os').homedir();
1936
+ const plan = researchProvider.planResearch({ questions, ecosystem, config: planConfig, cwd, homeDir });
1937
+ output(plan, raw);
1938
+ break;
1939
+ }
1940
+
1941
+ // ─── Classify Confidence ──────────────────────────────────────────────
1942
+ //
1943
+ // classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]
1944
+ // -> classifyConfidence({ provider, verifiedAgainstOfficial, legitimacyVerdict }); output(result, raw)
1945
+ //
1946
+ // legitimacyVerdict is CODE-COMPUTED via checkPackages — never caller-supplied — so an agent cannot self-assert OK→HIGH.
1947
+
1948
+ case 'classify-confidence': {
1949
+ const researchProvider = require('./lib/research-provider.cjs');
1950
+ const providerIdx = args.indexOf('--provider');
1951
+ const provider = providerIdx !== -1 ? args[providerIdx + 1] : null;
1952
+ if (!provider || provider.startsWith('--')) {
1953
+ error('Usage: gsd-tools query classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]', ERROR_REASON.USAGE);
1954
+ }
1955
+ const verified = args.includes('--verified');
1956
+ const pkgIdx = args.indexOf('--package');
1957
+ const pkg = pkgIdx !== -1 ? args[pkgIdx + 1] : null;
1958
+ const ecoIdx = args.indexOf('--ecosystem');
1959
+ const ecosystem = ecoIdx !== -1 ? args[ecoIdx + 1] : null;
1960
+ let legitimacyVerdict = null;
1961
+ if (pkg && (!pkg.startsWith('--'))) {
1962
+ const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
1963
+ if (!ecosystem || ecosystem.startsWith('--') || !VALID_ECOSYSTEMS.has(ecosystem)) {
1964
+ error('Usage: gsd-tools query classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]', ERROR_REASON.USAGE);
1965
+ }
1966
+ const pkgLegitimacy = require('./lib/package-legitimacy.cjs');
1967
+ const results = await pkgLegitimacy.checkPackages({ ecosystem, packages: [pkg] }, {});
1968
+ legitimacyVerdict = results[0] ? results[0].verdict : null;
1969
+ }
1970
+ const confidence = researchProvider.classifyConfidence({ provider, verifiedAgainstOfficial: verified, legitimacyVerdict });
1971
+ output({ provider, package: pkg || null, ecosystem: ecosystem || null, legitimacyVerdict, verified, confidence }, raw);
1972
+ break;
1973
+ }
1974
+
1975
+ // ─── Package Legitimacy ────────────────────────────────────────────────
1976
+ //
1977
+ // package-legitimacy check --ecosystem <eco> <pkg1> <pkg2> ...
1978
+ //
1979
+ // checkPackages is ASYNC. This entire runCommand function is async, so
1980
+ // we can await directly. On rejection we call error() which exits.
1981
+
1982
+ case 'package-legitimacy': {
1983
+ const pkgLegitimacy = require('./lib/package-legitimacy.cjs');
1984
+ const subcommand = args[1];
1985
+ if (subcommand !== 'check') {
1986
+ error('Unknown package-legitimacy subcommand. Available: check', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1987
+ }
1988
+ const ecoIdx = args.indexOf('--ecosystem');
1989
+ const ecosystem = ecoIdx !== -1 ? args[ecoIdx + 1] : null;
1990
+ const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
1991
+ if (!ecosystem || !VALID_ECOSYSTEMS.has(ecosystem)) {
1992
+ error('Usage: gsd-tools package-legitimacy check --ecosystem <npm|pypi|crates> <pkg1> ...', ERROR_REASON.USAGE);
1993
+ }
1994
+ // Collect positional package names.
1995
+ // Only --ecosystem takes a value. Every non-flag arg is a package name.
1996
+ // Any unknown --flag is a usage error (do not silently skip+consume the next arg).
1997
+ const packages = [];
1998
+ for (let i = 2; i < args.length; i++) {
1999
+ const a = args[i];
2000
+ if (a === '--ecosystem') { i++; continue; }
2001
+ if (a.startsWith('--')) {
2002
+ error(`package-legitimacy: unknown flag ${a}`, ERROR_REASON.USAGE);
2003
+ }
2004
+ packages.push(a);
2005
+ }
2006
+ if (packages.length === 0) {
2007
+ error('Usage: gsd-tools package-legitimacy check --ecosystem <eco> <pkg1> <pkg2> ...', ERROR_REASON.USAGE);
2008
+ }
2009
+ let pkgResults;
2010
+ try {
2011
+ pkgResults = await pkgLegitimacy.checkPackages({ ecosystem, packages }, {});
2012
+ } catch (pkgErr) {
2013
+ error(`package-legitimacy: ${pkgErr && pkgErr.message ? pkgErr.message : String(pkgErr)}`, ERROR_REASON.UNKNOWN);
2014
+ }
2015
+ output(pkgResults, raw);
2016
+ break;
2017
+ }
2018
+
2019
+ case 'effort': {
2020
+ const subcommand = args[1];
2021
+ if (subcommand === 'sync') {
2022
+ const effortSyncArgs = args.slice(2);
2023
+ let dryRun = true;
2024
+ let effortSyncConfigDir;
2025
+ let effortSyncRuntime;
2026
+ for (let i = 0; i < effortSyncArgs.length; i++) {
2027
+ const a = effortSyncArgs[i];
2028
+ if (a === '--apply') { dryRun = false; continue; }
2029
+ if (a === '--dry-run') { dryRun = true; continue; }
2030
+ if (a.startsWith('--config-dir=')) { effortSyncConfigDir = a.slice('--config-dir='.length); continue; }
2031
+ if (a === '--config-dir') {
2032
+ const v = effortSyncArgs[i + 1];
2033
+ if (!v || v.startsWith('--')) error('Missing value for --config-dir', ERROR_REASON.USAGE);
2034
+ effortSyncConfigDir = v; i++; continue;
2035
+ }
2036
+ if (a.startsWith('--runtime=')) { effortSyncRuntime = a.slice('--runtime='.length); continue; }
2037
+ if (a === '--runtime') {
2038
+ const v = effortSyncArgs[i + 1];
2039
+ if (!v || v.startsWith('--')) error('Missing value for --runtime', ERROR_REASON.USAGE);
2040
+ effortSyncRuntime = v; i++; continue;
2041
+ }
2042
+ if (a === '--raw') continue;
2043
+ if (a.startsWith('-')) error(`Unknown flag for effort sync: ${a}`, ERROR_REASON.USAGE);
2044
+ error(`effort sync takes no positional arguments; got: ${a}`, ERROR_REASON.USAGE);
2045
+ }
2046
+ commands.cmdEffortSync(cwd, raw, { dryRun, configDir: effortSyncConfigDir, runtime: effortSyncRuntime });
2047
+ } else {
2048
+ error('Unknown effort subcommand. Available: sync', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2049
+ }
2050
+ break;
2051
+ }
2052
+
2053
+ // ─── User Story Validation (bug #1145) ────────────────────────────────────
2054
+ //
2055
+ // Invocation shapes (from mvp-phase.md and verify-work.md):
2056
+ // pp_run query user-story.validate --story "$USER_STORY"
2057
+ // pp_run query user-story.validate --story "$PHASE_GOAL" --pick valid
2058
+ //
2059
+ // Returns JSON: { valid: boolean, errors: string[], slots: { role, capability, outcome } | null }
2060
+ // - valid: true only when the story fully matches the canonical format
2061
+ // - errors: per-slot diagnostic strings (empty on success)
2062
+ // - slots: extracted role/capability/outcome on success; null on failure
2063
+ //
2064
+ // Canonical format (user-story-template.md):
2065
+ // "As a [user role], I want to [capability], so that [outcome]."
2066
+ // Each slot must be non-empty and contain non-whitespace content.
2067
+ //
2068
+ // No .planning/ access needed — pure string validation.
2069
+
2070
+ // #1146: single base-branch resolver for all forking workflows.
2071
+ // Workflows call `pp_run query git.base-branch` (dotted form normalised to
2072
+ // command='git', args=['git','base-branch']).
2073
+ case 'git': {
2074
+ const subcommand = args[1];
2075
+ if (subcommand !== 'base-branch') {
2076
+ error(
2077
+ `Unknown git subcommand: ${subcommand || '(none)'}. Available: base-branch`,
2078
+ ERROR_REASON.SDK_UNKNOWN_COMMAND,
2079
+ );
2080
+ break;
2081
+ }
2082
+ cmdGitBaseBranch(cwd, args.slice(2));
2083
+ break;
2084
+ }
2085
+
2086
+ case 'user-story': {
2087
+ const subcommand = args[1];
2088
+ if (subcommand !== 'validate') {
2089
+ error(`Unknown user-story subcommand: ${subcommand || '(none)'}. Available: validate`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
2090
+ break;
2091
+ }
2092
+
2093
+ const storyIdx = args.indexOf('--story');
2094
+ const story = (storyIdx !== -1 && args[storyIdx + 1] && !args[storyIdx + 1].startsWith('--'))
2095
+ ? args[storyIdx + 1]
2096
+ : '';
2097
+
2098
+ // Canonical extraction regex — requires non-whitespace content in each slot
2099
+ // (\S.*? ensures the slot isn't whitespace-only).
2100
+ // Named groups: role / capability / outcome.
2101
+ const USER_STORY_RE = /^As a (\S.*?), I want to (\S.*?), so that (\S.*?)\.$/;
2102
+
2103
+ const errors = [];
2104
+ const trimmed = story.trim();
2105
+ let slots = null;
2106
+
2107
+ if (!trimmed) {
2108
+ errors.push('Story is empty. Required format: "As a [role], I want to [capability], so that [outcome]."');
2109
+ } else {
2110
+ // Per-clause guards produce targeted, actionable error messages before
2111
+ // attempting the full regex. Guards are ordered: role → capability → outcome → period.
2112
+ if (!/^As a \S/i.test(trimmed)) {
2113
+ errors.push('Story must start with "As a [user role]," (role must be non-empty).');
2114
+ }
2115
+ if (!/, I want to \S/i.test(trimmed)) {
2116
+ errors.push('Story must include ", I want to [capability]," (capability must be non-empty).');
2117
+ }
2118
+ if (!/, so that \S/i.test(trimmed)) {
2119
+ errors.push('Story must include ", so that [outcome]." (outcome must be non-empty).');
2120
+ }
2121
+ if (!trimmed.endsWith('.')) {
2122
+ errors.push('Story must end with a period (.).');
2123
+ }
2124
+ // Full-regex check only when per-clause guards all passed — avoids
2125
+ // redundant "format mismatch" noise on top of specific error messages.
2126
+ if (errors.length === 0) {
2127
+ const m = USER_STORY_RE.exec(trimmed);
2128
+ if (!m) {
2129
+ errors.push('Story does not match the canonical format: "As a [role], I want to [capability], so that [outcome]."');
2130
+ } else {
2131
+ slots = { role: m[1], capability: m[2], outcome: m[3] };
2132
+ }
2133
+ }
2134
+ }
2135
+
2136
+ output({ valid: errors.length === 0, errors, slots }, raw);
2137
+ break;
2138
+ }
2139
+
2140
+ case 'drift-guard': {
2141
+ // ADR-22: deterministic authority resolution + severity classification.
2142
+ // Subcommands:
2143
+ // drift-guard authority → effective authority string
2144
+ // drift-guard severity --status <S> [--authority <A>] → {severity, hardBlock}
2145
+ const subcommand = args[1];
2146
+
2147
+ // Read config.json directly for both plan_review.source_grounding_authority
2148
+ // and intel.enabled. Neither key is in the config-loader.cjs whitelist that
2149
+ // config-loader.cjs's loadConfig() whitelist does not return; plan_review is only in config.cjs's private
2150
+ // buildConfig(), and intel is a federated capability config key.
2151
+ let configuredAuthority = 'grep';
2152
+ let intelEnabled = false;
2153
+ try {
2154
+ const { planningDir } = require('./lib/planning-workspace.cjs');
2155
+ const cfgPath = require('path').join(planningDir(cwd), 'config.json');
2156
+ if (require('fs').existsSync(cfgPath)) {
2157
+ const rawCfg = JSON.parse(require('fs').readFileSync(cfgPath, 'utf-8'));
2158
+ if (rawCfg && rawCfg.plan_review && rawCfg.plan_review.source_grounding_authority) {
2159
+ configuredAuthority = String(rawCfg.plan_review.source_grounding_authority);
2160
+ }
2161
+ if (rawCfg && rawCfg.intel && rawCfg.intel.enabled === true) {
2162
+ intelEnabled = true;
2163
+ }
2164
+ }
2165
+ } catch {
2166
+ // not fatal — defaults apply
2167
+ }
2168
+
2169
+ const effectiveAuthority = getEffectiveAuthority(configuredAuthority, intelEnabled);
2170
+
2171
+ if (subcommand === 'authority') {
2172
+ // Pass rawValue as 3rd arg so --raw returns unquoted string (not JSON)
2173
+ output(effectiveAuthority, raw, effectiveAuthority);
2174
+ break;
2175
+ }
2176
+
2177
+ if (subcommand === 'severity') {
2178
+ const statusIdx = args.indexOf('--status');
2179
+ const statusVal = statusIdx !== -1 ? args[statusIdx + 1] : undefined;
2180
+ if (!statusVal || statusVal.startsWith('--')) {
2181
+ error('drift-guard severity requires --status <VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE>', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2182
+ break;
2183
+ }
2184
+ const authIdx = args.indexOf('--authority');
2185
+ const authVal = authIdx !== -1 ? args[authIdx + 1] : undefined;
2186
+ const authorityForClassify = (authVal && !authVal.startsWith('--'))
2187
+ ? authVal
2188
+ : effectiveAuthority;
2189
+ const result = classifyDriftSeverity({ status: statusVal, authority: authorityForClassify });
2190
+ output(result, raw);
2191
+ break;
2192
+ }
2193
+
2194
+ error(
2195
+ `Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity`,
2196
+ ERROR_REASON.SDK_UNKNOWN_COMMAND,
2197
+ );
2198
+ break;
2199
+ }
2200
+
2201
+ default: {
2202
+ // ADR-959: try capability-registry dispatch before emitting the unknown-command error.
2203
+ // An unmigrated command still hits its hardcoded `case` above — untouched.
2204
+ // A migrated command's `case` is removed at cutover, so it reaches here and
2205
+ // dispatchCapabilityCommand routes it to the capability's registered router.
2206
+ // commandFamilies now includes migrated capabilities (e.g. graphify → graphify-command-router.cjs);
2207
+ // this returns true when a registered capability owns the command, false otherwise.
2208
+ if (dispatchCapabilityCommand({ command, args, cwd, raw, error })) break;
2209
+
2210
+ // #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
2211
+ // above split it so `command` here is the head ("foo"). Use
2212
+ // originalCommand to reconstruct the original dotted form and suggest
2213
+ // the spaced equivalent — surfacing a useful diagnostic instead of just
2214
+ // "Unknown command: foo".
2215
+ const wasDotted =
2216
+ typeof originalCommand === 'string' &&
2217
+ originalCommand !== command &&
2218
+ originalCommand.includes('.');
2219
+ let suggestion = '';
2220
+ if (wasDotted) {
2221
+ const dotIdx = originalCommand.indexOf('.');
2222
+ const head = originalCommand.slice(0, dotIdx);
2223
+ const rest = originalCommand.slice(dotIdx + 1);
2224
+ suggestion = ` — did you mean: "${head} ${rest}"?`;
2225
+ }
2226
+ error(`Unknown command: ${command}${suggestion}`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
2227
+ }
2228
+ }
2229
+ }
2230
+
2231
+ // ─── CLI entry point ──────────────────────────────────────────────────────────
2232
+ if (require.main === module) {
2233
+ runMain(main);
2234
+ }
2235
+
2236
+ // ─── Exports (for tests) ──────────────────────────────────────────────────────
2237
+ // ADR-959: export dispatchCapabilityCommand so tests can exercise it with
2238
+ // synthetic registry + requireModule injections.
2239
+ module.exports = { dispatchCapabilityCommand };