@opengsd/gsd-core 1.11.0 → 1.13.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 (498) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-debug-session-manager.md +1 -1
  6. package/agents/gsd-debugger.md +1 -1
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +78 -42
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +0 -1
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +3 -1
  15. package/agents/gsd-plan-checker.md +91 -112
  16. package/agents/gsd-planner.md +20 -4
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +82 -7
  21. package/agents/gsd-ui-researcher.md +70 -3
  22. package/agents/gsd-verifier.md +24 -2
  23. package/bin/install.js +847 -200
  24. package/commands/gsd/discuss-phase.md +1 -1
  25. package/commands/gsd/execute-phase.md +1 -1
  26. package/commands/gsd/import.md +1 -1
  27. package/commands/gsd/ns-workflow.md +2 -1
  28. package/commands/gsd/phase.md +1 -1
  29. package/commands/gsd/quick-batch.md +105 -0
  30. package/commands/gsd/quick.md +8 -4
  31. package/commands/gsd/surface.md +18 -8
  32. package/gsd-core/bin/gsd-tools.cjs +761 -100
  33. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  34. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  35. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  36. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  37. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  38. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  39. package/gsd-core/bin/lib/audit.cjs +163 -41
  40. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  41. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  42. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  43. package/gsd-core/bin/lib/capability-registry.cjs +785 -144
  44. package/gsd-core/bin/lib/capability-state.cjs +25 -4
  45. package/gsd-core/bin/lib/capability-validator.cjs +321 -18
  46. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  47. package/gsd-core/bin/lib/check-command-router.cjs +229 -6
  48. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  49. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  50. package/gsd-core/bin/lib/clusters.cjs +1 -0
  51. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  52. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  53. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  54. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  55. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  56. package/gsd-core/bin/lib/commands.cjs +877 -54
  57. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  58. package/gsd-core/bin/lib/config-loader.cjs +121 -29
  59. package/gsd-core/bin/lib/config.cjs +92 -2
  60. package/gsd-core/bin/lib/configuration.cjs +129 -37
  61. package/gsd-core/bin/lib/core-utils.cjs +118 -14
  62. package/gsd-core/bin/lib/decisions.cjs +213 -1
  63. package/gsd-core/bin/lib/edge-probe.cjs +23 -2
  64. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  65. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  66. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  67. package/gsd-core/bin/lib/frontmatter.cjs +975 -326
  68. package/gsd-core/bin/lib/gap-checker.cjs +41 -8
  69. package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  71. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  72. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
  73. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  74. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  75. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  76. package/gsd-core/bin/lib/host-integration.cjs +96 -11
  77. package/gsd-core/bin/lib/init-command-router.cjs +132 -21
  78. package/gsd-core/bin/lib/init.cjs +252 -56
  79. package/gsd-core/bin/lib/install-engine.cjs +252 -15
  80. package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
  81. package/gsd-core/bin/lib/install-profiles.cjs +100 -18
  82. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  83. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  84. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  85. package/gsd-core/bin/lib/intel.cjs +101 -26
  86. package/gsd-core/bin/lib/io.cjs +195 -15
  87. package/gsd-core/bin/lib/learnings.cjs +85 -14
  88. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  89. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  90. package/gsd-core/bin/lib/markdown-table.cjs +175 -4
  91. package/gsd-core/bin/lib/milestone.cjs +112 -7
  92. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  93. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  94. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  95. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  96. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  97. package/gsd-core/bin/lib/phase-id.cjs +321 -13
  98. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  99. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  100. package/gsd-core/bin/lib/phase.cjs +1175 -115
  101. package/gsd-core/bin/lib/plan-document.cjs +273 -0
  102. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  103. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  104. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  105. package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
  106. package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
  107. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  108. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  109. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  110. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  111. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  112. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  113. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  114. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  115. package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
  116. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  117. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  118. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  119. package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
  120. package/gsd-core/bin/lib/roadmap.cjs +248 -64
  121. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
  122. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  123. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
  124. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
  125. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  126. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  127. package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
  128. package/gsd-core/bin/lib/smart-entry.cjs +19 -31
  129. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  130. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  131. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  132. package/gsd-core/bin/lib/state-document.cjs +216 -5
  133. package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
  134. package/gsd-core/bin/lib/state-transition.cjs +850 -145
  135. package/gsd-core/bin/lib/state.cjs +1629 -287
  136. package/gsd-core/bin/lib/surface.cjs +33 -10
  137. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  138. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  139. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  140. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  141. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  142. package/gsd-core/bin/lib/uat.cjs +2542 -387
  143. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  144. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  145. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  146. package/gsd-core/bin/lib/update-context.cjs +6 -2
  147. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  148. package/gsd-core/bin/lib/validate.cjs +230 -12
  149. package/gsd-core/bin/lib/vendor/README.md +43 -5
  150. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  151. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  152. package/gsd-core/bin/lib/verification.cjs +287 -13
  153. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  154. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  155. package/gsd-core/bin/lib/verify.cjs +441 -56
  156. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  157. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  158. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  159. package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
  160. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  161. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  162. package/gsd-core/bin/shared/exit-codes.json +8 -0
  163. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  164. package/gsd-core/bin/shared/model-catalog.json +8 -1
  165. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  166. package/gsd-core/references/agent-contracts.md +6 -5
  167. package/gsd-core/references/api-coverage.md +24 -2
  168. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  169. package/gsd-core/references/checkpoints.md +37 -19
  170. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  171. package/gsd-core/references/edge-probe.md +17 -5
  172. package/gsd-core/references/execute-mvp-tdd.md +18 -18
  173. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  174. package/gsd-core/references/execute-phase-response-language.md +6 -0
  175. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  176. package/gsd-core/references/executor-examples.md +42 -0
  177. package/gsd-core/references/failing-direction.md +78 -0
  178. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  179. package/gsd-core/references/gate-prompts.md +1 -1
  180. package/gsd-core/references/git-integration.md +5 -5
  181. package/gsd-core/references/git-planning-commit.md +3 -3
  182. package/gsd-core/references/gsd-run-resolver.md +1 -1
  183. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  184. package/gsd-core/references/model-profiles.md +1 -1
  185. package/gsd-core/references/mvp-concepts.md +2 -2
  186. package/gsd-core/references/nyquist-compliance.md +74 -0
  187. package/gsd-core/references/offer-next.md +3 -5
  188. package/gsd-core/references/phase-argument-parsing.md +3 -3
  189. package/gsd-core/references/plan-checker-examples.md +41 -0
  190. package/gsd-core/references/planner-antipatterns.md +25 -0
  191. package/gsd-core/references/planner-chunked.md +5 -1
  192. package/gsd-core/references/planner-coupling.md +42 -0
  193. package/gsd-core/references/planner-failing-direction.md +53 -0
  194. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  195. package/gsd-core/references/planner-quick-batch.md +71 -0
  196. package/gsd-core/references/planner-reviews.md +47 -0
  197. package/gsd-core/references/planner-revision.md +76 -3
  198. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  199. package/gsd-core/references/planning-config.md +39 -9
  200. package/gsd-core/references/response-language-directive.md +9 -0
  201. package/gsd-core/references/reviewer-instances.md +31 -0
  202. package/gsd-core/references/revision-loop.md +118 -11
  203. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  204. package/gsd-core/references/tdd.md +15 -12
  205. package/gsd-core/references/ui-brand.md +65 -21
  206. package/gsd-core/references/ui-consideration-probe.md +1 -1
  207. package/gsd-core/references/universal-anti-patterns.md +2 -2
  208. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  209. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  210. package/gsd-core/references/verify-mvp-mode.md +1 -1
  211. package/gsd-core/references/workstream-flag.md +11 -11
  212. package/gsd-core/templates/README.md +1 -1
  213. package/gsd-core/templates/SECURITY.md +3 -3
  214. package/gsd-core/templates/UI-SPEC.md +25 -3
  215. package/gsd-core/templates/VALIDATION.md +3 -3
  216. package/gsd-core/templates/phase-prompt.md +7 -0
  217. package/gsd-core/templates/state.md +7 -0
  218. package/gsd-core/templates/verification-report.md +5 -0
  219. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  220. package/gsd-core/workflows/add-backlog.md +3 -1
  221. package/gsd-core/workflows/add-phase.md +5 -3
  222. package/gsd-core/workflows/add-tests.md +4 -9
  223. package/gsd-core/workflows/add-todo.md +2 -2
  224. package/gsd-core/workflows/ai-integration-phase.md +5 -10
  225. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  226. package/gsd-core/workflows/audit-fix.md +14 -3
  227. package/gsd-core/workflows/audit-milestone.md +11 -9
  228. package/gsd-core/workflows/audit-uat.md +19 -2
  229. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  230. package/gsd-core/workflows/autonomous.md +12 -26
  231. package/gsd-core/workflows/check-todos.md +2 -2
  232. package/gsd-core/workflows/cleanup.md +3 -3
  233. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
  234. package/gsd-core/workflows/code-review-fix.md +3 -1
  235. package/gsd-core/workflows/code-review.md +192 -69
  236. package/gsd-core/workflows/complete-milestone.md +28 -14
  237. package/gsd-core/workflows/debug.md +6 -4
  238. package/gsd-core/workflows/diagnose-issues.md +17 -7
  239. package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
  240. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  241. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  242. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  243. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  244. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
  245. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  246. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  247. package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
  248. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  249. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  250. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
  252. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  253. package/gsd-core/workflows/discuss-phase.md +2 -2
  254. package/gsd-core/workflows/do.md +46 -19
  255. package/gsd-core/workflows/docs-update.md +6 -5
  256. package/gsd-core/workflows/edit-phase.md +3 -1
  257. package/gsd-core/workflows/eval-review.md +5 -10
  258. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
  259. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
  260. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  261. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  263. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +29 -5
  264. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  265. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  266. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
  267. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  268. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  269. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  270. package/gsd-core/workflows/execute-phase.md +68 -66
  271. package/gsd-core/workflows/execute-plan.md +25 -20
  272. package/gsd-core/workflows/explore.md +3 -1
  273. package/gsd-core/workflows/extract-learnings.md +3 -1
  274. package/gsd-core/workflows/fast.md +8 -2
  275. package/gsd-core/workflows/forensics.md +3 -1
  276. package/gsd-core/workflows/graduation.md +6 -6
  277. package/gsd-core/workflows/health.md +4 -7
  278. package/gsd-core/workflows/help/modes/brief.md +2 -0
  279. package/gsd-core/workflows/help/modes/default.md +2 -0
  280. package/gsd-core/workflows/help/modes/full.md +12 -0
  281. package/gsd-core/workflows/help/modes/topic.md +2 -0
  282. package/gsd-core/workflows/help.md +2 -0
  283. package/gsd-core/workflows/import.md +17 -14
  284. package/gsd-core/workflows/inbox.md +5 -6
  285. package/gsd-core/workflows/ingest-docs.md +45 -12
  286. package/gsd-core/workflows/insert-phase.md +7 -5
  287. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  288. package/gsd-core/workflows/list-seeds.md +7 -3
  289. package/gsd-core/workflows/list-workspaces.md +3 -1
  290. package/gsd-core/workflows/manager.md +15 -26
  291. package/gsd-core/workflows/map-codebase.md +3 -1
  292. package/gsd-core/workflows/milestone-summary.md +3 -1
  293. package/gsd-core/workflows/mvp-phase.md +3 -3
  294. package/gsd-core/workflows/new-milestone.md +10 -22
  295. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  296. package/gsd-core/workflows/new-project.md +17 -29
  297. package/gsd-core/workflows/new-workspace.md +2 -2
  298. package/gsd-core/workflows/next.md +4 -2
  299. package/gsd-core/workflows/node-repair.md +2 -0
  300. package/gsd-core/workflows/note.md +2 -0
  301. package/gsd-core/workflows/onboard.md +1 -1
  302. package/gsd-core/workflows/pause-work.md +20 -5
  303. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  304. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  305. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
  306. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
  307. package/gsd-core/workflows/plan-phase.md +251 -54
  308. package/gsd-core/workflows/plan-review-convergence.md +148 -19
  309. package/gsd-core/workflows/plant-seed.md +3 -3
  310. package/gsd-core/workflows/pr-branch.md +195 -51
  311. package/gsd-core/workflows/profile-user.md +17 -15
  312. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  313. package/gsd-core/workflows/progress.md +52 -15
  314. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  315. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
  316. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  317. package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
  318. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  319. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  320. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  321. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  322. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  323. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  324. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  325. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  326. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  327. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  328. package/gsd-core/workflows/quick-batch.md +203 -0
  329. package/gsd-core/workflows/quick.md +33 -32
  330. package/gsd-core/workflows/reapply-patches.md +2 -0
  331. package/gsd-core/workflows/remove-phase.md +6 -4
  332. package/gsd-core/workflows/remove-workspace.md +3 -3
  333. package/gsd-core/workflows/resume-project.md +14 -14
  334. package/gsd-core/workflows/review.md +404 -21
  335. package/gsd-core/workflows/scan.md +3 -1
  336. package/gsd-core/workflows/section-manifest.json +12 -0
  337. package/gsd-core/workflows/secure-phase.md +3 -3
  338. package/gsd-core/workflows/session-report.md +2 -0
  339. package/gsd-core/workflows/settings-advanced.md +9 -9
  340. package/gsd-core/workflows/settings-integrations.md +66 -32
  341. package/gsd-core/workflows/settings.md +4 -6
  342. package/gsd-core/workflows/ship.md +22 -16
  343. package/gsd-core/workflows/sketch-wrap-up.md +13 -17
  344. package/gsd-core/workflows/sketch.md +13 -19
  345. package/gsd-core/workflows/smart-entry.md +4 -6
  346. package/gsd-core/workflows/spec-phase.md +31 -4
  347. package/gsd-core/workflows/spike-wrap-up.md +9 -11
  348. package/gsd-core/workflows/spike.md +21 -32
  349. package/gsd-core/workflows/stats.md +4 -2
  350. package/gsd-core/workflows/sync-skills.md +13 -5
  351. package/gsd-core/workflows/thread.md +13 -7
  352. package/gsd-core/workflows/transition.md +7 -5
  353. package/gsd-core/workflows/ui-phase.md +36 -21
  354. package/gsd-core/workflows/ui-review.md +7 -11
  355. package/gsd-core/workflows/ultraplan-phase.md +7 -13
  356. package/gsd-core/workflows/undo.md +9 -17
  357. package/gsd-core/workflows/update.md +47 -48
  358. package/gsd-core/workflows/validate-phase.md +3 -3
  359. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  360. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  361. package/gsd-core/workflows/verify-work.md +106 -21
  362. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  363. package/hooks/dist/gsd-check-update-worker.js +19 -2
  364. package/hooks/dist/gsd-config-reload.js +18 -12
  365. package/hooks/dist/gsd-context-monitor.js +302 -22
  366. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  367. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  368. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  369. package/hooks/dist/gsd-cursor-stop.js +2 -1
  370. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  371. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  372. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  373. package/hooks/dist/gsd-graphify-update.sh +22 -18
  374. package/hooks/dist/gsd-node-runner.sh +77 -0
  375. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  376. package/hooks/dist/gsd-prompt-guard.js +46 -12
  377. package/hooks/dist/gsd-read-guard.js +18 -7
  378. package/hooks/dist/gsd-read-injection-scanner.js +22 -13
  379. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  380. package/hooks/dist/gsd-session-state.sh +1 -0
  381. package/hooks/dist/gsd-statusline.js +222 -29
  382. package/hooks/dist/gsd-validate-commit.sh +523 -12
  383. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  384. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  385. package/hooks/dist/gsd-workflow-guard.js +36 -17
  386. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  387. package/hooks/dist/gsd-write-guard.js +35 -25
  388. package/hooks/dist/lib/cli-exit.js +560 -0
  389. package/hooks/dist/lib/exit-code-registry.js +98 -0
  390. package/hooks/dist/lib/git-cmd.js +210 -1
  391. package/hooks/dist/lib/git-probe.js +84 -0
  392. package/hooks/dist/lib/hook-exit.js +81 -0
  393. package/hooks/dist/lib/injection-patterns.js +36 -6
  394. package/hooks/dist/managed-hooks-registry.cjs +4 -0
  395. package/hooks/gsd-agent-isolation-guard.js +77 -38
  396. package/hooks/gsd-check-update-worker.js +19 -2
  397. package/hooks/gsd-config-reload.js +18 -12
  398. package/hooks/gsd-context-monitor.js +302 -22
  399. package/hooks/gsd-cursor-post-tool.js +3 -1
  400. package/hooks/gsd-cursor-pre-tool.js +3 -1
  401. package/hooks/gsd-cursor-session-start.js +2 -1
  402. package/hooks/gsd-cursor-stop.js +2 -1
  403. package/hooks/gsd-cursor-subagent-start.js +28 -23
  404. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  405. package/hooks/gsd-ensure-canonical-path.js +2 -1
  406. package/hooks/gsd-graphify-update.sh +22 -18
  407. package/hooks/gsd-node-runner.sh +77 -0
  408. package/hooks/gsd-phase-boundary.sh +1 -0
  409. package/hooks/gsd-prompt-guard.js +46 -12
  410. package/hooks/gsd-read-guard.js +18 -7
  411. package/hooks/gsd-read-injection-scanner.js +22 -13
  412. package/hooks/gsd-secret-read-guard.js +1079 -0
  413. package/hooks/gsd-session-state.sh +1 -0
  414. package/hooks/gsd-statusline.js +222 -29
  415. package/hooks/gsd-validate-commit.sh +523 -12
  416. package/hooks/gsd-windsurf-pre-command.js +16 -11
  417. package/hooks/gsd-windsurf-pre-write.js +22 -13
  418. package/hooks/gsd-workflow-guard.js +36 -17
  419. package/hooks/gsd-worktree-path-guard.js +36 -21
  420. package/hooks/gsd-write-guard.js +35 -25
  421. package/hooks/hooks.json +6 -0
  422. package/hooks/lib/cli-exit.js +560 -0
  423. package/hooks/lib/exit-code-registry.js +98 -0
  424. package/hooks/lib/git-cmd.js +210 -1
  425. package/hooks/lib/git-probe.js +84 -0
  426. package/hooks/lib/hook-exit.js +81 -0
  427. package/hooks/lib/injection-patterns.js +36 -6
  428. package/hooks/managed-hooks-registry.cjs +4 -0
  429. package/package.json +14 -9
  430. package/scripts/base64-scan.sh +74 -12
  431. package/scripts/build-hooks.js +12 -0
  432. package/scripts/check-glossary-refs.cjs +77 -15
  433. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  434. package/scripts/ci-check-job-near-cap.cjs +49 -0
  435. package/scripts/ci-pr-mergeability.cjs +262 -0
  436. package/scripts/ci-test-scope.cjs +52 -12
  437. package/scripts/ci-timeout-report.cjs +230 -0
  438. package/scripts/docs-guard-registry.cjs +406 -0
  439. package/scripts/gen-capability-registry.cjs +8 -6
  440. package/scripts/gen-exit-code-docs.cjs +318 -0
  441. package/scripts/gen-exit-code-registry.cjs +891 -0
  442. package/scripts/gen-features.cjs +836 -0
  443. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  444. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  445. package/scripts/gen-loop-host-contract.cjs +189 -4
  446. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  447. package/scripts/gen-state-md-docs.cjs +727 -0
  448. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  449. package/scripts/lib/ci-job-timing.cjs +72 -0
  450. package/scripts/lib/cli-exit.cjs +546 -44
  451. package/scripts/lib/drift-scan.cjs +32 -2
  452. package/scripts/lib/exit-code-registry.cjs +98 -0
  453. package/scripts/lib/ndjson-reporter.cjs +119 -0
  454. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  455. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  456. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  457. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  458. package/scripts/lint-docs-guard-registration.cjs +495 -0
  459. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
  460. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  461. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  462. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  463. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  464. package/scripts/lint-phase-enumeration-drift.cjs +45 -14
  465. package/scripts/lint-phase-id-drift.cjs +133 -8
  466. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  467. package/scripts/lint-portable-grep.cjs +176 -0
  468. package/scripts/lint-removed-but-needed.cjs +184 -16
  469. package/scripts/lint-response-language-coverage.cjs +524 -0
  470. package/scripts/lint-seam-enforcement.cjs +182 -0
  471. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  472. package/scripts/lint-source-test-name-collision.cjs +241 -0
  473. package/scripts/lint-state-write-path-drift.cjs +337 -432
  474. package/scripts/lint-test-file-count.allowlist.json +124 -4
  475. package/scripts/lint-test-file-count.cjs +25 -3
  476. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  477. package/scripts/lint-vendored-deps.cjs +208 -35
  478. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  479. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  480. package/scripts/mutation-matrix.cjs +599 -50
  481. package/scripts/npm-audit-baseline.cjs +376 -0
  482. package/scripts/prompt-injection-scan.sh +83 -14
  483. package/scripts/require-issue-link-policy.cjs +16 -1
  484. package/scripts/secret-scan.sh +75 -13
  485. package/scripts/select-docs-guards.cjs +56 -0
  486. package/scripts/sync-runtime-launcher.cjs +22 -3
  487. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  488. package/skills/gsd-execute-phase/SKILL.md +1 -1
  489. package/skills/gsd-import/SKILL.md +1 -1
  490. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  491. package/skills/gsd-phase/SKILL.md +1 -1
  492. package/skills/gsd-quick/SKILL.md +8 -4
  493. package/skills/gsd-quick-batch/SKILL.md +105 -0
  494. package/skills/gsd-surface/SKILL.md +18 -8
  495. package/vscode/package.json +1 -1
  496. package/bin/lib/ui-safety-gate.cjs +0 -109
  497. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  498. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -30,6 +30,9 @@
