@opengsd/gsd-core 1.10.0 → 1.11.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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -20,6 +20,9 @@
20
20
  * resolve-model <agent-type> Get model for agent based on profile
21
21
  * find-phase <phase> Find phase directory by number
22
22
  * commit <message> [--files f1 f2] [--no-verify] Commit planning docs
23
+ * commit-docs-guard enable|disable Opt-in .git/hooks/pre-commit guard
24
+ * that refuses a commit staging
25
+ * .planning/ when commit_docs is false
23
26
  * commit-to-subrepo <msg> --files f1 f2 Route commits to sub-repos
24
27
  * verify-summary <path> Verify a SUMMARY.md file
25
28
  * generate-slug <text> Convert text to URL-safe slug
@@ -68,6 +71,14 @@
68
71
  * milestone complete <version> Archive milestone, create MILESTONES.md
69
72
  * [--name <name>]
70
73
  * [--no-archive-phases] Skip moving phase dirs to milestones/vX.Y-phases/ (archived by default)
74
+ * [--archive-quick] Move .planning/quick/* dirs to milestones/vX.Y-quick/ + reset the
75
+ * Quick Tasks Completed table (#2142; opt-in, default OFF)
76
+ *
77
+ * milestone archive-quick <version> Move .planning/quick/* dirs to milestones/vX.Y-quick/ + reset the
78
+ * Quick Tasks Completed table, WITHOUT the milestone complete close-out
79
+ * (no ROADMAP/REQUIREMENTS/MILESTONES.md writes, no completion guards);
80
+ * safe against an already-completed milestone (#2142 escalation)
81
+ * [--dry-run] Preview what would move, mutates nothing
71
82
  *
72
83
  * User Story Validation:
73
84
  * user-story validate --story "..." Validate "As a / I want to / so that" format
@@ -80,6 +91,7 @@
80
91
  * drift-guard severity --status <S> Classify a symbol verdict into { severity, hardBlock }
81
92
  * [--authority <A>] Status: VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE
82
93
  * Authority: grep|intel|treesitter|lsp|scip (default: config-resolved)
94
+ * drift-guard phase-status [--phase N] Compare STATE.md vs ROADMAP.md phase status
83
95
  *
84
96
  * Validation:
85
97
  * validate consistency Check phase numbering, disk/roadmap sync
@@ -259,8 +271,7 @@ try {
259
271
  }
260
272
  } catch { /* advisory — never block */ }
261
273
 
262
- const { getActiveWorkstream } = require('./lib/planning-workspace.cjs');
263
- const { resolveActiveWorkstream, applyResolvedWorkstreamEnv } = require('./lib/active-workstream-store.cjs');
274
+ const { resolveActiveWorkstream, applyResolvedWorkstreamEnv, peekActiveWorkstream } = require('./lib/active-workstream-store.cjs');
264
275
  const state = require('./lib/state.cjs');
265
276
  const phase = require('./lib/phase.cjs');
266
277
  const roadmap = require('./lib/roadmap.cjs');
@@ -305,7 +316,7 @@ const { routeCheckCommand } = require('./lib/check-command-router.cjs');
305
316
  const { routeTaskCommand } = require('./lib/task-command-router.cjs');
306
317
  const { parseNamedArgs, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
307
318
  const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
308
- const { getEffectiveAuthority, classifyDriftSeverity } = require('./lib/plan-drift-guard.cjs');
319
+ const { getEffectiveAuthority, classifyDriftSeverity, comparePhaseStatus } = require('./lib/plan-drift-guard.cjs');
309
320
 
310
321
  // ─── Bridge collapsed (Phase 4) ────────────────────────────────────────────────
311
322
  // Non-family commands now run through their CJS handlers directly. Keep the
@@ -921,6 +932,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
921
932
  commands.cmdCheckCommit(cwd, raw);
922
933
  }
923
934
 
935
+ function routeCommitDocsGuard({ args, cwd, raw, error }) {
936
+ const subcommand = args[1];
937
+ if (subcommand === 'enable') {
938
+ commands.cmdCommitDocsGuardEnable(cwd, raw);
939
+ } else if (subcommand === 'disable') {
940
+ commands.cmdCommitDocsGuardDisable(cwd, raw);
941
+ } else {
942
+ error('Unknown commit-docs-guard subcommand. Available: enable, disable', ERROR_REASON.SDK_UNKNOWN_COMMAND);
943
+ }
944
+ }
945
+
924
946
  function routeCommitToSubrepo({ args, cwd, raw, error }) {
925
947
  const message = args[1];
926
948
  const filesIndex = args.indexOf('--files');
@@ -1285,30 +1307,45 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1285
1307
  return;
1286
1308
  }
1287
1309
 
1288
- // Effort argv is resolved per lane by the host's own execution policy, exactly as the legs did
1289
- // via `resolve-execution … --pick effort_argv_string`. A lane whose slug is not a known host
1290
- // simply gets none.
1291
- // Resolved through the SAME `resolve-execution` surface the bash legs used
1292
- // (`--host <slug> --pick effort_argv_string`), so the host's negotiated effortSurface still
1293
- // decides whether an argument is emitted and the catalog still owns the syntax (ADR-1239 #2481,
1294
- // ADR-443's escalation ladder). `cmdResolveExecution` writes to stdout and exits, so it cannot
1295
- // be called in-process for a value — this spawns the same bounded query the legs did, once per
1296
- // selected lane. A lane whose slug is not a known host resolves to no effort argument at all.
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.
1317
+ //
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 };
1297
1328
  const effortFor = (slug) => {
1298
1329
  try {
1299
1330
  const r = cp.spawnSync(
1300
1331
  process.execPath,
1301
- [__filename, 'query', 'resolve-execution', 'gsd-plan-checker',
1302
- // NOT `--raw`: that prints the resolved EFFORT ('low'), ignoring --pick. The picked
1303
- // field is what carries the host-specific syntax ('--effort low' for claude,
1304
- // '-c model_reasoning_effort=low' for codex), which is the whole point of asking.
1305
- '--host', slug, '--pick', 'effort_argv_string'],
1332
+ [__filename, 'query', 'resolve-execution', 'gsd-plan-checker', '--host', slug],
1306
1333
  { cwd, encoding: 'utf8', timeout: 15000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 },
1307
1334
  );
1308
- if (r.status !== 0) return [];
1309
- const s = String(r.stdout || '').trim();
1310
- return s ? s.split(/\s+/).filter(Boolean) : [];
1311
- } catch { return []; }
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; }
1312
1349
  };
1313
1350
 
1314
1351
  /**
@@ -1341,7 +1378,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1341
1378
  // so losing all of them to one bad manifest is strictly worse. Belt and braces on purpose.
1342
1379
  let r;
1343
1380
  try {
1344
- r = resolveLanePlan({ lane, configGet, runDir, repoRoot, effortArgs: effortFor(slug) });
1381
+ const effort = effortFor(slug);
1382
+ r = resolveLanePlan({ lane, configGet, runDir, repoRoot, effortArgs: effort.argv, effortValue: effort.value });
1345
1383
  } catch (e) {
1346
1384
  return { slug, ok: false, reason: 'malformed_lane', detail: `resolver threw: ${e && e.message ? e.message : String(e)}` };
1347
1385
  }
@@ -1385,10 +1423,23 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1385
1423
  // ENOENT (CreateProcess cannot start .cmd). Apply the same #2667 shim
1386
1424
  // gate used in runWithTimeout: detect .cmd/.bat and mediate through
1387
1425
  // cmd.exe /d /s /c with an explicit argv array (no shell:true).
1388
- const isWin = process.platform === 'win32';
1389
- const winShim = isWin && /\.(cmd|bat)$/i.test(path.basename(binary));
1390
- const spawnBinary = winShim ? (process.env.ComSpec || 'cmd.exe') : binary;
1391
- const spawnArgv = winShim ? ['/d', '/s', '/c', binary, ...argv] : argv;
1426
+ //
1427
+ // #3275: descriptors declare BARE names, so the gate above never saw an
1428
+ // extension — resolve through the shared PATH+PATHEXT resolver FIRST
1429
+ // (the same one `hasBinary` uses, so probe and spawn can never disagree
1430
+ // about what the lane's binary is). POSIX keeps the bare name: Node's own
1431
+ // PATH search already worked there, and the #3275 acceptance contract
1432
+ // holds macOS/Linux behavior unchanged. A name that resolves to nothing
1433
+ // falls back to the declared name so the ENOENT still surfaces (#3086).
1434
+ // #3411: the resolve-then-mediate pair is one seam call now. Both halves had
1435
+ // private copies here; `projectSpawnInvocation` owns them, so a fix to either
1436
+ // reaches every spawn site instead of only this one.
1437
+ //
1438
+ // Unlike execTool, this lane adopts the RESOLVED path even for a non-batch
1439
+ // binary: that is the behavior #3445 shipped and `deps.hasBinary` answers
1440
+ // from the same resolver, so probe and spawn must agree on the exact file.
1441
+ const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
1442
+ const { command: spawnBinary, args: spawnArgv, windowsVerbatimArguments } = projectSpawnInvocation(binary, argv);
1392
1443
  const r = cp.spawnSync(spawnBinary, spawnArgv, {
1393
1444
  input: opts.input,
1394
1445
  encoding: 'utf8',
@@ -1396,6 +1447,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1396
1447
  killSignal: 'SIGKILL',
1397
1448
  maxBuffer: 64 * 1024 * 1024,
1398
1449
  shell: false, // argv array only — never a shell string (no interpolation of config values)
1450
+ // #2483: a lane's declared env pairs merged OVER this process's environment, for this
1451
+ // child only. Passing a fresh object leaves `process.env` untouched, so nothing leaks
1452
+ // into the orchestrating session or into the next lane.
1453
+ ...(opts.env ? { env: { ...process.env, ...opts.env } } : {}),
1454
+ ...(windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
1399
1455
  });
1400
1456
  return {
1401
1457
  status: r.status,
@@ -1424,24 +1480,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1424
1480
  // all (a probe that costs a process is a probe you avoid running, which is how the original
1425
1481
  // Kimi probe ended up unbounded), and `shell: true` with an args array is deprecated in
1426
1482
  // Node 26 (DEP0190) because the arguments are concatenated rather than escaped.
1427
- hasBinary: (name) => {
1428
- if (!name || name.includes('/') || name.includes('\\')) {
1429
- try { return fsx.statSync(name).isFile(); } catch { return false; }
1430
- }
1431
- const exts = process.platform === 'win32'
1432
- ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
1433
- : [''];
1434
- for (const dir of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
1435
- for (const ext of exts) {
1436
- const candidate = path.join(dir, name + ext);
1437
- try {
1438
- const st = fsx.statSync(candidate);
1439
- if (st.isFile()) return true;
1440
- } catch { /* next candidate */ }
1441
- }
1442
- }
1443
- return false;
1444
- },
1483
+ //
1484
+ // #3275: the scan lives in `resolveSpawnBinary` now, SHARED with `deps.spawn`
1485
+ // above. Two private copies of "what is this declared binary?" is how the
1486
+ // defect hid: the probe resolved WITH PATHEXT while spawn resolved WITHOUT,
1487
+ // so a lane reported available for a spawn that could never start. One
1488
+ // resolver, both seams — if one changes, the other changes with it.
1489
+ hasBinary: (name) => resolveSpawnBinary(name) !== null,
1445
1490
  configGet,