30
30
  * list-todos [area] Count and enumerate pending todos
31
31
  * list-seeds [status] List captured seeds (optional status filter)
32
32
  * verify-path-exists <path> Check file/directory existence
33
+ * quick-tasks-migrate Migrate STATE.md's "Quick Tasks Completed"
34
+ * table onto the canonical schema (#3730).
35
+ * No-op when absent or already canonical.
33
36
  * quick-tasks-append --task <text> Append a row to STATE.md's "Quick Tasks
34
37
  * Completed" table (schema-backed via
35
38
  * markdown-table.cjs; #2133/ADR-2143).
@@ -68,7 +71,12 @@
68
71
  * gaps_found-only, never call on the pass path
69
72
  *
70
73
  * Milestone Operations:
71
- * milestone complete <version> Archive milestone, create MILESTONES.md
74
+ * milestone complete <version> (--confirm | --dry-run)
75
+ * Archive milestone, create MILESTONES.md — one of the two is required
76
+ * --confirm REQUIRED to mutate (#3726): the archive is irreversible (ROADMAP/
77
+ * REQUIREMENTS archived, phase dirs MOVED, STATE.md rewritten), so
78
+ * without this flag the command refuses and mutates nothing
79
+ * --dry-run Preview what would move, mutates nothing (no --confirm needed; #2118)
72
80
  * [--name <name>]
73
81
  * [--no-archive-phases] Skip moving phase dirs to milestones/vX.Y-phases/ (archived by default)
74
82
  * [--archive-quick] Move .planning/quick/* dirs to milestones/vX.Y-quick/ + reset the
@@ -98,6 +106,15 @@
98
106
  * validate health [--repair] Check .planning/ integrity, optionally repair
99
107
  * validate agents Check GSD agent installation status
100
108
  *
109
+ * Planning Snapshot:
110
+ * planning inspect Read-only schema-v1 canonical planning snapshot
111
+ * (milestone identity, active phase, per-phase
112
+ * verification/roadmap-acceptance/UAT evidence kept
113
+ * separate, requirement rows with mapped-phase
114
+ * traceability, plan/task rows with planned+changed
115
+ * file provenance, and independent accepted_phases /
116
+ * completed_plans fractions). Takes no arguments.
117
+ *
101
118
  * Progress:
102
119
  * progress [json|table|bar] Render progress in various formats
103
120
  *
@@ -240,13 +257,22 @@ try {
240
257
  process.stderr.write((bootErr && bootErr.message ? bootErr.message : String(bootErr)) + '\n');
241
258
  // Fatal bootstrap failure before the CLI's ExitError/runMain machinery (which
242
259
  // lives in ./lib) is available to load, so a direct exit is the only option.
243
- // eslint-disable-next-line n/no-process-exit
260
+ // #3910: this call runs BEFORE ./lib/cli-exit.cjs is even required, so the
261
+ // registered-exit seam (runMain/ExitError/terminateNow) does not exist yet
262
+ // at this point in the process's lifetime — there is nothing to route
263
+ // through. This is the second (and only other) sanctioned allowlist entry
264
+ // for local/require-registered-exit, alongside terminateNow's own body.
265
+ // #3914: n/no-process-exit and local/require-registered-exit are
266
+ // complementary, not predecessor/successor (see eslint.config.mjs and
267
+ // docs/adr/3889-process-exit-contract.md) — both remain 'error' on this
268
+ // glob, so both need a disable directive here.
269
+ // eslint-disable-next-line n/no-process-exit, local/require-registered-exit
244
270
  process.exit(1);
245
271
  }
246
272
 
247
- const { ExitError, runMain } = require('./lib/cli-exit.cjs');
273
+ const { ExitError, runMain, resolveContractVersion } = require('./lib/cli-exit.cjs');
248
274
  const io = require('./lib/io.cjs');
249
- const { error, ERROR_REASON, setJsonErrorMode, output } = io;
275
+ const { error, ERROR_REASON, setJsonErrorMode, output, formatDiagnosticToken } = io;
250
276
  const projectRoot = require('./lib/project-root.cjs');
251
277
  // Resolve findProjectRoot lazily at call time rather than binding it at module
252
278
  // load. It is sourced from project-root.cjs; a call-time lookup is robust
@@ -286,6 +312,7 @@ const estimateCli = require('./lib/estimate-cli.cjs');
286
312
  const template = require('./lib/template.cjs');
287
313
  const milestone = require('./lib/milestone.cjs');
288
314
  const commands = require('./lib/commands.cjs');
315
+ const runtimeIdentity = require('./lib/runtime-identity.cjs');
289
316
  const init = require('./lib/init.cjs');
290
317
  const frontmatter = require('./lib/frontmatter.cjs');
291
318
  const workstream = require('./lib/workstream.cjs');
@@ -297,6 +324,7 @@ const { routeVerifyCommand } = require('./lib/verify-command-router.cjs');
297
324
  const { routeEvalCommand } = require('./lib/eval-command-router.cjs');
298
325
  const evalMod = require('./lib/eval.cjs');
299
326
  const { routeVerificationCommand } = require('./lib/verification-command-router.cjs');
327
+ const { routePlanningCommand } = require('./lib/planning-command-router.cjs');
300
328
  const verification = require('./lib/verification.cjs');
301
329
  const { routeInitCommand } = require('./lib/init-command-router.cjs');
302
330
  // Stale-bake guard (#1688): warns once when model config changed since agents
@@ -309,12 +337,17 @@ const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
309
337
  const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
310
338
  const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
311
339
  const { routeRoadmapCommand } = require('./lib/roadmap-command-router.cjs');
340
+ // #3676 (Phase 4, epic #3344): quick-batch is a first-party, always-on
341
+ // command family (like `/gsd:quick`) — wired directly into
342
+ // HOST_COMMAND_ROUTERS, not the opt-in capability-registry/`activationKey`
343
+ // path graphify uses.
344
+ const { routeQuickBatchCommand } = require('./lib/quick-batch-command-router.cjs');
312
345
  const { routeCapabilityCommand } = require('./lib/capability-command-router.cjs');
313
346
  const { routeAgentCommand, AGENT_FAILURE_CLASSES } = require('./lib/agent-command-router.cjs');
314
347
  const smartEntryMod = require('./lib/smart-entry.cjs');
315
348
  const { routeCheckCommand } = require('./lib/check-command-router.cjs');
316
349
  const { routeTaskCommand } = require('./lib/task-command-router.cjs');
317
- const { parseNamedArgs, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
350
+ const { parseNamedArgsOrExit, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
318
351
  const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
319
352
  const { getEffectiveAuthority, classifyDriftSeverity, comparePhaseStatus } = require('./lib/plan-drift-guard.cjs');
320
353
 
@@ -952,7 +985,16 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
952
985
 
953
986
  function routePrSubrepo({ args, cwd, raw, error }) {
954
987
  const message = args[1];
955
- const { repo, branch } = parseNamedArgs(args, ['repo', 'branch']);
988
+ // #3884: the commit message is an optional leading positional the
989
+ // caller owns (args[1]) — but when it is OMITTED, args[1] is itself
990
+ // the first flag (e.g. `--repo`), and a static `positionals: 2`
991
+ // treats that flag's own value as an unexpected trailing positional
992
+ // before cmdPrSubrepo's own "commit message required" guard ever
993
+ // runs. Widen the boundary only when args[1] genuinely looks like a
994
+ // message (not flag-shaped), mirroring the same fix applied to
995
+ // `state complete-phase`.
996
+ const messagePresent = message !== undefined && !message.startsWith('--');
997
+ const { repo, branch } = parseNamedArgsOrExit(args, { valueFlags: ['repo', 'branch'], positionals: messagePresent ? 2 : 1 }, error);
956
998
  commands.cmdPrSubrepo(cwd, repo, branch, message, raw);
957
999
  }
958
1000
 
@@ -969,7 +1011,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
969
1011
  template.cmdTemplateSelect(cwd, args[2], raw);
970
1012
  } else if (subcommand === 'fill') {
971
1013
  const templateType = args[2];
972
- const { phase, plan, name, type, wave, fields: fieldsRaw } = parseNamedArgs(args, ['phase', 'plan', 'name', 'type', 'wave', 'fields']);
1014
+ const { phase, plan, name, type, wave, fields: fieldsRaw } = parseNamedArgsOrExit(args, { valueFlags: ['phase', 'plan', 'name', 'type', 'wave', 'fields'], positionals: 3 }, error);
973
1015
  let fields = {};
974
1016
  if (fieldsRaw) {
975
1017
  const { safeJsonParse } = require('./lib/security.cjs');
@@ -1018,14 +1060,14 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1018
1060
  }
1019
1061
  // CJS fallback (SDK unavailable or unknown subcommand)
1020
1062
  if (subcommand === 'get') {
1021
- frontmatter.cmdFrontmatterGet(cwd, file, parseNamedArgs(args, ['field']).field, raw);
1063
+ frontmatter.cmdFrontmatterGet(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['field'], positionals: 3 }, error).field, raw);
1022
1064
  } else if (subcommand === 'set') {
1023
- const { field, value } = parseNamedArgs(args, ['field', 'value']);
1065
+ const { field, value } = parseNamedArgsOrExit(args, { valueFlags: ['field', 'value'], positionals: 3 }, error);
1024
1066
  frontmatter.cmdFrontmatterSet(cwd, file, field, value !== null ? value : undefined, raw);
1025
1067
  } else if (subcommand === 'merge') {
1026
- frontmatter.cmdFrontmatterMerge(cwd, file, parseNamedArgs(args, ['data']).data, raw);
1068
+ frontmatter.cmdFrontmatterMerge(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['data'], positionals: 3 }, error).data, raw);
1027
1069
  } else if (subcommand === 'validate') {
1028
- frontmatter.cmdFrontmatterValidate(cwd, file, parseNamedArgs(args, ['schema']).schema, raw);
1070
+ frontmatter.cmdFrontmatterValidate(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['schema'], positionals: 3 }, error).schema, raw);
1029
1071
  } else {
1030
1072
  error('Unknown frontmatter subcommand. Available: get, set, merge, validate', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1031
1073
  }
@@ -1069,6 +1111,15 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1069
1111
  commands.cmdCurrentTimestamp(args[1] || 'full', raw);
1070
1112
  }
1071
1113
 
1114
+ function routeRuntimeIdentity({ raw }) {
1115
+ // #3146: report this runtime's package identity so a shipped workflow can
1116
+ // tell whether it reached THIS package's gsd-tools or a colliding one.
1117
+ // Kept on the CJS fast path for the same reason as current-timestamp — the
1118
+ // launcher preamble spawns it once per workflow run, so SDK bridge startup
1119
+ // would be a per-run tax on every workflow.
1120
+ runtimeIdentity.cmdRuntimeIdentity(raw);
1121
+ }
1122
+
1072
1123
  function routeSkillsRoot({ args, raw, error }) {
1073
1124
  // #3024: resolve the global skills base directory for a runtime.
1074
1125
  // The sync-skills workflow previously shelled out to install.js --skills-root,
@@ -1134,16 +1185,72 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1134
1185
  commands.cmdVerifyPathExists(cwd, args[1], raw);
1135
1186
  }
1136
1187
 
1188
+ function routeQuickTasksMigrate({ cwd, raw }) {
1189
+ // #3730 (option b, maintainer decision 2026-09-02): the supported repair
1190
+ // path for a legacy Quick Tasks table GSD's own pre-registry prose created.
1191
+ // Silent no-op when the section is absent or the table is already canonical
1192
+ // — the quick/fast workflows call this before their first append, so the
1193
+ // migration runs exactly once, on the first quick run, unprompted otherwise.
1194
+ const statePath = path.join(cwd, '.planning', 'STATE.md');
1195
+ if (!fs.existsSync(statePath)) {
1196
+ output({ ok: true, migrated: false, reason: `STATE.md not found at ${statePath}` }, raw);
1197
+ return;
1198
+ }
1199
+ const { migrateQuickTasksTable } = require('./lib/markdown-table.cjs');
1200
+ let report;
1201
+ state.readModifyWriteStateMd(statePath, (content) => {
1202
+ const result = migrateQuickTasksTable(content);
1203
+ if (!result.ok) {
1204
+ throw new ExitError(1, `⚠ quick-tasks-migrate: ${result.reason}`);
1205
+ }
1206
+ report = result.value;
1207
+ return result.value.content;
1208
+ }, cwd, { resync: false });
1209
+ output({
1210
+ ok: true,
1211
+ migrated: report.migrated,
1212
+ ...(report.migrated ? { from: report.from, rows: report.rows } : {}),
1213
+ }, raw);
1214
+ }
1215
+
1137
1216
  function routeQuickTasksAppend({ args, cwd, raw, error }) {
1138
1217
  // #2133 / ADR-2143 §3,§7: schema-backed replacement for fast.md's inline
1139
1218
  // `awk NF-2` Quick Tasks column arithmetic. Row construction is delegated
1140
1219
  // to the pure appendQuickTaskRow (markdown-table.cjs); this case only
1141
1220
  // handles the I/O (read STATE.md, resolve date/commit, write STATE.md).
1142
1221
  const qtaArgs = args.slice(1);
1143
- const qtaTask = parseNamedArgs(qtaArgs, ['task']).task || args[1];
1222
+ // Ambiguous boundary (ADR-3473 §8.4 Item 2 note): this command accepts
1223
+ // EITHER a positional free-text description (qtaArgs[0]) OR --task
1224
+ // <value> — the same token index is a caller-owned positional in one
1225
+ // input shape and a flag in the other, which a single fixed
1226
+ // `positionals` cursor cannot represent. `positionals: 'rest'`
1227
+ // disables the boundary walk (as with `init quick`) so extraction
1228
+ // (used for the --task form) and the `|| args[1]` fallback (used for
1229
+ // the positional form) both keep working unchanged.
1230
+ // #3356 defect 1: `--quick-id` / `--slug` / `--directory` are
1231
+ // OPTIONAL widenings. A caller with no quick id or task directory
1232
+ // (fast.md, the original #2133 caller) omits them and keeps the
1233
+ // exact prior ordinal-`#`/`'—'`-Directory row. A caller that DOES
1234
+ // have a real quick id + task dir (i.e. can match `workflows/
1235
+ // quick.md`'s own Step 7c row for the same inputs) supplies them
1236
+ // and gets the byte-equivalent canonical row `quick.md:632`
1237
+ // documents — closing the false-equivalence gap `quick.md:627`
1238
+ // claims. `--directory` wins outright when given explicitly;
1239
+ // otherwise a supplied `--quick-id` + `--slug` pair derives the
1240
+ // canonical permalink the same way `workflows/quick.md` renders it.
1241
+ const qtaParsed = parseNamedArgsOrExit(
1242
+ qtaArgs,
1243
+ { valueFlags: ['task', 'quick-id', 'slug', 'directory'], positionals: 'rest' },
1244
+ error,
1245
+ );
1246
+ const qtaTask = qtaParsed.task || args[1];
1144
1247
  if (!qtaTask) {
1145
1248
  error('quick-tasks-append requires --task <description> (or a positional description)', ERROR_REASON.USAGE);
1146
1249
  }
1250
+ const qtaQuickId = qtaParsed['quick-id'] || undefined;
1251
+ const qtaSlug = qtaParsed['slug'] || undefined;
1252
+ const qtaDirectory = qtaParsed['directory']
1253
+ || (qtaQuickId && qtaSlug ? `[${qtaQuickId}-${qtaSlug}](./quick/${qtaQuickId}-${qtaSlug}/)` : undefined);
1147
1254
 
1148
1255
  const statePath = path.join(cwd, '.planning', 'STATE.md');
1149
1256
  if (!fs.existsSync(statePath)) {
@@ -1169,8 +1276,21 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1169
1276
  // still releases the lock before the throw propagates; the transform
1170
1277
  // throws before returning new content, so nothing is ever written).
1171
1278
  let mutation;
1279
+ // #3356 defect 2: this write touches only the Quick Tasks body
1280
+ // table — a single appended row — so it must not trigger the
1281
+ // default full re-derive of the disk-derived `progress.*`
1282
+ // frontmatter block. `{ resync: false }` mirrors every other
1283
+ // body-only STATE.md writer's convention (src/state.cts's own
1284
+ // docstring on `readModifyWriteStateMd` prescribes it); this route
1285
+ // was the lone outlier still passing no options at all.
1172
1286
  state.readModifyWriteStateMd(statePath, (content) => {
1173
- const result = appendQuickTaskRow(content, { description: qtaTask, date, commit });
1287
+ const result = appendQuickTaskRow(content, {
1288
+ description: qtaTask,
1289
+ date,
1290
+ commit,
1291
+ quickId: qtaQuickId,
1292
+ directory: qtaDirectory,
1293
+ });
1174
1294
  if (!result.ok) {
1175
1295
  // Mirrors fast.md's old "skip with a brief log" behaviour (#2133): this
1176
1296
  // is an expected, recoverable condition (no table / unrecognized
@@ -1181,7 +1301,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1181
1301
  }
1182
1302
  mutation = result.value;
1183
1303
  return result.value.content;
1184
- }, cwd);
1304
+ }, cwd, { resync: false });
1185
1305
 
1186
1306
  output({ ok: true, row: mutation.row, variant: mutation.variant }, raw, mutation.row);
1187
1307
  }
@@ -1202,7 +1322,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1202
1322
  const fsx = require('node:fs');
1203
1323
  const os = require('node:os');
1204
1324
  const { REVIEWER_LANES, mergeReviewerLanes } = require('./lib/review-lane-descriptor.cjs');
1205
- const { resolveLanePlan } = require('./lib/review-lane-invocation.cjs');
1325
+ const { resolveLanePlan, resolveLaneEffort } = require('./lib/review-lane-invocation.cjs');
1326
+ const modelCatalog = require('./lib/model-catalog.cjs');
1206
1327
  const runner = require('./lib/review-lane-runner.cjs');
1207
1328
  const cfgLoader = require('./lib/config-loader.cjs');
1208
1329
  const capabilityLoader = require('./lib/capability-loader.cjs');
@@ -1214,13 +1335,15 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1214
1335
  const sub = args[1];
1215
1336
  // Fail fast on an unrecognized subcommand. Without this check, `sub` fell through
1216
1337
  // to the `sub !== 'invoke'` usage-error branch far below (after loading the
1217
- // capability registry AND building a per-lane plan for every lane — which itself
1218
- // spawns one child `query resolve-execution` process per lane via `effortFor`,
1219
- // up to 12 subprocess spawns for the default lane set) before ever reporting the
1220
- // error. That made an invalid subcommand slow instead of instant, and under bench
1221
- // load (many sequential node spawns) `review-lane bogus` could exceed a caller's
1222
- // spawn timeout and be killed before writing anything to stderr — the CI-observed
1223
- // failure was empty stdout AND stderr, not the expected usage message (#3148).
1338
+ // capability registry AND building a per-lane plan for every lane) before ever
1339
+ // reporting the error. That made an invalid subcommand slow instead of instant, and
1340
+ // under bench load `review-lane bogus` could exceed a caller's spawn timeout and be
1341
+ // killed before writing anything to stderr — the CI-observed failure was empty stdout
1342
+ // AND stderr, not the expected usage message (#3148). The plan path used to be far
1343
+ // heavier still: it spawned one child `query resolve-execution` process PER LANE to
1344
+ // fetch effort, up to 12 subprocess spawns for the default set. #4255 resolves effort
1345
+ // in-process from the lane's own declaration, so that cost is gone; the fail-fast
1346
+ // check stays because building 12 plans is still work an unrecognized sub should skip.
1224
1347
  // `plan`/`invoke` are the only subs that need the expensive plan-building path
1225
1348
  // below; `sections`/`flags` return earlier still. Anything else errors here, before
1226
1349
  // any of that work starts.
@@ -1307,46 +1430,29 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1307
1430
  return;
1308
1431
  }
1309
1432
 
1310
- // Effort argv is resolved per lane by the host's own execution policy, through the SAME
1311
- // `resolve-execution` surface the bash legs used (`--host <slug>`), so the host's negotiated
1312
- // effortSurface still decides whether an argument is emitted and the catalog still owns the
1313
- // syntax (ADR-1239 #2481, ADR-443's escalation ladder). `cmdResolveExecution` writes to
1314
- // stdout and exits, so it cannot be called in-process for a value — this spawns the same
1315
- // bounded query the legs did, once per selected lane. A lane whose slug is not a known host
1316
- // resolves to no effort argument at all.
1433
+ // Effort argv is resolved from the LANE's own review configuration (#4255), then rendered
1434
+ // through the host's negotiated `effortSurface` so ADR-1239/#2481's trust-boundary invariant
1435
+ // still decides whether an argument is emitted at all and the catalog still owns the syntax.
1317
1436
  //
1318
- // NOT `--raw` and NOT `--pick` (#2295). `--raw` prints only the resolved EFFORT ('low') with
1319
- // no host-specific rendering at all. `--pick effort_argv_string` used to be the answer — the
1320
- // rendered array re-joined into a string ('-c model_reasoning_effort=low') — but the caller
1321
- // then had to `.split(/\s+/)` that string back apart to get an argv array, and re-splitting a
1322
- // string the callee just joined is a lossy round trip: any argv element that legitimately
1323
- // contains a space would come back split into two argv elements, corrupting the very argv it
1324
- // was rendered to preserve. Reading the UNPICKED object instead gives both `effort_argv` (a
1325
- // real string array, used verbatim, no re-splitting) and `effort_argv_value` (the bare level,
1326
- // #2295's `plan.effort`) from the one spawn.
1327
- const EMPTY_EFFORT = { argv: [], value: null };
1328
- const effortFor = (slug) => {
1437
+ // What this replaced: a `query resolve-execution gsd-plan-checker --host <slug>` spawn per
1438
+ // lane. The agent id was a hardcoded literal, so the `--host` argument chose only the argv
1439
+ // RENDERING while the LEVEL always came from the installed plan-checker's frontmatter — `low`
1440
+ // under every shipped model profile. Every prompt-fed reviewer therefore ran at a fast
1441
+ // structural verifier's effort, and because the rendered argument is a CLI config override it
1442
+ // silently beat the effort the operator had configured for that CLI. At `low` a large
1443
+ // source-grounded prompt makes a model end its turn with no final message, so the lane came
1444
+ // back empty and the stub read as a crash.
1445
+ //
1446
+ // `resolveLaneEffort` is pure and lives beside the other lane resolution; this closure only
1447
+ // injects the rendering, which needs the registry and the catalog.
1448
+ const renderLaneEffort = (host, level) => {
1329
1449
  try {
1330
- const r = cp.spawnSync(
1331
- process.execPath,
1332
- [__filename, 'query', 'resolve-execution', 'gsd-plan-checker', '--host', slug],
1333
- { cwd, encoding: 'utf8', timeout: 15000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 },
1334
- );
1335
- if (r.status !== 0) return EMPTY_EFFORT;
1336
- let parsed;
1337
- try {
1338
- parsed = JSON.parse(String(r.stdout || ''));
1339
- } catch { return EMPTY_EFFORT; }
1340
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return EMPTY_EFFORT;
1341
- const argv = Array.isArray(parsed.effort_argv)
1342
- ? parsed.effort_argv.filter((a) => typeof a === 'string' && a !== '')
1343
- : [];
1344
- const value = typeof parsed.effort_argv_value === 'string' && parsed.effort_argv_value
1345
- ? parsed.effort_argv_value
1346
- : null;
1347
- return { argv, value };
1348
- } catch { return EMPTY_EFFORT; }
1450
+ const surface = commands.effortSurfaceForHost(cwd, host);
1451
+ const r = modelCatalog.renderEffortArgv(host, level, surface);
1452
+ return { argv: Array.isArray(r && r.argv) ? r.argv : [], value: (r && r.value) || null };
1453
+ } catch { return { argv: [], value: null }; }
1349
1454
  };
1455
+ const effortFor = (lane) => resolveLaneEffort(lane, configGet, renderLaneEffort);
1350
1456
 
1351
1457
  /**
1352
1458
  * Per-lane prompt budget (#2797 semantics, preserved exactly).
@@ -1378,7 +1484,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1378
1484
  // so losing all of them to one bad manifest is strictly worse. Belt and braces on purpose.
1379
1485
  let r;
1380
1486
  try {
1381
- const effort = effortFor(slug);
1487
+ const effort = effortFor(lane);
1382
1488
  r = resolveLanePlan({ lane, configGet, runDir, repoRoot, effortArgs: effort.argv, effortValue: effort.value });
1383
1489
  } catch (e) {
1384
1490
  return { slug, ok: false, reason: 'malformed_lane', detail: `resolver threw: ${e && e.message ? e.message : String(e)}` };
@@ -1538,7 +1644,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1538
1644
  `model override (it declares no modelConfigKey). The review will use the CLI's own default.\n`,
1539
1645
  );
1540
1646
  }
1541
- const instanceEffort = effortFor(entry.slug);
1647
+ const instanceEffort = effortFor(lane);
1542
1648
  const overridden = resolveLanePlan({
1543
1649
  lane,
1544
1650
  configGet: (k) => (key && k === key ? instanceModel : configGet(k)),
@@ -1669,6 +1775,31 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1669
1775
  }
1670
1776
  }
1671
1777
 
1778
+ /**
1779
+ * #3737 — strict read of the project-level worktree opt-out from
1780
+ * `.planning/config.json`. True ONLY when `workflow.use_worktrees` is the
1781
+ * boolean `false`; an absent key, unreadable/malformed config, or any
1782
+ * non-boolean value (including the string "false") degrades to false —
1783
+ * worktrees are ON by default, so the degraded answer is "not opted out".
1784
+ * Direct file read, deliberately NOT loadConfig: this resolver backs
1785
+ * sentinel writes and must never trigger config normalization/rewrites
1786
+ * (same discipline as resolveDispatchIsolationDecision's resolveRuntime
1787
+ * comment above). Never throws.
1788
+ */
1789
+ function projectWorktreesOptedOut(cwd) {
1790
+ // #3972: single owner — planning-workspace's worktreesOptedOut ladder
1791
+ // (scoped own-key, root inheritance under the ws gate, strict === false).
1792
+ // Kept as a local name so routeDispatchIsolation's call sites read the
1793
+ // same as they did in #3938/#3963; the logic itself now lives beside
1794
+ // planningDir/planningRoot where every isolation surface can share it.
1795
+ try {
1796
+ const { worktreesOptedOut } = require('./lib/planning-workspace.cjs');
1797
+ return worktreesOptedOut(cwd);
1798
+ } catch {
1799
+ return false;
1800
+ }
1801
+ }
1802
+
1672
1803
  function routeDispatchIsolation({ args, cwd, raw, error }) {
1673
1804
  // #2584 Phase 3 (#2627): typed query exposing the negotiated
1674
1805
  // `dispatch.isolation` to the execute-phase wave scheduler, so the
@@ -1742,6 +1873,25 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1742
1873
  ? args[planIdx + 1]
1743
1874
  : null;
1744
1875
 
1876
+ // #3737: the project-level opt-out (workflow.use_worktrees === false) is
1877
+ // decided HERE, before the sentinel write — not only in the workflow
1878
+ // shell blocks that run after this resolve. Pre-fix, any plain re-query
1879
+ // (config re-read, wave transition, second plan dispatch) re-persisted
1880
+ // the naturally-resolved host capability over the `--force-isolation
1881
+ // none` record the dispatch-isolation reference mandates, and the guard
1882
+ // then denied the sequential dispatch the config explicitly asked for.
1883
+ // Applied AFTER --force-isolation so the documented rule holds: the
1884
+ // opt-out wins on every host, over both the natural resolution and any
1885
+ // force. Strict `=== false`: the default is worktrees ON, so an absent
1886
+ // key, an unreadable/malformed config, or a non-boolean value degrades
1887
+ // to "not opted out" (mirrors readConfigJsonBoolean's no-coercion
1888
+ // discipline in lib/init.cjs).
1889
+ if (projectWorktreesOptedOut(cwd)) {
1890
+ isolation = 'none';
1891
+ harnessFlag = null;
1892
+ exec = null;
1893
+ }
1894
+
1745
1895
  // Side-effect write (#3045 CORE REDESIGN) — see the doc comment above.
1746
1896
  // Never allowed to affect this query's own stdout contract or throw.
1747
1897
  try {
@@ -1758,6 +1908,261 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1758
1908
  }
1759
1909
  }
1760
1910
 
1911
+ /**
1912
+ * #3673 (ADR-1239 Phase 1) — typed, PURE-READ query exposing the negotiated
1913
+ * `dispatch.maxConcurrency` numeric sub-field. Sibling of `dispatch-isolation`
1914
+ * above, but — per the #3673 design doc's explicit rejection of a write path
1915
+ * for this query — writes NOTHING: no sentinel file, no side effect at all.
1916
+ * A read-only capacity lookup for a later (Phase 4) scheduler to consult.
1917
+ *
1918
+ * Precedence:
1919
+ * 1. GSD_DISPATCH_MAX_CONCURRENCY, if it parses as a positive safe
1920
+ * integer → source: "live". A malformed value (non-numeric, zero,
1921
+ * negative, fractional) is treated exactly as if the variable were
1922
+ * unset — it falls through to tier 2, never errors.
1923
+ * 2. registry.runtimes[id].runtime.hostIntegration.dispatch.maxConcurrency,
1924
+ * if it is a positive safe integer → source: "descriptor". The
1925
+ * "undocumented" sentinel and any other malformed shape fall through
1926
+ * to tier 3.
1927
+ * 3. 1 (the fail-closed floor) → source: "fallback".
1928
+ *
1929
+ * `isPositiveSafeInteger`, required below from `./lib/host-integration.cjs`,
1930
+ * is the SAME exported predicate src/host-integration.cts's
1931
+ * negotiateHostCapabilities applies to the descriptor value — applied
1932
+ * identically to both the env value and the descriptor value here so the
1933
+ * two tiers can never silently disagree on what counts as valid.
1934
+ *
1935
+ * `reason` names why the WINNING tier (or the fallback) was chosen: "ok" on
1936
+ * the happy path (live or descriptor honored), else "missing" (descriptor
1937
+ * omitted the field), "undocumented" (descriptor sentinel), "non_integer"
1938
+ * (fractional or NaN), "unsafe_integer" (integer but outside
1939
+ * Number.isSafeInteger's range), "non_positive" (zero or negative),
1940
+ * "non_numeric" (string/object/array/null/boolean), or "unknown_runtime"
1941
+ * (no registry entry for the resolved runtime id — resolution itself never
1942
+ * throws; failure fails closed to the same fallback tier).
1943
+ *
1944
+ * Output:
1945
+ * --raw → prints exactly one positive integer, nothing else
1946
+ * --json → prints { runtime, capacity, declared, source, reason }
1947
+ * default → same as --raw
1948
+ */
1949
+ function routeDispatchCapacity({ args, cwd, raw }) {
1950
+ const { UNDOCUMENTED, isPositiveSafeInteger } = require('./lib/host-integration.cjs');
1951
+
1952
+ let runtimeId = null;
1953
+ let runtimeEntry = null;
1954
+ try {
1955
+ const { resolveRuntime } = require('./lib/runtime-slash.cjs');
1956
+ runtimeId = resolveRuntime(cwd);
1957
+ const registry = require('./lib/capability-registry.cjs');
1958
+ runtimeEntry = registry.runtimes != null ? registry.runtimes[runtimeId] : null;
1959
+ } catch {
1960
+ runtimeEntry = null;
1961
+ }
1962
+ const declared = runtimeEntry?.runtime?.hostIntegration?.dispatch?.maxConcurrency ?? null;
1963
+
1964
+ let liveValue = null;
1965
+ const rawEnv = process.env.GSD_DISPATCH_MAX_CONCURRENCY;
1966
+ if (typeof rawEnv === 'string' && rawEnv.trim().length > 0) {
1967
+ const parsed = Number(rawEnv.trim());
1968
+ if (isPositiveSafeInteger(parsed)) {
1969
+ liveValue = parsed;
1970
+ }
1971
+ }
1972
+
1973
+ let capacity;
1974
+ let source;
1975
+ let reason;
1976
+ if (liveValue !== null) {
1977
+ capacity = liveValue;
1978
+ source = 'live';
1979
+ reason = 'ok';
1980
+ } else if (runtimeEntry == null) {
1981
+ capacity = 1;
1982
+ source = 'fallback';
1983
+ reason = 'unknown_runtime';
1984
+ } else if (declared === UNDOCUMENTED) {
1985
+ capacity = 1;
1986
+ source = 'fallback';
1987
+ reason = 'undocumented';
1988
+ } else if (isPositiveSafeInteger(declared)) {
1989
+ capacity = declared;
1990
+ source = 'descriptor';
1991
+ reason = 'ok';
1992
+ } else if (declared === null || declared === undefined) {
1993
+ capacity = 1;
1994
+ source = 'fallback';
1995
+ reason = 'missing';
1996
+ } else if (typeof declared !== 'number') {
1997
+ capacity = 1;
1998
+ source = 'fallback';
1999
+ reason = 'non_numeric';
2000
+ } else if (!Number.isSafeInteger(declared)) {
2001
+ capacity = 1;
2002
+ source = 'fallback';
2003
+ reason = Number.isInteger(declared) ? 'unsafe_integer' : 'non_integer';
2004
+ } else {
2005
+ capacity = 1;
2006
+ source = 'fallback';
2007
+ reason = 'non_positive';
2008
+ }
2009
+
2010
+ if (args.indexOf('--json') !== -1) {
2011
+ output({ runtime: runtimeId, capacity, declared, source, reason }, raw);
2012
+ } else {
2013
+ process.stdout.write(String(capacity));
2014
+ }
2015
+ }
2016
+
2017
+ // #3714 follow-up — the dispatch seam gated only on PRESENCE of an explicit
2018
+ // pin, never on its VALUE, so an Anthropic-flavored global default
2019
+ // (~/.gsd/defaults.json model_overrides["gsd-executor"] = "sonnet"/"opus"/
2020
+ // "claude-*") reached `codex exec --model sonnet`: the documented #2310/
2021
+ // #2311 400 on a passive-posture host (ADR-1239/ADR-2313). It also let a
2022
+ // repo-committed .planning/config.json inject shell-hostile argv (a
2023
+ // `-c approval_policy=never` suffix, `$(...)`/`;` command injection,
2024
+ // embedded control characters) straight onto exec's argv.
2025
+ //
2026
+ // This mirrors — deliberately, not by re-derivation — the same VALUE
2027
+ // policy bin/install.js's generateCodexAgentToml() already applies to the
2028
+ // identical model_overrides["gsd-executor"] config key for the .toml
2029
+ // surface (bin/install.js ~3983-4046): trim; a whitespace-only value drops
2030
+ // silently (#3241, no warning); an Anthropic-flavored value
2031
+ // (isAnthropicFlavoredModel, single-sourced on bin/lib/model-catalog.cjs
2032
+ // per #3241 specifically so it cannot diverge across Codex-posture
2033
+ // surfaces) drops WITH a warning; a real pin survives verbatim. Two
2034
+ // additions beyond the .toml surface, both specific to this seam: the
2035
+ // 'inherit' sentinel (case/whitespace-insensitive) is a no-op here already
2036
+ // and must stay one, and a value that doesn't look like a model id at all
2037
+ // (the injection case above — the .toml surface never had to consider this
2038
+ // because TOML string-quoting isn't a shell argv boundary) is dropped with
2039
+ // a warning rather than ever reaching child_process argv.
2040
+ // Single source of truth for the model-id "allowed characters" notion
2041
+ // (#3714 follow-up — "Generative Fix Divergence"): the accept regex
2042
+ // (MODEL_ID_CHARSET_RE, used to ADMIT a pin) and the sanitizer keep-class
2043
+ // (MODEL_ID_SANITIZE_STRIP_RE, used to RENDER a rejected pin into a
2044
+ // warning) are both derived from this one character-class body so they
2045
+ // cannot drift apart again the way they already did once (the '@' added
2046
+ // for Vertex pins landed in the accept regex but not the sanitizer,
2047
+ // rendering "text-bison@002" as "text-bison?002" in the warning). '@' is
2048
+ // included for Vertex model-version pins ("text-bison@002",
2049
+ // "chat-bison@001"), which are legitimate model ids reachable through a
2050
+ // custom model_provider.
2051
+ // This body is interpolated raw into BOTH a positive character class
2052
+ // (MODEL_ID_CHARSET_RE, `[BODY]`) and a negated one
2053
+ // (MODEL_ID_SANITIZE_STRIP_RE, `[^BODY]`) below — only plain characters
2054
+ // and `x-y` ranges are safe here. A class metacharacter (`^`, `]`, `\`)
2055
+ // would mean different things in the two derived regexes if ever added.
2056
+ const MODEL_ID_CHARSET_BODY = 'A-Za-z0-9._:/@-';
2057
+ // The first character must be alphanumeric (#3714 hardening): a leading
2058
+ // '.', '_', ':', '/', or '@' has no legitimate model-id use case and, for
2059
+ // resolveOrchestratorExec's documented role as a GENERAL descriptor→argv
2060
+ // seam other hosts may adopt, a leading '@' or '/' is exactly the shape an
2061
+ // @-response-file or /-switch parser would key on. The leading-dash shape
2062
+ // is enforced separately by LEADING_DASH_RE below — it is NOT relaxed
2063
+ // here, since a flag-shaped value ("-c", "--config") must still fail the
2064
+ // resolver's own unsafe_leading_dash_model guard path via that dedicated
2065
+ // check, not this charset.
2066
+ const MODEL_ID_CHARSET_RE = new RegExp(`^[A-Za-z0-9][${MODEL_ID_CHARSET_BODY}]*$`);
2067
+ // Keep-class for sanitizing a REJECTED pin before it reaches the warning
2068
+ // (a guaranteed-reachable raw-to-TTY sink — the dispatch step runs with no
2069
+ // `2>` redirect). Built from the same MODEL_ID_CHARSET_BODY as the accept
2070
+ // regex above, so every character the matcher accepts also survives the
2071
+ // sanitizer unchanged, and a widened charset can never diverge from its
2072
+ // rendering again.
2073
+ //
2074
+ // This `g`-flagged instance is for internal `.replace()` use ONLY — a
2075
+ // `/g` regex is stateful (`.lastIndex` persists across calls) and
2076
+ // `.test()` on it alternates true/false/true across repeated calls on the
2077
+ // same string, a false-green trap for any test that reaches for `.test()`
2078
+ // instead of `.replace()`. To make that trap impossible rather than just
2079
+ // documenting it, this `g`-flagged object is never exported; the exported
2080
+ // `MODEL_ID_SANITIZE_STRIP_RE` below is a separate, non-global instance
2081
+ // built from the same body, safe for `.test()`/`.match()` in tests.
2082
+ const MODEL_ID_SANITIZE_STRIP_RE_G = new RegExp(`[^${MODEL_ID_CHARSET_BODY}]`, 'g');
2083
+ // Non-global companion of MODEL_ID_SANITIZE_STRIP_RE_G, exported for
2084
+ // tests. Do not use with `.replace()` on a value containing more than one
2085
+ // disallowed character — it only replaces the first match. Production
2086
+ // code must use the `g`-flagged instance above instead.
2087
+ const MODEL_ID_SANITIZE_STRIP_RE = new RegExp(`[^${MODEL_ID_CHARSET_BODY}]`);
2088
+ // A model id has no legitimate reason to be long; this also keeps a
2089
+ // pathological pin away from the Windows argv ceiling (execFileSync aborts
2090
+ // if argv > 32,767 chars — CLAUDE.md "Windows ARGV Overflow"). A pin over
2091
+ // this length is DROPPED WITH A WARNING like every other rejection, never
2092
+ // truncated into argv — a truncated model id is a different model id.
2093
+ const MODEL_ID_MAX_LENGTH = 200;
2094
+ const _dispatchModelPinDropWarned = new Set();
2095
+ function _warnDispatchModelPinDropped(agentName, rawValue, reason) {
2096
+ const key = `${agentName}::${rawValue}::${reason}`;
2097
+ if (_dispatchModelPinDropWarned.has(key)) return;
2098
+ _dispatchModelPinDropWarned.add(key);
2099
+ // Sanitize BEFORE truncating: every value that reaches this warning
2100
+ // failed the model-id charset test by definition (or, for the
2101
+ // over-length case, still only ever contains charset-legal bytes) —
2102
+ // sanitizing first catches raw control/escape bytes (ESC, BEL, CSI
2103
+ // sequences) using the identity-sanitizing pattern already used for
2104
+ // --as at gsd-tools.cjs:1526. Sanitizing before truncating also ensures
2105
+ // a truncated escape sequence can never survive (e.g. an SGR sequence
2106
+ // cut before its reset, leaving sticky terminal state) — truncation
2107
+ // only ever cuts already-safe characters.
2108
+ const sanitized = String(rawValue).replace(MODEL_ID_SANITIZE_STRIP_RE_G, '?');
2109
+ const safe = sanitized.length > 64 ? `${sanitized.slice(0, 64)}…` : sanitized;
2110
+ process.stderr.write(
2111
+ `gsd: warning — dispatch model pin for agent "${agentName}" (value "${safe}") ${reason}; ` +
2112
+ `dropping it so the spawned executor falls back to the session model.\n`,
2113
+ );
2114
+ }
2115
+ // A value starting with '-' (or '--') is a flag/option shape, not a model
2116
+ // id — `-c`, `--config`, `-`, `--`, `-p` are unsafe to hand to
2117
+ // resolveOrchestratorExec, whose own `unsafe_leading_dash_model` guard
2118
+ // rejects them and fails the WHOLE resolution to `{ ok: false }` ->
2119
+ // exec:null -> a FATAL wave abort (executor-isolation-dispatch.md:299-303),
2120
+ // even on hosts (e.g. kimi-code) that declare no modelFlag at all and
2121
+ // previously ignored the pin entirely. This check MUST run BEFORE
2122
+ // MODEL_ID_CHARSET_RE below: the charset is anchored to `[A-Za-z0-9]` at
2123
+ // the first character, so every dash-leading value already fails the
2124
+ // charset test and would otherwise be swallowed by the generic "unsafe
2125
+ // characters" message, losing the more specific and more actionable
2126
+ // flag/option diagnosis. Reject here, at the VALUE-policy layer, so a
2127
+ // leading-dash value degrades to "no model" like every other rejected
2128
+ // shape, instead of reaching a resolver whose failure mode is fatal
2129
+ // rather than a graceful drop.
2130
+ const LEADING_DASH_RE = /^-/;
2131
+ /**
2132
+ * Resolve the VALUE policy for an explicit dispatch model pin. `rawValue`
2133
+ * is whatever resolveAgentModelOverride(..., null) returned — a string
2134
+ * pin, the 'inherit' sentinel, or null/''/undefined for "no explicit pin".
2135
+ * Returns the trimmed model string to embed, or `undefined` to emit no
2136
+ * --model flag at all. Never throws; never fails closed to an error —
2137
+ * every rejection degrades to "no model" (drop-and-warn), matching the
2138
+ * documented desired behavior of falling back to the session model rather
2139
+ * than aborting the wave.
2140
+ */
2141
+ function resolveDispatchModelPin(agentName, rawValue) {
2142
+ if (typeof rawValue !== 'string') return undefined; // not a string -> no model
2143
+ const trimmed = rawValue.trim();
2144
+ if (trimmed === '') return undefined; // whitespace-only -> no model, no warning (#3241)
2145
+ if (trimmed.toLowerCase() === 'inherit') return undefined; // sentinel -> no model, no warning
2146
+ const { isAnthropicFlavoredModel } = require('./lib/model-catalog.cjs');
2147
+ if (isAnthropicFlavoredModel(trimmed)) {
2148
+ _warnDispatchModelPinDropped(agentName, rawValue, 'is an Anthropic-flavored model/alias, not a valid Codex model');
2149
+ return undefined;
2150
+ }
2151
+ if (LEADING_DASH_RE.test(trimmed)) {
2152
+ _warnDispatchModelPinDropped(agentName, rawValue, 'looks like a flag/option, not a model id (leading "-")');
2153
+ return undefined;
2154
+ }
2155
+ if (!MODEL_ID_CHARSET_RE.test(trimmed)) {
2156
+ _warnDispatchModelPinDropped(agentName, rawValue, 'does not look like a model id (unsafe characters)');
2157
+ return undefined;
2158
+ }
2159
+ if (trimmed.length > MODEL_ID_MAX_LENGTH) {
2160
+ _warnDispatchModelPinDropped(agentName, rawValue, `exceeds the maximum model id length (${MODEL_ID_MAX_LENGTH} characters)`);
2161
+ return undefined;
2162
+ }
2163
+ return trimmed;
2164
+ }
2165
+
1761
2166
  const DISPATCH_ISOLATION_VOCABULARY = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
1762
2167
 
1763
2168
  /**
@@ -1821,14 +2226,67 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1821
2226
  const promptIdx = args.indexOf('--prompt');
1822
2227
  const promptArg = promptIdx !== -1 ? args[promptIdx + 1] : undefined;
1823
2228
  const hostIntegration = require('./lib/host-integration.cjs');
2229
+ // #3714: resolve an EXPLICIT, non-sentinel per-agent model pin for the
2230
+ // spawned worktree executor. Passing `null` as the runtime resolver
2231
+ // (3rd arg) is LOAD-BEARING, not an oversight — it is what keeps
2232
+ // profile/tier-derived models out of argv. Codex's `modelMode: passive`
2233
+ // posture (ADR-1239) and ADR-2313 forbid GSD driving model selection on
2234
+ // this host; only an operator's EXPLICIT override may cross this seam.
2235
+ // resolveAgentModelOverride(..., null) returns a value ONLY when the
2236
+ // operator pinned one explicitly (measured: unpinned -> null,
2237
+ // "inherit" -> "inherit" meaning "use the ambient session model, don't
2238
+ // pass a flag", "" -> null, profile-only -> null). Do NOT swap in
2239
+ // resolve-model / a full model-resolver here: that resolver falls back
2240
+ // to a default (e.g. "sonnet") for the unpinned/profile-only cases,
2241
+ // and emitting that on Codex's exec argv is exactly the documented
2242
+ // #2310/#2311 regression (a model unknown to Codex forced into a
2243
+ // passive-posture host).
2244
+ //
2245
+ // Presence of a pin is necessary but not sufficient: resolveDispatchModelPin
2246
+ // applies the same VALUE policy the install-side .toml surface already
2247
+ // applies to this config key (trim / drop-inherit / drop-Anthropic-flavored
2248
+ // with a warning / drop-non-model-id-charset with a warning) so a global
2249
+ // Anthropic-flavored default or an injected config value never reaches argv.
2250
+ //
2251
+ // This whole VALUE policy — including its warning — is gated on the
2252
+ // resolved runtime's descriptor actually declaring a non-empty
2253
+ // `modelFlag`. The policy runs at this host-NEUTRAL site, so a host
2254
+ // with no modelFlag at all (kimi, kimi-code, opencode) was never
2255
+ // going to emit a --model regardless of the pin's value; running the
2256
+ // policy anyway produced a stderr warning claiming "dropping it so
2257
+ // the spawned executor falls back to the session model" on every
2258
+ // dispatch for such a host — misleading today, and actively wrong if
2259
+ // a Claude-capable host ever declares a modelFlag. When the
2260
+ // descriptor declares no modelFlag, skip the policy entirely: no
2261
+ // model, no warning, argv byte-identical to before this pin policy
2262
+ // existed.
2263
+ const declaresModelFlag = typeof runtimeEntry?.runtime?.orchestratorExec?.modelFlag === 'string' &&
2264
+ runtimeEntry.runtime.orchestratorExec.modelFlag.length > 0;
2265
+ let model;
2266
+ if (declaresModelFlag) {
2267
+ const { readGsdEffectiveModelOverrides, resolveAgentModelOverride } =
2268
+ require('./lib/install-model-override-resolver.cjs');
2269
+ const pinned = resolveAgentModelOverride(
2270
+ 'gsd-executor', readGsdEffectiveModelOverrides(cwd), null);
2271
+ model = resolveDispatchModelPin('gsd-executor', pinned);
2272
+ }
1824
2273
  const resolution = hostIntegration.resolveOrchestratorExec(
1825
2274
  runtimeEntry?.runtime?.orchestratorExec,
1826
2275
  cwdTarget,
1827
2276
  promptArg,
2277
+ model,
1828
2278
  );
1829
- // A host declaring orchestrator-worktree whose exec descriptor does
1830
- // not resolve cannot be spawned — degrade to sequential rather than
1831
- // hand the scheduler an unusable command.
2279
+ // A host declaring orchestrator-worktree whose exec descriptor does not
2280
+ // resolve halts THIS wave's dispatch: isolation is forced to 'none' here,
2281
+ // and executor-isolation-dispatch.md:299-303 treats a null exec as FATAL
2282
+ // (exit 1) after the worktree has already been created — it does not
2283
+ // degrade to sequential execution. resolveDispatchModelPin rejects any
2284
+ // leading-dash value (flag/option shape) before it ever reaches this
2285
+ // resolver specifically so it cannot trip the resolver's own
2286
+ // `unsafe_leading_dash_model` guard and turn a bad config value into
2287
+ // this fatal path; every other unresolvable model value likewise
2288
+ // degrades to "no --model" (session model fallback) rather than to
2289
+ // resolution.ok === false.
1832
2290
  if (resolution.ok) {
1833
2291
  exec = { command: resolution.command, args: resolution.args, cwd: resolution.cwd };
1834
2292
  } else {
@@ -2137,8 +2595,19 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
2137
2595
  .filter((t) => t.length > 0);
2138
2596
  termsOverride = list.length > 0 ? { pluralization: list } : undefined;
2139
2597
  }
2598
+ // An unresolvable phase section is not a negative verdict. Feeding
2599
+ // `''` to the detector reported "examined, found nothing" for a
2600
+ // probe that never had input — no ROADMAP.md, or a phase number
2601
+ // absent from it, both read as a confident `detected:false`
2602
+ // (ADR-3889 failure class (c), #3909). Exit stays 0: this is an
2603
+ // ADR-2980 degraded result carried in the payload, and ADR-3889 P8
2604
+ // pins the gsd-tools exit projection at v1.
2140
2605
  const section = roadmap.getRoadmapPhaseWithFallback(cwd, phaseNum);
2141
- const result = detectAssumptionDelta(section ?? '', termsOverride);
2606
+ if (typeof section !== 'string' || section.trim() === '') {
2607
+ output({ skipped: true, reason: 'phase_unresolved' }, raw);
2608
+ return;
2609
+ }
2610
+ const result = detectAssumptionDelta(section, termsOverride);
2142
2611
  output(result, raw);
2143
2612
  return;
2144
2613
  }
@@ -2185,7 +2654,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
2185
2654
  // --no-archive-phases' inverted shape, absence of this flag means
2186
2655
  // "do nothing" rather than "skip a default-on behavior".
2187
2656
  const archiveQuick = args.includes('--archive-quick');
2188
- milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun, archiveQuick }, raw);
2657
+ // #3726: explicit mutation opt-in — without --confirm (and without
2658
+ // --dry-run) the command refuses before touching anything. Distinct
2659
+ // from --force, which bypasses the narrow scope guards only.
2660
+ const confirm = args.includes('--confirm');
2661
+ milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun, archiveQuick, confirm }, raw);
2189
2662
  } else if (subcommand === 'archive-quick') {
2190
2663
  // #2142 escalation: narrow archival-only entry point (does NOT
2191
2664
  // touch ROADMAP/REQUIREMENTS/MILESTONES.md, runs no completion
@@ -2207,11 +2680,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
2207
2680
  const subcommand = args[1];
2208
2681
  if (subcommand === 'render-checkpoint') {
2209
2682
  const uat = require('./lib/uat.cjs');
2210
- const options = parseNamedArgs(args, ['file']);
2683
+ const options = parseNamedArgsOrExit(args, { valueFlags: ['file'], positionals: 2 }, error);
2211
2684
  uat.cmdRenderCheckpoint(cwd, options, raw);
2212
2685
  } else if (subcommand === 'classify-coverage') {
2213
2686
  const coverage = require('./lib/coverage.cjs');
2214
- const options = parseNamedArgs(args, ['summary', 'file']);
2687
+ const options = parseNamedArgsOrExit(args, { valueFlags: ['summary', 'file'], positionals: 2 }, error);
2215
2688
  coverage.cmdClassify(cwd, options, raw);
2216
2689
  } else {
2217
2690
  error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage', ERROR_REASON.SDK_UNKNOWN_COMMAND);
@@ -2226,7 +2699,20 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
2226
2699
  function routeTodo({ args, cwd, raw, error }) {
2227
2700
  const subcommand = args[1];
2228
2701
  if (subcommand === 'complete') {
2229
- commands.cmdTodoComplete(cwd, args[2], raw);
2702
+ // #4096: this verb's flag surface is closed. `--dry-run` is parsed
2703
+ // and plumbed (mirroring routeMilestone / #2118), and any OTHER
2704
+ // flag fails loudly instead of being silently ignored — an
2705
+ // accepted-but-ignored safety flag converts a deliberate preview
2706
+ // into the mutation it was meant to avoid. (`--raw` is spliced by
2707
+ // the dispatcher before routing; it stays in the set as a guard
2708
+ // against that splice ever moving.)
2709
+ const TODO_COMPLETE_KNOWN_FLAGS = new Set(['--dry-run', '--raw']);
2710
+ for (const a of args.slice(3)) {
2711
+ if (typeof a === 'string' && a.startsWith('-') && !TODO_COMPLETE_KNOWN_FLAGS.has(a)) {
2712
+ error(`Unknown flag for todo complete: ${a}`, ERROR_REASON.USAGE);
2713
+ }
2714
+ }
2715
+ commands.cmdTodoComplete(cwd, args[2], { dryRun: args.includes('--dry-run') }, raw);
2230
2716
  } else if (subcommand === 'match-phase') {
2231
2717
  commands.cmdTodoMatchPhase(cwd, args[2], raw);
2232
2718
  } else {
@@ -2236,8 +2722,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
2236
2722
 
2237
2723
  function routeScaffold({ args, cwd, raw, error }) {
2238
2724
  const scaffoldType = args[1];
2725
+ // `--name` is multi-word (consumed separately by parseMultiwordArg,
2726
+ // below) — a token count the single-token-per-flag boundary walk
2727
+ // cannot represent. `positionals: 'rest'` disables that walk for
2728
+ // this call, matching the existing (unchanged) permissive behavior
2729
+ // for --name; --phase extraction is unaffected either way.
2239
2730
  const scaffoldOptions = {
2240
- phase: parseNamedArgs(args, ['phase']).phase,
2731
+ phase: parseNamedArgsOrExit(args, { valueFlags: ['phase'], positionals: 'rest' }, error).phase,
2241
2732
  name: parseMultiwordArg(args, 'name'),
2242
2733
  };
2243
2734
  commands.cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
@@ -2441,9 +2932,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
2441
2932
  );
2442
2933
  }
2443
2934
  } catch (e) {
2935
+ // ADR-3889: error() now throws ExitError instead of calling
2936
+ // process.exit(1) directly, so an ExitError raised by error() INSIDE
2937
+ // this try (e.g. the "Unknown windows subcommand" call above, or one
2938
+ // inside cmdWindowsStatus/Append/Waive/MarkFixed) lands HERE instead of
2939
+ // terminating uncatchably. It must be re-thrown unconditionally, before
2940
+ // the WindowsError name check below, or it falls through to the
2941
+ // generic branch and gets re-wrapped with a wrong message/reason,
2942
+ // discarding the original exit code.
2943
+ if (e instanceof ExitError) throw e;
2444
2944
  // WindowsError carries a REASON code; surface it through the structured
2445
- // error path so tests can assert on the typed reason. `error()` calls
2446
- // process.exit(1) internally so we never reach the fall-through.
2945
+ // error path so tests can assert on the typed reason.
2447
2946
  if (e && e.name === 'WindowsError' && typeof e.reason === 'string') {
2448
2947
  error(e.message || 'broken-windows error', e.reason);
2449
2948
  }
@@ -3712,14 +4211,19 @@ const HOST_COMMAND_ROUTERS = {
3712
4211
  'verification': routeVerification,
3713
4212
  'generate-slug': routeGenerateSlug,
3714
4213
  'current-timestamp': routeCurrentTimestamp,
4214
+ 'runtime-identity': routeRuntimeIdentity,
3715
4215
  'project-instruction-file': routeProjectInstructionFile,
3716
4216
  'list-todos': routeListTodos,
3717
4217
  'list-seeds': routeListSeeds,
3718
4218
  'verify-path-exists': routeVerifyPathExists,
3719
4219
  'quick-tasks-append': routeQuickTasksAppend,
4220
+ 'quick-tasks-migrate': routeQuickTasksMigrate,
4221
+ // #3676 (Phase 4, epic #3344): quick-batch coordination verbs.
4222
+ 'quick-batch': routeQuickBatchCommand,
3720
4223
  'normalize-test-command': routeNormalizeTestCommand,
3721
4224
  'dispatch-should-flatten': routeDispatchShouldFlatten,
3722
4225
  'dispatch-isolation': routeDispatchIsolation,
4226
+ 'dispatch-capacity': routeDispatchCapacity,
3723
4227
  'inspect-dispatch-isolation': routeInspectDispatchIsolation,
3724
4228
  'record-dispatch-isolation': routeRecordDispatchIsolation,
3725
4229
  'resolve-dispatch-type': routeResolveDispatchType,
@@ -3728,6 +4232,10 @@ const HOST_COMMAND_ROUTERS = {
3728
4232
  'skill-manifest': routeSkillManifest,
3729
4233
  'history-digest': routeHistoryDigest,
3730
4234
  'phases': routePhases,
4235
+ // #2790: read-only schema-v1 planning snapshot. The router imports its own
4236
+ // io/planning-inspect deps, so it needs no module injection — it receives
4237
+ // { args, cwd, raw, error } and ignores the rest of the dispatch context.
4238
+ 'planning': routePlanningCommand,
3731
4239
  'assumption-delta': routeAssumptionDelta,
3732
4240
  'requirements': routeRequirements,
3733
4241
  'gap-analysis': routeGapAnalysis,
@@ -3968,23 +4476,25 @@ function runWithTimeout(argv) {
3968
4476
  // this string and HOST_COMMAND_ROUTERS/SKIP_ROOT_RESOLUTION are three
3969
4477
  // independently hand-maintained sites and nothing previously caught them
3970
4478
  // drifting apart when a query command was added to only one or two.
3971
- const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
4479
+ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--project-dir <path>] [--ws <name>] [--json-errors] [--exit-contract=<v>]\n' +
3972
4480
  'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-docs-guard, commit-to-subrepo, pr-subrepo, ' +
3973
4481
  'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, ' +
3974
4482
  'context-predicates, current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
3975
4483
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
3976
4484
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
3977
- 'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
3978
- 'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, scaffold, smart-entry, state, ' +
3979
- 'config-set-model-profile, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
4485
+ 'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, planning, profile-questionnaire, ' +
4486
+ 'profile-sample, progress, project-instruction-file, prompt-budget, quick-batch, quick-tasks-append, quick-tasks-migrate, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, runtime-identity, scaffold, smart-entry, state, ' +
4487
+ 'config-set-model-profile, dispatch-capacity, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
3980
4488
  'resolve-execution, review-lane, skill-manifest, skills-root, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
3981
4489
  'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
3982
4490
  'Global flags:\n' +
3983
4491
  ' --raw Emit raw output without post-processing\n' +
3984
4492
  ' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
3985
4493
  ' --cwd <path> Override working directory for project-root resolution\n' +
4494
+ ' --project-dir <path> Explicit project root; skips the ancestor walk-up entirely (must already contain .planning/)\n' +
3986
4495
  ' --ws <name> Override active workstream (or set GSD_WORKSTREAM)\n' +
3987
- ' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n\n' +
4496
+ ' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n' +
4497
+ ' --exit-contract=<v> Exit-code contract version: v1 (default) or v2 (or set GSD_EXIT_CONTRACT)\n\n' +
3988
4498
  'For command-specific argument requirements, invoke the command without args ' +
3989
4499
  '(e.g. `gsd-tools phase add`) — the resulting error lists what is required.';
3990
4500
 
@@ -4003,6 +4513,10 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
4003
4513
  // below (never as the live Set itself; see that function's doc comment).
4004
4514
  const SKIP_ROOT_RESOLUTION = new Set([
4005
4515
  'generate-slug', 'current-timestamp', 'verify-path-exists',
4516
+ // #3146: runtime-identity is a pure local read of baked package coordinates.
4517
+ // It is probed from whatever cwd a workflow happens to be in — including
4518
+ // outside any project — so it must never require a resolvable project root.
4519
+ 'runtime-identity',
4006
4520
  // #2844: verify-summary was previously skipped, leaving relative file-claim
4007
4521
  // paths resolved against the raw process.cwd() — invoking from a subdirectory
4008
4522
  // manufactured "missing files" on an otherwise-correct SUMMARY. It now goes
@@ -4074,18 +4588,19 @@ function resolveMainWorktreeCwd(cwd, deps = {}) {
4074
4588
  async function main() {
4075
4589
  let args = process.argv.slice(2);
4076
4590
 
4077
- // #2351: run-with-timeout bounds a spawned command's wall clock portably
4078
- // (coreutils-independent). It MUST intercept HERE, before the global-flag
4079
- // parsing below — the wrapped command's argv is opaque and may itself contain
4080
- // --raw / --cwd / --pick that this dispatcher would otherwise consume.
4081
- {
4082
- let rwt = args;
4083
- if (rwt[0] === 'query') rwt = rwt.slice(1);
4084
- if (rwt[0] === 'run-with-timeout') {
4085
- // Return the child's exit code; runMain() maps it to process.exitCode.
4086
- return runWithTimeout(rwt.slice(1));
4087
- }
4088
- }
4591
+ // These two global-flag blocks (--json-errors, --exit-contract) MUST run
4592
+ // BEFORE the run-with-timeout interception below. run-with-timeout treats
4593
+ // args[0] (post `query` stripping) as the sentinel and otherwise passes the
4594
+ // remaining argv straight to the wrapped child — it never reaches the
4595
+ // dispatcher's "Unknown command" fallback, but a global flag left in LEADING
4596
+ // position (e.g. `--exit-contract=v2 run-with-timeout ...`) would be spliced
4597
+ // out too late if these ran after, since neither block currently exists
4598
+ // below this point to consume it. Splicing here, before run-with-timeout's
4599
+ // own argv slicing, is what keeps both flags position-independent for every
4600
+ // command, run-with-timeout included. Do not move these back below the
4601
+ // run-with-timeout block (#confirmed regression: leading --exit-contract=v2
4602
+ // and leading --json-errors both broke run-with-timeout when these blocks
4603
+ // sat after it).
4089
4604
 
4090
4605
  // --json-errors / GSD_JSON_ERRORS=1: when active, error() emits structured
4091
4606
  // JSON ({ ok: false, reason: <ERROR_REASON code>, message }) to stderr
@@ -4105,6 +4620,41 @@ async function main() {
4105
4620
  setJsonErrorMode(true);
4106
4621
  }
4107
4622
 
4623
+ // --exit-contract=<v> / GSD_EXIT_CONTRACT: resolve FIRST, before the splice
4624
+ // below, so an invalid value (e.g. `v3`, or an empty `--exit-contract=`)
4625
+ // throws EARLY — matching the --json-errors block's own "detect early,
4626
+ // before any flag parsing that can fire error()" rationale above. This also
4627
+ // memoizes the resolved version into the shared contract-version cell so a
4628
+ // later terminateNow()/runMain() call projects against it correctly.
4629
+ //
4630
+ // The argv splice must happen here too, otherwise the dispatcher below sees
4631
+ // "--exit-contract=<v>" as an unknown command when the flag is given in
4632
+ // LEADING position (argv[0] is what the dispatcher treats as the command
4633
+ // name). Splice EVERY occurrence, not just the first — findExitContractFlag
4634
+ // only consults the first match, so a stray second token would otherwise
4635
+ // survive into the dispatcher and reproduce the same "Unknown command".
4636
+ resolveContractVersion({ argv: process.argv, env: process.env });
4637
+ for (let i = args.length - 1; i >= 0; i--) {
4638
+ if (typeof args[i] === 'string' && args[i].startsWith('--exit-contract=')) {
4639
+ args.splice(i, 1);
4640
+ }
4641
+ }
4642
+
4643
+ // #2351: run-with-timeout bounds a spawned command's wall clock portably
4644
+ // (coreutils-independent). It MUST intercept HERE, before the remaining
4645
+ // flag parsing below — the wrapped command's argv is opaque and may itself
4646
+ // contain --raw / --cwd / --pick that this dispatcher would otherwise
4647
+ // consume. (--json-errors / --exit-contract are handled above this block,
4648
+ // not below, precisely so they keep working with run-with-timeout.)
4649
+ {
4650
+ let rwt = args;
4651
+ if (rwt[0] === 'query') rwt = rwt.slice(1);
4652
+ if (rwt[0] === 'run-with-timeout') {
4653
+ // Return the child's exit code; runMain() maps it to process.exitCode.
4654
+ return runWithTimeout(rwt.slice(1));
4655
+ }
4656
+ }
4657
+
4108
4658
  // Optional cwd override for sandboxed subagents running outside project root.
4109
4659
  let cwd = process.cwd();
4110
4660
  const cwdEqArg = args.find(arg => arg.startsWith('--cwd='));
@@ -4125,6 +4675,39 @@ async function main() {
4125
4675
  error(`Invalid --cwd: ${cwd}`, ERROR_REASON.USAGE);
4126
4676
  }
4127
4677
 
4678
+ // #3881: --project-dir <path> is a documented (docs/CONFIGURATION.md,
4679
+ // "Project-Root Resolution in Multi-Repo Workspaces") explicit override of
4680
+ // the project root. It is idempotent under findProjectRoot's ancestor
4681
+ // walk-up — i.e. it short-circuits the walk-up rather than seeding it —
4682
+ // so it MUST be validated and applied here, before findProjectRoot ever
4683
+ // runs, and its result must skip that call entirely below. A relative
4684
+ // value resolves against process.cwd(), matching --cwd's own resolution.
4685
+ let projectDirExplicit = false;
4686
+ const projectDirEqArg = args.find(arg => arg.startsWith('--project-dir='));
4687
+ const projectDirIdx = args.indexOf('--project-dir');
4688
+ let projectDirValue;
4689
+ if (projectDirEqArg) {
4690
+ projectDirValue = projectDirEqArg.slice('--project-dir='.length).trim();
4691
+ if (!projectDirValue) error('Missing value for --project-dir', ERROR_REASON.USAGE);
4692
+ args.splice(args.indexOf(projectDirEqArg), 1);
4693
+ } else if (projectDirIdx !== -1) {
4694
+ projectDirValue = args[projectDirIdx + 1];
4695
+ if (!projectDirValue || projectDirValue.startsWith('--')) error('Missing value for --project-dir', ERROR_REASON.USAGE);
4696
+ args.splice(projectDirIdx, 2);
4697
+ }
4698
+ if (projectDirValue !== undefined) {
4699
+ const resolvedProjectDir = path.resolve(projectDirValue);
4700
+ if (!fs.existsSync(resolvedProjectDir) || !fs.statSync(resolvedProjectDir).isDirectory()) {
4701
+ error(`Invalid --project-dir: ${resolvedProjectDir} (path does not exist or is not a directory)`, ERROR_REASON.USAGE);
4702
+ }
4703
+ const resolvedProjectDirPlanning = path.join(resolvedProjectDir, '.planning');
4704
+ if (!fs.existsSync(resolvedProjectDirPlanning) || !fs.statSync(resolvedProjectDirPlanning).isDirectory()) {
4705
+ error(`Invalid --project-dir: ${resolvedProjectDir} (no .planning/ directory found — --project-dir must name the project root itself, not an ancestor to walk up from)`, ERROR_REASON.USAGE);
4706
+ }
4707
+ cwd = resolvedProjectDir;
4708
+ projectDirExplicit = true;
4709
+ }
4710
+
4128
4711
  // Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
4129
4712
  // However, in monorepo worktrees where the subdirectory itself owns .planning/,
4130
4713
  // skip worktree resolution — the CWD is already the correct project root.
@@ -4237,24 +4820,45 @@ async function main() {
4237
4820
  }
4238
4821
  }
4239
4822
 
4240
- if (!SKIP_ROOT_RESOLUTION.has(command)) {
4823
+ // #3881: an explicit --project-dir already IS the resolved project root
4824
+ // (validated above) — findProjectRoot's ancestor walk-up must not run
4825
+ // over it, per docs/CONFIGURATION.md's documented idempotence.
4826
+ if (!projectDirExplicit && !SKIP_ROOT_RESOLUTION.has(command)) {
4241
4827
  cwd = findProjectRoot(cwd);
4242
4828
  }
4243
4829
 
4244
4830
  // When --pick is active, capture stdout and extract the requested field.
4831
+ // ADR-3473 §8.4 (#3365, #3358): an absent field or non-JSON command output
4832
+ // is a failure ("I could not answer"), never a demotion to an empty answer
4833
+ // at exit 0. `resolveAtFileOutput` MUST run before JSON.parse — @file:
4834
+ // payloads (io.cjs output() writes these for JSON > 50KB) are not
4835
+ // themselves JSON text, so resolving late would make every large result a
4836
+ // false "output was not JSON" (negative space N8).
4245
4837
  if (pickField) {
4246
4838
  const captured = await captureStdoutSyncWrites(async () => {
4247
4839
  await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
4248
4840
  });
4249
4841
  const resolved = resolveAtFileOutput(captured);
4842
+ let obj;
4250
4843
  try {
4251
- const obj = JSON.parse(resolved);
4252
- const value = extractField(obj, pickField);
4253
- const result = value === null || value === undefined ? '' : String(value);
4254
- fs.writeSync(1, result);
4844
+ obj = JSON.parse(resolved);
4255
4845
  } catch {
4256
- fs.writeSync(1, captured);
4846
+ error(`--pick ${formatDiagnosticToken(pickField)}: command output was not JSON`, ERROR_REASON.PICK_OUTPUT_NOT_JSON);
4847
+ return;
4257
4848
  }
4849
+ const { found, value } = extractField(obj, pickField);
4850
+ if (!found) {
4851
+ const rootDescription = isPlainRecord(obj)
4852
+ ? `available top-level keys: ${Object.keys(obj).map(formatKeyForDiagnosticList).join(', ') || '(none)'}`
4853
+ : `the command's output is a JSON ${describeJsonRootType(obj)}, not an object with that field`;
4854
+ error(`--pick ${formatDiagnosticToken(pickField)}: field not found; ${rootDescription}`, ERROR_REASON.PICK_FIELD_ABSENT);
4855
+ return;
4856
+ }
4857
+ // N1/N2: `null` and `''` are answers, not failures — an absent field
4858
+ // above already exited non-zero, so reaching here means the field EXISTS
4859
+ // and this is its real value (including `0` and `false`, #3365).
4860
+ const result = value === null || value === undefined ? '' : String(value);
4861
+ fs.writeSync(1, result);
4258
4862
  return;
4259
4863
  }
4260
4864
 
@@ -4316,27 +4920,68 @@ function resolveAtFileOutput(captured) {
4316
4920
  return fs.readFileSync(captured.slice(6), 'utf-8');
4317
4921
  }
4318
4922
 
4923
+ // A plain object root/intermediate value — everything else (null, an array,
4924
+ // a number, a string, a boolean) is treated as non-object for NAMED-key
4925
+ // lookup purposes (#3365 / #3358, ADR-3473 §8.4): only bracket notation may
4926
+ // reach into an array.
4927
+ function isPlainRecord(v) {
4928
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
4929
+ }
4930
+
4931
+ // Describes the JSON root's shape for a --pick "field not found" message
4932
+ // when the root is NOT a plain object (so listing "top-level keys" would be
4933
+ // meaningless).
4934
+ function describeJsonRootType(v) {
4935
+ if (Array.isArray(v)) return 'array';
4936
+ if (v === null) return 'null';
4937
+ return typeof v;
4938
+ }
4939
+
4940
+ // A command's JSON output can be a USER-authored document (e.g. `frontmatter
4941
+ // get`), so its top-level keys are untrusted the same way an argv token is.
4942
+ // `formatDiagnosticToken` (io.cjs) is the shared escape (see its JSDoc for
4943
+ // why `error()` cannot do this itself); this thin wrapper reuses that exact
4944
+ // escaping but strips the surrounding quotes JSON.stringify adds, so a key
4945
+ // list reads as "a, b, c" rather than the noisier "\"a\", \"b\", \"c\"" while
4946
+ // a key containing \n/\r/\t/other C0 bytes still cannot forge a second
4947
+ // stderr "Error:" line or span more than one line.
4948
+ function formatKeyForDiagnosticList(key) {
4949
+ return formatDiagnosticToken(key).slice(1, -1);
4950
+ }
4951
+
4319
4952
  /**
4320
4953
  * Extract a field from an object using dot-notation and bracket syntax.
4321
4954
  * Supports: 'field', 'parent.child', 'arr[-1]', 'arr[0]'
4955
+ *
4956
+ * Returns a discriminated `{ found, value }` rather than a bare value so a
4957
+ * caller can distinguish "the field exists and is null/''/0/false" (an
4958
+ * ANSWER, exit 0) from "no such field" (an absence, exit non-zero) — #3365.
4959
+ * Reports NOT-FOUND for: a missing key; a dotted path that dies partway; an
4960
+ * array index out of range (after negative-index normalization); a bracket
4961
+ * applied to a non-array; and any key lookup against a non-object (null, a
4962
+ * number, a string, a boolean, or an array root).
4322
4963
  */
4323
4964
  function extractField(obj, fieldPath) {
4324
4965
  const parts = fieldPath.split('.');
4325
4966
  let current = obj;
4326
4967
  for (const part of parts) {
4327
- if (current === null || current === undefined) return undefined;
4328
4968
  const bracketMatch = part.match(/^(.+?)\[(-?\d+)]$/);
4329
4969
  if (bracketMatch) {
4330
4970
  const key = bracketMatch[1];
4331
4971
  const index = parseInt(bracketMatch[2], 10);
4332
- current = current[key];
4333
- if (!Array.isArray(current)) return undefined;
4334
- current = index < 0 ? current[current.length + index] : current[index];
4972
+ if (!isPlainRecord(current)) return { found: false, value: undefined };
4973
+ const arr = current[key];
4974
+ if (!Array.isArray(arr)) return { found: false, value: undefined };
4975
+ const resolvedIndex = index < 0 ? arr.length + index : index;
4976
+ if (resolvedIndex < 0 || resolvedIndex >= arr.length) return { found: false, value: undefined };
4977
+ current = arr[resolvedIndex];
4335
4978
  } else {
4979
+ if (!isPlainRecord(current)) return { found: false, value: undefined };
4980
+ if (!Object.prototype.hasOwnProperty.call(current, part)) return { found: false, value: undefined };
4336
4981
  current = current[part];
4337
4982
  }
4338
4983
  }
4339
- return current;
4984
+ return { found: true, value: current };
4340
4985
  }
4341
4986
 
4342
4987
  async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) {
@@ -4405,5 +5050,21 @@ module.exports = {
4405
5050
  // #3275: exported for tests — the shared PATH+PATHEXT resolver behind
4406
5051
  // review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
4407
5052
  resolveSpawnBinary,
5053
+ // #3714 follow-up: exported for tests — the dispatch model-pin VALUE
5054
+ // policy (charset accept/render parity, max-length boundary, leading-char
5055
+ // anchor) is otherwise unreachable from outside the dispatchOverlayCapabilityCommand closure.
5056
+ resolveDispatchModelPin,
5057
+ MODEL_ID_CHARSET_RE,
5058
+ // The shared character-class body both MODEL_ID_CHARSET_RE and
5059
+ // MODEL_ID_SANITIZE_STRIP_RE are derived from — exported so a test can
5060
+ // assert its own expected charset literal EQUALS this value, making a
5061
+ // silent widening of the production body fail the test instead of only
5062
+ // the (unexported) regexes built from it.
5063
+ MODEL_ID_CHARSET_BODY,
5064
+ // Non-global companion of the internal g-flagged sanitize regex — see the
5065
+ // comment at its definition for why the g-flagged instance is never
5066
+ // exported.
5067
+ MODEL_ID_SANITIZE_STRIP_RE,
5068
+ MODEL_ID_MAX_LENGTH,
4408
5069
  };
4409
5070