1446
1491
  homeDir: os.homedir(),
1447
1492
  warn: (m) => process.stderr.write(`${m}\n`),
@@ -1493,12 +1538,14 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1493
1538
  `model override (it declares no modelConfigKey). The review will use the CLI's own default.\n`,
1494
1539
  );
1495
1540
  }
1541
+ const instanceEffort = effortFor(entry.slug);
1496
1542
  const overridden = resolveLanePlan({
1497
1543
  lane,
1498
1544
  configGet: (k) => (key && k === key ? instanceModel : configGet(k)),
1499
1545
  runDir,
1500
1546
  repoRoot,
1501
- effortArgs: effortFor(entry.slug),
1547
+ effortArgs: instanceEffort.argv,
1548
+ effortValue: instanceEffort.value,
1502
1549
  });
1503
1550
  if (overridden.ok) {
1504
1551
  // Preserve any instance retargeting already applied above.
@@ -1575,7 +1622,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1575
1622
  function routeDispatchShouldFlatten({ args, cwd, raw, error }) {
1576
1623
  // #1708 / #853: typed query replacing the `RUNTIME === 'codex'` prose rule.
1577
1624
  //
1578
- // Resolves the current runtime (GSD_RUNTIME > config.runtime > 'claude'),
1625
+ // Resolves the current runtime (GSD_RUNTIME > config.runtime > per-install
1626
+ // .gsd-runtime marker > 'claude'),
1579
1627
  // looks up registry.runtimes[id].runtime.hostIntegration.dispatch, and
1580
1628
  // calls shouldFlattenDispatch(dispatch) from host-integration.cjs.
1581
1629
  //
@@ -1629,7 +1677,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1629
1677
  // #853) for exactly this reason — it replaced a `RUNTIME === 'codex'`
1630
1678
  // prose rule.
1631
1679
  //
1632
- // Resolves the current runtime (GSD_RUNTIME > config.runtime > 'claude'),
1680
+ // Resolves the current runtime (GSD_RUNTIME > config.runtime > per-install
1681
+ // .gsd-runtime marker > 'claude'),
1633
1682
  // reads registry.runtimes[id].runtime.hostIntegration.dispatch.isolation,
1634
1683
  // and validates it against the closed vocabulary.
1635
1684
  //
@@ -1664,12 +1713,83 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1664
1713
  // `per-plan-worktree-gate.md`) override the naturally-resolved mode
1665
1714
  // while still going through this single write path. Best-effort: a
1666
1715
  // sentinel write failure here must never fail the wave.
1667
- const VALID_ISOLATION = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
1716
+ const decision = resolveDispatchIsolationDecision({ args, cwd });
1717
+ const runtimeId = decision.runtimeId;
1718
+ let { isolation, exec, harnessFlag } = decision;
1719
+
1720
+ // `--force-isolation <mode>` overrides the naturally-resolved mode with
1721
+ // context this resolver has no way to see on its own (e.g. the #2474
1722
+ // per-plan submodule intersection). Invalid/unrecognized values are
1723
+ // ignored rather than erroring — this is a best-effort recording call,
1724
+ // not a hard usage gate. Forcing to 'none' clears harnessFlag/exec since
1725
+ // neither applies to sequential dispatch.
1726
+ const forceIdx = args.indexOf('--force-isolation');
1727
+ const forcedIsolation = forceIdx !== -1 ? args[forceIdx + 1] : undefined;
1728
+ if (forcedIsolation && DISPATCH_ISOLATION_VOCABULARY.has(forcedIsolation)) {
1729
+ isolation = forcedIsolation;
1730
+ if (isolation === 'none') {
1731
+ harnessFlag = null;
1732
+ exec = null;
1733
+ }
1734
+ }
1735
+
1736
+ const phaseIdx = args.indexOf('--phase');
1737
+ const phaseArg = phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--')
1738
+ ? args[phaseIdx + 1]
1739
+ : null;
1740
+ const planIdx = args.indexOf('--plan');
1741
+ const planArg = planIdx !== -1 && args[planIdx + 1] && !args[planIdx + 1].startsWith('--')
1742
+ ? args[planIdx + 1]
1743
+ : null;
1744
+
1745
+ // Side-effect write (#3045 CORE REDESIGN) — see the doc comment above.
1746
+ // Never allowed to affect this query's own stdout contract or throw.
1747
+ try {
1748
+ writeDispatchIsolationSentinel(cwd, { isolation, harnessFlag, phase: phaseArg, plan: planArg });
1749
+ } catch {
1750
+ // writeDispatchIsolationSentinel already swallows its own errors into
1751
+ // a { recorded: false } result; this catch is defense in depth only.
1752
+ }
1753
+
1754
+ if (args.indexOf('--json') !== -1) {
1755
+ output({ runtime: runtimeId, isolation, exec, harnessFlag }, raw);
1756
+ } else {
1757
+ process.stdout.write(isolation);
1758
+ }
1759
+ }
1760
+
1761
+ const DISPATCH_ISOLATION_VOCABULARY = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
1762
+
1763
+ /**
1764
+ * Shared, side-effect-free resolution of the negotiated dispatch isolation:
1765
+ * runtime (GSD_RUNTIME > config.runtime > per-install .gsd-runtime marker >
1766
+ * 'claude') → declared
1767
+ * `dispatch.isolation` → harness-flag / orchestrator-exec degrade rules.
1768
+ * Extracted (#2486) so `routeDispatchIsolation` (the #3045 recording
1769
+ * dispatch path) and `routeInspectDispatchIsolation` (the read-only
1770
+ * inspection path) share exactly one negotiation implementation and cannot
1771
+ * drift apart. Resolution only — the caller decides whether the decision is
1772
+ * recorded to the sentinel.
1773
+ */
1774
+ function resolveDispatchIsolationDecision({ args, cwd }) {
1668
1775
  let isolation = 'none';
1669
1776
  let runtimeId = null;
1670
1777
  let exec = null;
1671
1778
  let harnessFlag = null;
1672
1779
  try {
1780
+ // Deliberately `resolveRuntime`, NOT `resolveActiveRuntime`/`loadConfig`:
1781
+ // loadConfig normalizes and rewrites legacy keys back to disk, and this
1782
+ // resolver backs the sentinel-free `inspect-dispatch-isolation` verb,
1783
+ // which must never write. resolveRuntime reads config.json directly.
1784
+ //
1785
+ // KNOWN LIMITATION, tracked separately: resolveRuntime stops at
1786
+ // GSD_RUNTIME > config.runtime > 'claude' and does not consult the
1787
+ // per-install `.gsd-runtime` marker, so on a non-Claude install whose
1788
+ // project config carries no `runtime` key this resolves 'claude'. That is
1789
+ // open-gsd/gsd-core#2395 — a pre-existing defect in the canonical
1790
+ // resolver, not introduced here, and deliberately NOT fixed in this PR
1791
+ // (its blast radius reaches every consumer of that resolver, so it is
1792
+ // being handled on its own).
1673
1793
  const { resolveRuntime } = require('./lib/runtime-slash.cjs');
1674
1794
  runtimeId = resolveRuntime(cwd);
1675
1795
 
@@ -1678,7 +1798,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1678
1798
  ? registry.runtimes[runtimeId]
1679
1799
  : null;
1680
1800
  const declared = runtimeEntry?.runtime?.hostIntegration?.dispatch?.isolation ?? null;
1681
- if (typeof declared === 'string' && VALID_ISOLATION.has(declared)) {
1801
+ if (typeof declared === 'string' && DISPATCH_ISOLATION_VOCABULARY.has(declared)) {
1682
1802
  isolation = declared;
1683
1803
  }
1684
1804
 
@@ -1721,41 +1841,52 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1721
1841
  exec = null;
1722
1842
  harnessFlag = null;
1723
1843
  }
1844
+ return { runtimeId, isolation, exec, harnessFlag };
1845
+ }
1724
1846
 
1725
- // `--force-isolation <mode>` overrides the naturally-resolved mode with
1726
- // context this resolver has no way to see on its own (e.g. the #2474
1727
- // per-plan submodule intersection). Invalid/unrecognized values are
1728
- // ignored rather than erroring — this is a best-effort recording call,
1729
- // not a hard usage gate. Forcing to 'none' clears harnessFlag/exec since
1730
- // neither applies to sequential dispatch.
1731
- const forceIdx = args.indexOf('--force-isolation');
1732
- const forcedIsolation = forceIdx !== -1 ? args[forceIdx + 1] : undefined;
1733
- if (forcedIsolation && VALID_ISOLATION.has(forcedIsolation)) {
1734
- isolation = forcedIsolation;
1735
- if (isolation === 'none') {
1736
- harnessFlag = null;
1737
- exec = null;
1738
- }
1739
- }
1740
-
1741
- const phaseIdx = args.indexOf('--phase');
1742
- const phaseArg = phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--')
1743
- ? args[phaseIdx + 1]
1744
- : null;
1745
- const planIdx = args.indexOf('--plan');
1746
- const planArg = planIdx !== -1 && args[planIdx + 1] && !args[planIdx + 1].startsWith('--')
1747
- ? args[planIdx + 1]
1748
- : null;
1749
-
1750
- // Side-effect write (#3045 CORE REDESIGN) — see the doc comment above.
1751
- // Never allowed to affect this query's own stdout contract or throw.
1752
- try {
1753
- writeDispatchIsolationSentinel(cwd, { isolation, harnessFlag, phase: phaseArg, plan: planArg });
1754
- } catch {
1755
- // writeDispatchIsolationSentinel already swallows its own errors into
1756
- // a { recorded: false } result; this catch is defense in depth only.
1847
+ function routeInspectDispatchIsolation({ args, cwd, raw }) {
1848
+ // #2486: sentinel-free sibling of `dispatch-isolation` for INSPECTION
1849
+ // surfaces — /gsd:health's W025 check and /gsd:settings' Worktrees
1850
+ // branching. The dispatch verb above intentionally records its resolved
1851
+ // decision to the isolation sentinel as an unconditional side effect
1852
+ // (#3045 CORE REDESIGN): correct for executor dispatch, where the record
1853
+ // must be structurally unskippable — but wrong for a read-only
1854
+ // diagnostic. A health check that records a phase:null/plan:null
1855
+ // sentinel can hard-block every executor dispatch for the sentinel's
1856
+ // lifetime, across sessions sharing the main checkout. Inspection
1857
+ // surfaces call this verb instead. Two claims, both narrower than
1858
+ // "side-effect-free", and both exactly true (#2486 review, Majors 2 & 4):
1859
+ //
1860
+ // 1. SENTINEL-FREE, not write-free. This route writes nothing itself, and
1861
+ // in particular never writes .gsd/dispatch-isolation-sentinel.json —
1862
+ // the only write that can hard-block a later executor dispatch. It is
1863
+ // NOT an unconditional claim of total filesystem purity: like every
1864
+ // gsd-tools invocation, it runs the shared bootstrap and
1865
+ // active-workstream resolution first. As of #3579's root-cause fix
1866
+ // that bootstrap resolves via the non-mutating peekActiveWorkstream
1867
+ // (never unlinks); an actual stale/invalid pointer is still
1868
+ // self-healed, but only by whichever verb's own getActiveWorkstream
1869
+ // call later consumes it for real — this inspection route makes no
1870
+ // such call, so it is now also side-effect-free on the pointer file.
1871
+ //
1872
+ // 2. SHARED NEGOTIATION, for the arguments this verb accepts. Both verbs
1873
+ // call resolveDispatchIsolationDecision, so the natural resolution
1874
+ // cannot drift. It is NOT a claim of byte-identical output for every
1875
+ // argv: routeDispatchIsolation applies --force-isolation AFTER the
1876
+ // shared helper returns, so the same argv could otherwise yield
1877
+ // 'none' there and the declared capability here. Rather than let a
1878
+ // caller receive a silently different answer, this verb REJECTS the
1879
+ // recording-only knobs outright — they exist to be recorded, and a
1880
+ // read has nothing to record.
1881
+ const RECORDING_ONLY_ARGS = ['--force-isolation', '--phase', '--plan'];
1882
+ const rejected = RECORDING_ONLY_ARGS.filter((flag) => args.indexOf(flag) !== -1);
1883
+ if (rejected.length > 0) {
1884
+ error(
1885
+ `inspect-dispatch-isolation: ${rejected.join(', ')} ${rejected.length === 1 ? 'is a' : 'are'} recording-only argument${rejected.length === 1 ? '' : 's'} and cannot be used on the inspection verb — it resolves the runtime's DECLARED capability and records nothing. Use 'query dispatch-isolation' if you need the override applied and the decision recorded.`,
1886
+ ERROR_REASON.USAGE,
1887
+ );
1757
1888
  }
1758
-
1889
+ const { runtimeId, isolation, exec, harnessFlag } = resolveDispatchIsolationDecision({ args, cwd });
1759
1890
  if (args.indexOf('--json') !== -1) {
1760
1891
  output({ runtime: runtimeId, isolation, exec, harnessFlag }, raw);
1761
1892
  } else {
@@ -1906,6 +2037,50 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1906
2037
  }
1907
2038
  }
1908
2039
 
2040
+ function routeResolveAgent({ args, cwd, raw, error }) {
2041
+ // #1689: resolve a per-plan agent_hint specialist name to the subagent_type
2042
+ // an Agent() call should use. Returns the name unchanged when a
2043
+ // matching agent file exists in the active runtime's agent dir(s);
2044
+ // 'gsd-executor' when the name is absent, blank, or does not resolve.
2045
+ // Fail-closed is the fallback (gsd-executor) — never echo an
2046
+ // unvalidated name, which would make Agent() error and block the wave.
2047
+ //
2048
+ // Output:
2049
+ // --raw (default) -> prints the resolved type (the hint, or 'gsd-executor')
2050
+ // --json -> prints { runtime, requested, resolved, fallback }
2051
+ const FALLBACK = 'gsd-executor';
2052
+ try {
2053
+ const nameIdx = args.indexOf('--name');
2054
+ const requested = nameIdx !== -1 ? args[nameIdx + 1] : '';
2055
+ const { resolveRuntime } = require('./lib/runtime-slash.cjs');
2056
+ const runtimeId = resolveRuntime(cwd);
2057
+ const { resolveAgentHint } = require('./lib/agent-install-check.cjs');
2058
+ let resolved = FALLBACK;
2059
+ let resolvedOk = false; // true only when resolveAgentHint returned a hit
2060
+ if (requested && !requested.startsWith('-')) {
2061
+ const hit = resolveAgentHint(requested, runtimeId, cwd);
2062
+ if (hit !== null) {
2063
+ resolved = hit;
2064
+ resolvedOk = true;
2065
+ }
2066
+ }
2067
+ // `fallback` = we did NOT honor a resolvable hint (absent/flag-shaped name,
2068
+ // the named agent did not resolve, or resolution errored). Requesting
2069
+ // gsd-executor explicitly and resolving to it is NOT a fallback.
2070
+ const fellBack = !resolvedOk;
2071
+ const jsonIdx = args.indexOf('--json');
2072
+ if (jsonIdx !== -1) {
2073
+ output({ runtime: runtimeId, requested: requested || null, resolved, fallback: fellBack }, raw);
2074
+ } else {
2075
+ process.stdout.write(String(resolved));
2076
+ }
2077
+ } catch {
2078
+ // Fail-closed: degrade to the legacy executor on any error so dispatch
2079
+ // never blocks on resolution.
2080
+ process.stdout.write(FALLBACK);
2081
+ }
2082
+ }
2083
+
1909
2084
  function routeAgentSkills({ args, cwd, raw, error }) {
1910
2085
  // --json emits typed IR { agent_type, block, skills_count } for test assertions
1911
2086
  // (#455). Default (no flag) outputs raw XML so workflow shell expansions work.
@@ -2006,9 +2181,20 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
2006
2181
  const force = args.includes('--force');
2007
2182
  // #2118: --dry-run prints a preview plan without mutating.
2008
2183
  const dryRun = args.includes('--dry-run');
2009
- milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun }, raw);
2184
+ // #2142: quick-task archival is opt-in (default OFF) — unlike
2185
+ // --no-archive-phases' inverted shape, absence of this flag means
2186
+ // "do nothing" rather than "skip a default-on behavior".
2187
+ const archiveQuick = args.includes('--archive-quick');
2188
+ milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun, archiveQuick }, raw);
2189
+ } else if (subcommand === 'archive-quick') {
2190
+ // #2142 escalation: narrow archival-only entry point (does NOT
2191
+ // touch ROADMAP/REQUIREMENTS/MILESTONES.md, runs no completion
2192
+ // guards) — safe to call against an already-completed milestone,
2193
+ // unlike `milestone complete --archive-quick`.
2194
+ const dryRun = args.includes('--dry-run');
2195
+ milestone.cmdQuickArchive(cwd, args[2], { dryRun }, raw);
2010
2196
  } else {
2011
- error('Unknown milestone subcommand. Available: complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2197
+ error('Unknown milestone subcommand. Available: complete, archive-quick', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2012
2198
  }
2013
2199
  }
2014
2200
 
@@ -3283,13 +3469,189 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
3283
3469
  return;
3284
3470
  }
3285
3471
 
3472
+ if (subcommand === 'phase-status') {
3473
+ // #1956: deterministic STATE.md-vs-ROADMAP.md phase-status drift.
3474
+ const { planningDir } = require('./lib/planning-workspace.cjs');
3475
+ const { stateExtractField, stateCurrentPositionSlice } = require('./lib/state-document.cjs');
3476
+ const { findRoadmapProgressTable } = require('./lib/roadmap-parser.cjs');
3477
+ const { phaseKeyFromProse } = require('./lib/phase-id.cjs');
3478
+ // STATE.md's YAML frontmatter carries its own lowercase `status:`
3479
+ // scalar ahead of the body's `## Current Position` prose "Status:"
3480
+ // line; stateExtractField's non-scoped regex would otherwise match
3481
+ // that frontmatter line first (it comes first in the file) and
3482
+ // silently report the wrong value. Strip frontmatter so extraction
3483
+ // is scoped to the body.
3484
+ const { stripFrontmatter } = require('./lib/frontmatter.cjs');
3485
+
3486
+ const phaseIdx = args.indexOf('--phase');
3487
+ const phaseArg = (phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--'))
3488
+ ? args[phaseIdx + 1]
3489
+ : undefined;
3490
+
3491
+ const dir = planningDir(cwd);
3492
+ const statePath = path.join(dir, 'STATE.md');
3493
+ const roadmapPath = path.join(dir, 'ROADMAP.md');
3494
+
3495
+ let stateContent = null;
3496
+ try {
3497
+ stateContent = fs.readFileSync(statePath, 'utf-8');
3498
+ } catch {
3499
+ // missing_state below
3500
+ }
3501
+ if (stateContent === null) {
3502
+ const phase = phaseArg !== undefined ? phaseKeyFromProse(phaseArg) : null;
3503
+ output({
3504
+ verdict: 'uncheckable',
3505
+ reason: 'missing_state',
3506
+ phase,
3507
+ stateStatus: null,
3508
+ roadmapStatus: null,
3509
+ authority: 'STATE.md',
3510
+ }, raw);
3511
+ return;
3512
+ }
3513
+
3514
+ let roadmapContent = null;
3515
+ try {
3516
+ roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
3517
+ } catch {
3518
+ // missing_roadmap below
3519
+ }
3520
+
3521
+ const stateBody = stripFrontmatter(stateContent);
3522
+ // #1956 fix: scope extraction to `## Current Position` (or `###`
3523
+ // in the bootstrap template) so a historical `Phase:` / `Status:`
3524
+ // line in an archive section (e.g. `## Session Continuity
3525
+ // Archive`) can't shadow the real one — same #2956 scope state.cts
3526
+ // uses for current_phase, via the shared owner in
3527
+ // state-document.cjs.
3528
+ //
3529
+ // Deliberately NO whole-body fallback here. `state.cts`'s WRITE
3530
+ // path falls back to the whole body when no Current Position
3531
+ // heading is found (legacy behavior it must preserve for
3532
+ // backward-compatible writes) — but that fallback is wrong for a
3533
+ // READ that feeds a drift finding: a STATE.md with no Current
3534
+ // Position heading is exactly the shape that let a stray historical
3535
+ // `Status:` line elsewhere in the body shadow the real value and
3536
+ // fabricate a 'drifted' verdict. A guess is worse than an
3537
+ // abstention for a drift detector, so an absent Current Position
3538
+ // section reports 'uncheckable' instead of guessing from the whole
3539
+ // document.
3540
+ const currentPositionBody = stateCurrentPositionSlice(stateBody);
3541
+ if (currentPositionBody === null) {
3542
+ const phase = phaseArg !== undefined ? phaseKeyFromProse(phaseArg) : null;
3543
+ output({
3544
+ verdict: 'uncheckable',
3545
+ reason: 'no_current_position',
3546
+ phase,
3547
+ stateStatus: null,
3548
+ roadmapStatus: null,
3549
+ authority: 'STATE.md',
3550
+ }, raw);
3551
+ return;
3552
+ }
3553
+
3554
+ // Resolve the target phase: --phase if given, else whatever
3555
+ // STATE.md's Current Position reports as current.
3556
+ const phase = phaseArg !== undefined
3557
+ ? phaseKeyFromProse(phaseArg)
3558
+ : phaseKeyFromProse(stateExtractField(currentPositionBody, 'Phase'));
3559
+
3560
+ if (roadmapContent === null) {
3561
+ output({
3562
+ verdict: 'uncheckable',
3563
+ reason: 'missing_roadmap',
3564
+ phase,
3565
+ stateStatus: null,
3566
+ roadmapStatus: null,
3567
+ authority: 'STATE.md',
3568
+ }, raw);
3569
+ return;
3570
+ }
3571
+
3572
+ const stateStatus = stateExtractField(currentPositionBody, 'Status');
3573
+
3574
+ // #1956/#2012: scoped to `## Progress` first (decoy-avoidance) —
3575
+ // see findRoadmapProgressTable's doc comment (roadmap-parser.cts).
3576
+ const table = findRoadmapProgressTable(roadmapContent);
3577
+ const matchedRow = table
3578
+ ? table.rows.find((row) => phaseKeyFromProse(row.Phase) === phase && phase !== null)
3579
+ : undefined;
3580
+
3581
+ if (!matchedRow) {
3582
+ const result = comparePhaseStatus({ stateStatus, roadmapStatus: null });
3583
+ output({
3584
+ verdict: 'uncheckable',
3585
+ reason: 'phase_not_in_roadmap',
3586
+ phase,
3587
+ stateStatus,
3588
+ roadmapStatus: null,
3589
+ stateRank: result.stateRank,
3590
+ roadmapRank: result.roadmapRank,
3591
+ authority: 'STATE.md',
3592
+ }, raw);
3593
+ return;
3594
+ }
3595
+
3596
+ const roadmapStatus = matchedRow.Status;
3597
+ const result = comparePhaseStatus({ stateStatus, roadmapStatus });
3598
+ output({
3599
+ verdict: result.verdict,
3600
+ phase,
3601
+ stateStatus,
3602
+ roadmapStatus,
3603
+ stateRank: result.stateRank,
3604
+ roadmapRank: result.roadmapRank,
3605
+ authority: 'STATE.md',
3606
+ }, raw);
3607
+ return;
3608
+ }
3609
+
3286
3610
  error(
3287
- `Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity`,
3611
+ `Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity, phase-status`,
3288
3612
  ERROR_REASON.SDK_UNKNOWN_COMMAND,
3289
3613
  );
3290
3614
  }
3291
3615
 
3292
3616
 
3617
+ /**
3618
+ * #3275: resolve a DECLARED command name to the file a spawn can actually start.
3619
+ *
3620
+ * Lane descriptors (src/review-lane-descriptor.cts) declare BARE, platform-unaware
3621
+ * binary names ('codex', 'gemini', 'kimi', 'agy'), and `review-lane invoke`'s
3622
+ * `deps.spawn` + `deps.hasBinary` both need the on-disk form of that name. Before
3623
+ * this helper existed they disagreed: `hasBinary` scanned PATH WITH PATHEXT (so
3624
+ * probes reported lanes AVAILABLE on Windows) while `spawn` received the bare name
3625
+ * — `CreateProcess` performs no PATHEXT resolution, so every spawn-transport lane
3626
+ * ENOENT'd there, and the #3086 `.cmd`/`.bat` cmd.exe mediation gate never fired
3627
+ * because the declared name never carried an extension. One shared resolver is the
3628
+ * only shape that cannot drift back apart.
3629
+ *
3630
+ * win32: tries PATHEXT entries ONLY — never the bare name. npm global installs
3631
+ * drop an EXTENSIONLESS POSIX sh shim (`...\npm\codex`) next to `codex.CMD`; a
3632
+ * bare-name-first scan resolves to it, the mediation gate sees no `.cmd`, and the
3633
+ * ENOENT returns unchanged (field-reported on Windows 11 — see the #3275 issue
3634
+ * comment pinning exactly this pitfall).
3635
+ *
3636
+ * POSIX: answers EXISTENCE only (the old `hasBinary` contract, preserved
3637
+ * byte-for-byte) by scanning PATH for the bare name. `deps.spawn` does NOT consult
3638
+ * this on POSIX — the bare name goes to spawnSync unchanged and Node's own PATH
3639
+ * search does the work, so macOS/Linux behavior is untouched (#3275 acceptance).
3640
+ *
3641
+ * Path-like names (any '/' or '\') bypass the PATH scan: the name is already an
3642
+ * address, so it passes through when the file exists and is a file.
3643
+ *
3644
+ * #3411: the scan itself now lives in the declared platform seam
3645
+ * (`src/shell-command-projection.cts` → `resolveExecutableBinary`). This function is
3646
+ * the `bin/` entry point onto it and holds no copy of the logic — `CONTEXT.md`
3647
+ * declares that file "All OS-facing I/O; single platform seam", and a private
3648
+ * duplicate here is what made it untrue.
3649
+ */
3650
+ function resolveSpawnBinary(name, platform = process.platform, env = process.env) {
3651
+ const { resolveExecutableBinary } = require('./lib/shell-command-projection.cjs');
3652
+ return resolveExecutableBinary(name, { platform, env });
3653
+ }
3654
+
3293
3655
  const HOST_COMMAND_ROUTERS = {
3294
3656
  // Each entry wraps its `route*Command` router so it receives the module-scope
3295
3657
  // lib the old `case` arm passed, plus the per-dispatch context
@@ -3339,6 +3701,7 @@ const HOST_COMMAND_ROUTERS = {
3339
3701
  'find-phase': routeFindPhase,
3340
3702
  'commit': routeCommit,
3341
3703
  'check-commit': routeCheckCommit,
3704
+ 'commit-docs-guard': routeCommitDocsGuard,
3342
3705
  'commit-to-subrepo': routeCommitToSubrepo,
3343
3706
  'pr-subrepo': routePrSubrepo,
3344
3707
  'verify-summary': routeVerifySummary,
@@ -3357,8 +3720,10 @@ const HOST_COMMAND_ROUTERS = {
3357
3720
  'normalize-test-command': routeNormalizeTestCommand,
3358
3721
  'dispatch-should-flatten': routeDispatchShouldFlatten,
3359
3722
  'dispatch-isolation': routeDispatchIsolation,
3723
+ 'inspect-dispatch-isolation': routeInspectDispatchIsolation,
3360
3724
  'record-dispatch-isolation': routeRecordDispatchIsolation,
3361
3725
  'resolve-dispatch-type': routeResolveDispatchType,
3726
+ 'resolve-agent': routeResolveAgent,
3362
3727
  'agent-skills': routeAgentSkills,
3363
3728
  'skill-manifest': routeSkillManifest,
3364
3729
  'history-digest': routeHistoryDigest,
@@ -3604,14 +3969,14 @@ function runWithTimeout(argv) {
3604
3969
  // independently hand-maintained sites and nothing previously caught them
3605
3970
  // drifting apart when a query command was added to only one or two.
3606
3971
  const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
3607
- 'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, pr-subrepo, ' +
3972
+ 'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-docs-guard, commit-to-subrepo, pr-subrepo, ' +
3608
3973
  'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, ' +
3609
3974
  'context-predicates, current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
3610
3975
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
3611
3976
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
3612
3977
  'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
3613
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, ' +
3614
- 'config-set-model-profile, dispatch-isolation, dispatch-should-flatten, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-dispatch-type, ' +
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, ' +
3615
3980
  'resolve-execution, review-lane, skill-manifest, skills-root, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
3616
3981
  'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
3617
3982
  'Global flags:\n' +
@@ -3769,8 +4134,21 @@ async function main() {
3769
4134
  // Priority: --ws flag > GSD_WORKSTREAM env var > session/shared pointer > null.
3770
4135
  let workstreamContext = null;
3771
4136
  try {
4137
+ // #3579 root-cause fix: this bootstrap resolution only decides whether to
4138
+ // populate GSD_WORKSTREAM env for downstream routing — it is a check, not
4139
+ // the consuming read. Using the mutating getActiveWorkstream here
4140
+ // self-healed (cleared) a present-but-unresolvable pointer BEFORE the
4141
+ // dispatched command's own resolution/diagnostic ran, so a second read in
4142
+ // the same process (e.g. a subcommand's own getActiveWorkstream call, or
4143
+ // a fail-safe guard's diagnoseUnresolvedActiveWorkstream) observed
4144
+ // already-cleared state — silently falling through to a fallback marker
4145
+ // it should never have inherited (isolation violation), or losing the
4146
+ // evidence a diagnostic needed to explain why nothing resolved. peek
4147
+ // shares the identical resolution logic and only differs by never
4148
+ // calling adapter.clear(); self-heal still happens, exactly once, at
4149
+ // whichever call site actually consumes the workstream for real.
3772
4150
  workstreamContext = resolveActiveWorkstream(cwd, args, process.env, {
3773
- getStored: getActiveWorkstream,
4151
+ getStored: peekActiveWorkstream,
3774
4152
  });
3775
4153
  args = workstreamContext.args;
3776
4154
  // Set env var so all modules (planningDir, planningPaths) auto-resolve workstream paths.
@@ -4024,5 +4402,8 @@ module.exports = {
4024
4402
  TOP_LEVEL_USAGE,
4025
4403
  skipsRootResolution,
4026
4404
  resolveMainWorktreeCwd,
4405
+ // #3275: exported for tests — the shared PATH+PATHEXT resolver behind
4406
+ // review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
4407
+ resolveSpawnBinary,
4027
4408
  };
4028
4409