@opengsd/gsd-core 1.14.0 → 1.15.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 (283) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-code-fixer.compact.md +7 -6
  8. package/agents/gsd-code-fixer.md +9 -8
  9. package/agents/gsd-debug-session-manager.compact.md +17 -2
  10. package/agents/gsd-debug-session-manager.md +17 -2
  11. package/agents/gsd-debugger.md +2 -2
  12. package/agents/gsd-eval-auditor.compact.md +1 -1
  13. package/agents/gsd-eval-auditor.md +1 -1
  14. package/agents/gsd-executor.md +13 -8
  15. package/agents/gsd-intel-updater.compact.md +1 -1
  16. package/agents/gsd-intel-updater.md +1 -1
  17. package/agents/gsd-phase-researcher.md +19 -11
  18. package/agents/gsd-plan-checker.md +8 -7
  19. package/agents/gsd-planner.md +12 -8
  20. package/agents/gsd-project-researcher.compact.md +1 -1
  21. package/agents/gsd-project-researcher.md +1 -1
  22. package/agents/gsd-research-synthesizer.compact.md +1 -1
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-auditor.md +155 -17
  25. package/agents/gsd-ui-researcher.compact.md +1 -1
  26. package/agents/gsd-ui-researcher.md +1 -1
  27. package/agents/gsd-verifier.md +10 -9
  28. package/bin/install.js +642 -95
  29. package/commands/gsd/autonomous.md +2 -2
  30. package/commands/gsd/capture.md +1 -1
  31. package/commands/gsd/mempalace-capture.md +7 -3
  32. package/commands/gsd/plan-review-convergence.md +6 -6
  33. package/commands/gsd/progress.md +1 -1
  34. package/commands/gsd/quick-batch.md +1 -1
  35. package/commands/gsd/review.md +2 -3
  36. package/gsd-core/bin/gsd-tools.cjs +335 -22
  37. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  38. package/gsd-core/bin/lib/audit.cjs +81 -13
  39. package/gsd-core/bin/lib/capability-registry.cjs +82 -187
  40. package/gsd-core/bin/lib/capability-validator.cjs +0 -1
  41. package/gsd-core/bin/lib/check-command-router.cjs +101 -14
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  43. package/gsd-core/bin/lib/commands.cjs +175 -42
  44. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  45. package/gsd-core/bin/lib/config.cjs +33 -7
  46. package/gsd-core/bin/lib/decisions.cjs +30 -14
  47. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  48. package/gsd-core/bin/lib/graphify.cjs +10 -2
  49. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  50. package/gsd-core/bin/lib/init.cjs +207 -41
  51. package/gsd-core/bin/lib/install-engine.cjs +13 -0
  52. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  53. package/gsd-core/bin/lib/milestone.cjs +18 -5
  54. package/gsd-core/bin/lib/model-resolver.cjs +159 -50
  55. package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
  56. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  57. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  58. package/gsd-core/bin/lib/phase-id.cjs +109 -7
  59. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  60. package/gsd-core/bin/lib/phase.cjs +227 -26
  61. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  62. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  63. package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
  64. package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
  65. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  66. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  67. package/gsd-core/bin/lib/project-root.cjs +41 -2
  68. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  69. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  70. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  71. package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
  72. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  73. package/gsd-core/bin/lib/roadmap.cjs +251 -31
  74. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
  75. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
  76. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  77. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
  78. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  79. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  80. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  81. package/gsd-core/bin/lib/state.cjs +42 -0
  82. package/gsd-core/bin/lib/surface.cjs +17 -1
  83. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  84. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  85. package/gsd-core/bin/lib/uat.cjs +8 -0
  86. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  87. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  88. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  89. package/gsd-core/bin/lib/verification.cjs +268 -15
  90. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  91. package/gsd-core/bin/lib/verify.cjs +132 -25
  92. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  93. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  94. package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
  95. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  96. package/gsd-core/references/checkpoints.md +5 -3
  97. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  98. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  99. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  100. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  101. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  102. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  103. package/gsd-core/references/edge-probe.md +195 -21
  104. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  105. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  106. package/gsd-core/references/gsd-run-resolver.md +1 -1
  107. package/gsd-core/references/model-profiles.md +1 -1
  108. package/gsd-core/references/phase-argument-parsing.md +9 -7
  109. package/gsd-core/references/phase-id-convention.md +28 -0
  110. package/gsd-core/references/planner-gap-closure.md +2 -0
  111. package/gsd-core/references/planner-load-graph-context.md +24 -13
  112. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  113. package/gsd-core/references/planning-config.md +11 -2
  114. package/gsd-core/references/tdd.md +27 -4
  115. package/gsd-core/references/ui-consideration-probe.md +10 -5
  116. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  117. package/gsd-core/references/worktree-path-safety.md +321 -0
  118. package/gsd-core/templates/verification-report.md +1 -1
  119. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  120. package/gsd-core/workflows/add-backlog.md +1 -1
  121. package/gsd-core/workflows/add-phase.md +1 -1
  122. package/gsd-core/workflows/add-tests.md +2 -2
  123. package/gsd-core/workflows/add-todo.md +3 -3
  124. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  125. package/gsd-core/workflows/audit-fix.md +1 -1
  126. package/gsd-core/workflows/audit-milestone.md +1 -1
  127. package/gsd-core/workflows/audit-uat.md +1 -1
  128. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  129. package/gsd-core/workflows/autonomous.md +16 -6
  130. package/gsd-core/workflows/check-todos.md +2 -2
  131. package/gsd-core/workflows/cleanup.md +2 -2
  132. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  133. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  134. package/gsd-core/workflows/code-review-fix.md +108 -22
  135. package/gsd-core/workflows/code-review.md +63 -46
  136. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
  137. package/gsd-core/workflows/complete-milestone.md +2 -2
  138. package/gsd-core/workflows/debug.md +3 -3
  139. package/gsd-core/workflows/diagnose-issues.md +1 -1
  140. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  141. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  142. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  143. package/gsd-core/workflows/discuss-phase.md +1 -1
  144. package/gsd-core/workflows/do.md +2 -2
  145. package/gsd-core/workflows/docs-update.md +3 -3
  146. package/gsd-core/workflows/edit-phase.md +1 -1
  147. package/gsd-core/workflows/eval-review.md +10 -3
  148. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  149. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  150. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  151. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
  152. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  153. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  154. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  155. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  156. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  157. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  158. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  159. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  160. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  161. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  162. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  163. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  164. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  165. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  166. package/gsd-core/workflows/execute-phase.md +36 -26
  167. package/gsd-core/workflows/execute-plan.md +5 -4
  168. package/gsd-core/workflows/explore.md +3 -3
  169. package/gsd-core/workflows/extract-learnings.md +2 -1
  170. package/gsd-core/workflows/fast.md +1 -1
  171. package/gsd-core/workflows/forensics.md +1 -1
  172. package/gsd-core/workflows/graduation.md +1 -1
  173. package/gsd-core/workflows/health.md +2 -2
  174. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  175. package/gsd-core/workflows/help/modes/full.md +5 -5
  176. package/gsd-core/workflows/help/modes/topic.md +15 -5
  177. package/gsd-core/workflows/import.md +2 -2
  178. package/gsd-core/workflows/inbox.md +2 -2
  179. package/gsd-core/workflows/ingest-docs.md +3 -3
  180. package/gsd-core/workflows/insert-phase.md +1 -1
  181. package/gsd-core/workflows/list-seeds.md +1 -1
  182. package/gsd-core/workflows/list-workspaces.md +1 -1
  183. package/gsd-core/workflows/manager.md +2 -2
  184. package/gsd-core/workflows/map-codebase.md +2 -2
  185. package/gsd-core/workflows/milestone-summary.md +1 -1
  186. package/gsd-core/workflows/mvp-phase.md +1 -1
  187. package/gsd-core/workflows/new-milestone.md +2 -2
  188. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  189. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  190. package/gsd-core/workflows/new-project.md +7 -7
  191. package/gsd-core/workflows/new-workspace.md +2 -2
  192. package/gsd-core/workflows/next.md +1 -1
  193. package/gsd-core/workflows/note.md +1 -1
  194. package/gsd-core/workflows/onboard.md +1 -1
  195. package/gsd-core/workflows/pause-work.md +1 -1
  196. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  197. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  198. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  199. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  200. package/gsd-core/workflows/plan-phase.md +24 -7
  201. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  202. package/gsd-core/workflows/plant-seed.md +62 -20
  203. package/gsd-core/workflows/pr-branch.md +113 -13
  204. package/gsd-core/workflows/profile-user.md +2 -2
  205. package/gsd-core/workflows/progress.md +1 -1
  206. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  207. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  208. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  209. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  210. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  211. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  212. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  213. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  214. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  215. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  216. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  217. package/gsd-core/workflows/quick-batch.md +1 -1
  218. package/gsd-core/workflows/quick.md +21 -9
  219. package/gsd-core/workflows/reapply-patches.md +9 -3
  220. package/gsd-core/workflows/remove-phase.md +1 -1
  221. package/gsd-core/workflows/remove-workspace.md +2 -2
  222. package/gsd-core/workflows/resume-project.md +1 -1
  223. package/gsd-core/workflows/review.md +31 -16
  224. package/gsd-core/workflows/scan.md +1 -1
  225. package/gsd-core/workflows/secure-phase.md +3 -2
  226. package/gsd-core/workflows/settings-advanced.md +30 -10
  227. package/gsd-core/workflows/settings-integrations.md +2 -3
  228. package/gsd-core/workflows/settings.md +4 -4
  229. package/gsd-core/workflows/ship.md +3 -2
  230. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  231. package/gsd-core/workflows/sketch.md +1 -1
  232. package/gsd-core/workflows/smart-entry.md +2 -2
  233. package/gsd-core/workflows/spec-phase.md +15 -5
  234. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  235. package/gsd-core/workflows/spike.md +1 -1
  236. package/gsd-core/workflows/stats.md +1 -1
  237. package/gsd-core/workflows/sync-skills.md +5 -5
  238. package/gsd-core/workflows/thread.md +1 -1
  239. package/gsd-core/workflows/transition.md +1 -1
  240. package/gsd-core/workflows/ui-phase.md +44 -8
  241. package/gsd-core/workflows/ui-review.md +18 -4
  242. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  243. package/gsd-core/workflows/undo.md +339 -20
  244. package/gsd-core/workflows/update.md +7 -7
  245. package/gsd-core/workflows/validate-phase.md +3 -2
  246. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  247. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  248. package/gsd-core/workflows/verify-work.md +81 -16
  249. package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
  250. package/hooks/dist/gsd-secret-read-guard.js +27 -1
  251. package/hooks/dist/gsd-statusline.js +70 -13
  252. package/hooks/dist/gsd-validate-commit.sh +63 -4
  253. package/hooks/gsd-agent-isolation-guard.js +24 -0
  254. package/hooks/gsd-secret-read-guard.js +27 -1
  255. package/hooks/gsd-statusline.js +70 -13
  256. package/hooks/gsd-validate-commit.sh +63 -4
  257. package/package.json +3 -2
  258. package/scripts/build-hooks.js +15 -6
  259. package/scripts/check-contract-drift.cjs +127 -11
  260. package/scripts/command-contract-helpers.cjs +3 -0
  261. package/scripts/docs-guard-registry.cjs +28 -0
  262. package/scripts/gen-loop-host-contract.cjs +69 -0
  263. package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
  264. package/scripts/lib/ndjson-reporter.cjs +3 -2
  265. package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
  266. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
  267. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  268. package/scripts/lint-phase-id-drift.cjs +290 -5
  269. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  270. package/scripts/lint-retired-runtime-name.cjs +619 -0
  271. package/scripts/lint-state-write-path-drift.cjs +93 -0
  272. package/scripts/lint-test-file-count.allowlist.json +28 -9
  273. package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
  274. package/scripts/prompt-injection-scan.sh +4 -0
  275. package/scripts/release-tarball-smoke.cjs +194 -1
  276. package/skills/gsd-autonomous/SKILL.md +2 -2
  277. package/skills/gsd-capture/SKILL.md +1 -1
  278. package/skills/gsd-mempalace-capture/SKILL.md +7 -3
  279. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  280. package/skills/gsd-progress/SKILL.md +1 -1
  281. package/skills/gsd-quick-batch/SKILL.md +1 -1
  282. package/skills/gsd-review/SKILL.md +2 -3
  283. package/vscode/package.json +1 -1
@@ -19,6 +19,10 @@
19
19
  * state signal-resume Remove WAITING.json signal
20
20
  * resolve-model <agent-type> Get model for agent based on profile
21
21
  * find-phase <phase> Find phase directory by number
22
+ * select-revert-commits (--phase P | --plan NN-MM) --range R
23
+ * List commits within R whose DECLARED conventional-commit
24
+ * scope identifies P or NN-MM (exact match; phase mode also
25
+ * accepts a NN-MM-scoped commit). Used by /gsd:undo (#4661).
22
26
  * commit <message> [--files f1 f2] [--no-verify] Commit planning docs
23
27
  * commit-docs-guard enable|disable Opt-in .git/hooks/pre-commit guard
24
28
  * that refuses a commit staging
@@ -60,7 +64,7 @@
60
64
  * roadmap update-plan-progress <N> Update progress table row from disk (PLAN vs SUMMARY counts)
61
65
  * roadmap annotate-dependencies <N> Add wave dependency notes + cross-cutting constraints to ROADMAP.md
62
66
  * roadmap validate Validate phase ID convention compliance
63
- * roadmap upgrade [--apply] --convention milestone-prefixed Migrate phase IDs to M-NN convention
67
+ * roadmap upgrade [--apply] --convention <milestone-prefixed|bracket> Migrate phase IDs (dry-run by default)
64
68
  *
65
69
  * Requirements Operations:
66
70
  * requirements mark-complete <ids> Mark requirement IDs as complete in REQUIREMENTS.md
@@ -949,6 +953,219 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
949
953
  if (!handled) phase.cmdFindPhase(cwd, args[1], raw);
950
954
  }
951
955
 
956
+ /**
957
+ * #4906 Phase 5 (issue #4661, absorbed from the original bug report):
958
+ * `gsd-tools.cjs query select-revert-commits --phase <id>|--plan <id>
959
+ * --range <git-revision-range>`.
960
+ *
961
+ * Replaces the two `git log --oneline | grep -E ...` pipelines
962
+ * `gsd-core/workflows/undo.md` used to run for `/gsd:undo --phase`/`--plan`
963
+ * selection. `--range` is the ALREADY-COMPUTED `$UNDO_RANGE` the workflow's
964
+ * own phase-directory anchor logic derives — this subcommand changes only
965
+ * the subject-matching step, never the ranging behavior.
966
+ *
967
+ * Validates `--phase`/`--range <id>` via `validatePhaseNumber`
968
+ * (src/security.cts) BEFORE running any git command — an invalid id is
969
+ * refused here, never reaching git or the selection regex (closes bug
970
+ * class 1 of #4661 by construction). A `--plan NN-MM` id is two
971
+ * phase-number-shaped segments joined by the FIRST `-`: `validatePhaseNumber`'s
972
+ * bracket-style alternative requires a leading letter, so a plain numeric
973
+ * plan id never matches it whole — each segment is validated on its own
974
+ * instead of forking a second grammar.
975
+ *
976
+ * Selection itself is delegated to `selectCommitsByDeclaredScope`
977
+ * (src/undo-commit-selection.cts), which parses each commit subject
978
+ * through the SAME anchored conventional-commit header regex the
979
+ * changelog/PR-title gate uses and compares the declared scope to the
980
+ * validated id by exact string equality — no regex is ever built from
981
+ * user input.
982
+ *
983
+ * Output: JSON `{selected, classified}` by default; with `--raw`, one
984
+ * `<full-sha> <subject>` line per selected commit (preserves the visual
985
+ * shape `git log --oneline` produced, so the workflow's downstream
986
+ * confirm-screen display, >50 truncation count, and `git revert
987
+ * --no-commit` loop over COMMITS are unchanged).
988
+ */
989
+ function routeSelectRevertCommits({ args, cwd, raw, error, invokingCwd }) {
990
+ // #4906/#4465 review fix: `cwd` here has already been remapped to the
991
+ // MAIN worktree root by `resolveMainWorktreeCwd` (main(), applied
992
+ // BLANKET to every command before dispatch — correct for a router that
993
+ // reads `.planning/`, e.g. `find-phase`, but wrong for this one). This
994
+ // command reads NO `.planning/` content — it only runs `git log` — and
995
+ // that git operation must see the CALLER's own worktree: its HEAD, its
996
+ // branch, its reachable commit range. Using the remapped `cwd` instead
997
+ // silently resolves `--range` against a DIFFERENT repository checkout
998
+ // (reproduced: from a linked worktree with no local `.planning/`, a
999
+ // `--range HEAD~3..HEAD` that succeeds when run directly in that
1000
+ // worktree fails with "ambiguous argument" through this command,
1001
+ // because it silently ran against main's shorter HEAD instead —
1002
+ // tests/undo-commit-selection-4465.test.cjs, "linked worktree: the main
1003
+ // worktree's planning is the same repository, and is not refused").
1004
+ // `invokingCwd` (threaded from main() through dispatchHostCommand) is
1005
+ // the pre-remap value; falling back to `cwd` keeps direct callers that
1006
+ // omit it (e.g. `dispatchHostCommand` invoked without the new field)
1007
+ // working exactly as before rather than crashing on `undefined`.
1008
+ const gitCwd = invokingCwd || cwd;
1009
+ const { validatePhaseNumber } = require('./lib/security.cjs');
1010
+ const { normalizePhaseName } = require('./lib/phase-id.cjs');
1011
+ const { selectCommitsByDeclaredScope } = require('./lib/undo-commit-selection.cjs');
1012
+ const { execGit } = require('./lib/shell-command-projection.cjs');
1013
+
1014
+ // #4906 Phase 5 review fix: the RETIRED phase-mode grep's `0*` prefix
1015
+ // (`\(0*${TARGET_PHASE}(-[0-9]+)?\):`) tolerated an UNPADDED user-supplied
1016
+ // phase number against a zero-padded commit scope — `--phase 3` matched
1017
+ // `feat(03-01): ...` because `0*3` matches the literal text `03`.
1018
+ // `selectCommitsByDeclaredScope` compares by exact string equality, and
1019
+ // `gsd-core/workflows/undo.md`'s parse_arguments step passes `TARGET_PHASE`
1020
+ // through with NO normalization, so without this, `--phase 3` would
1021
+ // silently select nothing against real (zero-padded) commit scopes — a
1022
+ // behavior regression, not just a documented known-limit. Zero-pad via
1023
+ // `normalizePhaseName` (src/phase-id.cts), the canonical owner of this
1024
+ // exact operation (confirmed: `normalizePhaseName('3') === '03'`).
1025
+ //
1026
+ // GUARDED to digit-first ids only: `normalizePhaseName` also runs
1027
+ // `stripProjectCodePrefix`, which strips a leading `LETTERS-`-shaped
1028
+ // prefix UNCONDITIONALLY — `normalizePhaseName('PROJ-42') === '42'`,
1029
+ // discarding the "PROJ-" entirely (confirmed by direct call). That is
1030
+ // exactly the shape `validatePhaseNumber`'s bracket-style alternative
1031
+ // legitimately accepts as a phase id in ITS OWN right (`PROJ-42`,
1032
+ // `AUTH-101` — see tests/security.test.cjs). Running a letter-first id
1033
+ // through it would silently widen the match (a wildcard-style
1034
+ // over-match reintroducing bug classes 1/2). The old grep's `0*` was
1035
+ // never meaningful for a letter-first target anyway — `0*` before a
1036
+ // non-'0' character matches zero repetitions, a no-op — so a
1037
+ // letter-first id needs neither padding nor this normalization.
1038
+ // `PROJECT_CODE_PREFIX_STRIP_RE_I` (`src/phase-id.cts:33`) itself
1039
+ // requires `^[A-Z]`, so it can never fire on a digit-first id — the
1040
+ // guard below is therefore exactly the safe/unsafe boundary.
1041
+ const normalizePaddedPhase = (id) => (/^\d/.test(id) ? normalizePhaseName(id) : id);
1042
+
1043
+ const parsed = parseNamedArgsOrExit(
1044
+ args,
1045
+ { valueFlags: ['phase', 'plan', 'range'], positionals: 1 },
1046
+ error,
1047
+ );
1048
+ const rawPhase = parsed.phase;
1049
+ const rawPlan = parsed.plan;
1050
+ const range = parsed.range;
1051
+
1052
+ if ((rawPhase && rawPlan) || (!rawPhase && !rawPlan)) {
1053
+ error('select-revert-commits requires exactly one of --phase <id> or --plan <id>', ERROR_REASON.USAGE);
1054
+ return;
1055
+ }
1056
+ if (!range) {
1057
+ error('select-revert-commits requires --range <git-revision-range>', ERROR_REASON.USAGE);
1058
+ return;
1059
+ }
1060
+ // Belt-and-suspenders alongside the --end-of-options guard below (mirrors
1061
+ // src/git-base-branch.cts's isSafeRevisionRef posture for the same class
1062
+ // of argument): refuse an option-shaped range before it ever reaches git.
1063
+ if (typeof range !== 'string' || range.startsWith('-')) {
1064
+ error(`Invalid --range: ${JSON.stringify(range)}`, ERROR_REASON.USAGE);
1065
+ return;
1066
+ }
1067
+
1068
+ let mode;
1069
+ let normalizedId;
1070
+ if (rawPhase) {
1071
+ mode = 'phase';
1072
+ const check = validatePhaseNumber(rawPhase);
1073
+ if (!check.valid) {
1074
+ error(`Invalid --phase: ${check.error}`, ERROR_REASON.USAGE);
1075
+ return;
1076
+ }
1077
+ normalizedId = normalizePaddedPhase(check.normalized);
1078
+ } else {
1079
+ mode = 'plan';
1080
+ // Split on the FIRST `-` only, giving exactly two segments (phase,
1081
+ // plan). This is deliberately NOT a general N-segment composite-id
1082
+ // parser: checked against docs/reference/plan-md.md (frontmatter
1083
+ // `plan` field + `.planning/phases/<NN>-<slug>/<NN>-<PP>-PLAN.md`
1084
+ // layout) and every `--plan` usage in gsd-core/workflows/*.md, the only
1085
+ // plan-id shape this repo documents or emits is the plain two-segment
1086
+ // `NN-MM`. A THREE-segment purely-numeric shape (`NN-MM-PP`) does exist
1087
+ // elsewhere in the codebase, but as a MILESTONE-phase-plan composite
1088
+ // under the (already-deprecated-forward) `milestone-prefixed`
1089
+ // `phase_id_convention` — a different id space than phase-plan, which
1090
+ // `docs/adr/612-bracket-phase-id-convention.md:15` itself documents as
1091
+ // having "no deterministic parse" once a token carries both a
1092
+ // milestone-joined phase AND a plan (the exact ambiguity that
1093
+ // motivated bracket's own `[PROJECT.MM] PP-PP` grammar). No function in
1094
+ // `src/phase-id.cts` validates the phase-PLAN grammar end-to-end
1095
+ // either: `parsePhaseId` explicitly REJECTS a bare `NN-MM` token by
1096
+ // design (its own doc comment lists `02-04` as a rejected "ambiguous /
1097
+ // bare token"), and `getPhaseDirFromPhaseId`'s N-segment dash grammar
1098
+ // is a MILESTONE-phase(-subphase) directory-name constructor — a
1099
+ // structurally similar but semantically different id space (first
1100
+ // segment = milestone, not phase) whose return value doesn't map back
1101
+ // to (phase, plan) anyway. Given no suitable whole-string validator
1102
+ // exists and no real usage needs more than two segments, a `NN-MM-PP`
1103
+ // id is refused today (see
1104
+ // tests/undo-commit-selection.test.cjs "refuses a 3-segment plan id"),
1105
+ // not silently mis-parsed — extending this to N segments is a product
1106
+ // decision for whichever future issue actually needs it. The same
1107
+ // "only the plain two-segment shape is supported" standard applies to
1108
+ // a bracket-style PHASE segment here too: `--plan PROJ-42-01` splits
1109
+ // on the first `-` into phase segment `PROJ` (no internal dash) and
1110
+ // plan segment `42-01`, and `validatePhaseNumber`'s bracket branch
1111
+ // requires the internal dash on ITS side (`PROJ-42`, not bare `PROJ`),
1112
+ // so the phase segment fails and the whole id is refused — bracket-style
1113
+ // ids are supported for `--phase` only, never as a `--plan` segment (no
1114
+ // real usage found; see tests/undo-commit-selection.test.cjs "bracket-
1115
+ // style phase segment in --plan is refused").
1116
+ const dashIdx = rawPlan.indexOf('-');
1117
+ if (dashIdx === -1) {
1118
+ error(`Invalid --plan: "${rawPlan}" (expected NN-MM)`, ERROR_REASON.USAGE);
1119
+ return;
1120
+ }
1121
+ const phasePart = rawPlan.slice(0, dashIdx);
1122
+ const planPart = rawPlan.slice(dashIdx + 1);
1123
+ const phaseCheck = validatePhaseNumber(phasePart);
1124
+ if (!phaseCheck.valid) {
1125
+ error(`Invalid --plan: "${rawPlan}" (phase segment: ${phaseCheck.error})`, ERROR_REASON.USAGE);
1126
+ return;
1127
+ }
1128
+ const planCheck = validatePhaseNumber(planPart);
1129
+ if (!planCheck.valid) {
1130
+ error(`Invalid --plan: "${rawPlan}" (plan segment: ${planCheck.error})`, ERROR_REASON.USAGE);
1131
+ return;
1132
+ }
1133
+ normalizedId = `${normalizePaddedPhase(phaseCheck.normalized)}-${normalizePaddedPhase(planCheck.normalized)}`;
1134
+ }
1135
+
1136
+ const GIT_LOG_TIMEOUT_MS = 15000;
1137
+ const gitResult = execGit(
1138
+ ['log', '--format=%H%x00%s', '--no-merges', '--no-decorate', '--end-of-options', range],
1139
+ { cwd: gitCwd, timeout: GIT_LOG_TIMEOUT_MS },
1140
+ );
1141
+ if (gitResult.exitCode !== 0) {
1142
+ // `execGit` (src/shell-command-projection.cts) exposes `timedOut` on
1143
+ // its SpawnResultOutput — thread it through so a hung `git log` reads
1144
+ // distinctly from an ordinary non-zero exit with no stderr, which
1145
+ // otherwise both surfaced as the same unhelpful "(no stderr)" message.
1146
+ const gitFailureDetail = gitResult.timedOut
1147
+ ? `git log timed out after ${GIT_LOG_TIMEOUT_MS}ms`
1148
+ : `git log failed: ${gitResult.stderr || '(no stderr)'}`;
1149
+ error(
1150
+ `select-revert-commits: ${gitFailureDetail} for range "${range}"`,
1151
+ ERROR_REASON.USAGE,
1152
+ );
1153
+ return;
1154
+ }
1155
+
1156
+ const commits = (gitResult.stdout ? gitResult.stdout.split('\n') : [])
1157
+ .filter((line) => line.length > 0)
1158
+ .map((line) => {
1159
+ const nulIdx = line.indexOf('\0');
1160
+ return nulIdx === -1 ? null : { sha: line.slice(0, nulIdx), subject: line.slice(nulIdx + 1) };
1161
+ })
1162
+ .filter((c) => c !== null);
1163
+
1164
+ const result = selectCommitsByDeclaredScope(commits, normalizedId, mode);
1165
+ const rawLines = result.selected.map((c) => `${c.sha} ${c.subject}`).join('\n');
1166
+ output(result, raw, rawLines);
1167
+ }
1168
+
952
1169
  function routeCommit({ args, cwd, raw, error }) {
953
1170
  const amend = args.includes('--amend');
954
1171
  const noVerify = args.includes('--no-verify');
@@ -1183,7 +1400,18 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1183
1400
  // First positional that isn't a flag also works (lenient); otherwise ignore unknown flags.
1184
1401
  if (!a.startsWith('-') && !pifRuntime) { pifRuntime = a; }
1185
1402
  }
1186
- const filename = getProjectInstructionFile(pifRuntime);
1403
+ // A retired runtime id now THROWS rather than resolving (#4709 AC#1).
1404
+ // Map it to the same clean single-line error routeSkillsRoot emits for
1405
+ // an unknown runtime — a CLI must not answer a bad flag value with a
1406
+ // stack trace. The thrown message already names the successor and the
1407
+ // retiring issue, so it is surfaced verbatim.
1408
+ let filename;
1409
+ try {
1410
+ filename = getProjectInstructionFile(pifRuntime);
1411
+ } catch (err) {
1412
+ if (err && err.code === 'GSD_RETIRED_RUNTIME') error(err.message);
1413
+ throw err;
1414
+ }
1187
1415
  process.stdout.write(filename + '\n');
1188
1416
  }
1189
1417
 
@@ -1252,9 +1480,21 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1252
1480
  // claims. `--directory` wins outright when given explicitly;
1253
1481
  // otherwise a supplied `--quick-id` + `--slug` pair derives the
1254
1482
  // canonical permalink the same way `workflows/quick.md` renders it.
1483
+ // #4906 Phase 3 (#4958, evidence #4736): `--status` is the OPTIONAL
1484
+ // widening that closes the last gap between this CLI and
1485
+ // `workflows/quick.md`'s own Step 7c row shapes. `appendQuickTaskRow`
1486
+ // (markdown-table.cjs) has always accepted `status` — the schema's
1487
+ // `Status` column exists precisely for `$VALIDATE_MODE` runs — but
1488
+ // nothing on this CLI surface could set it, so quick.md's
1489
+ // VALIDATE_MODE branch had no escaping-safe path and kept
1490
+ // interpolating `${DESCRIPTION}` into raw markdown (#4736 D1: an
1491
+ // unescaped `|` in a task description permanently rags the table).
1492
+ // Omitted, `status` stays undefined and `appendQuickTaskRow` falls
1493
+ // back to its own `'—'` default — unchanged for every existing
1494
+ // caller (fast.md, quick.md's non-VALIDATE_MODE branch).
1255
1495
  const qtaParsed = parseNamedArgsOrExit(
1256
1496
  qtaArgs,
1257
- { valueFlags: ['task', 'quick-id', 'slug', 'directory'], positionals: 'rest' },
1497
+ { valueFlags: ['task', 'quick-id', 'slug', 'directory', 'status'], positionals: 'rest' },
1258
1498
  error,
1259
1499
  );
1260
1500
  const qtaTask = qtaParsed.task || args[1];
@@ -1265,6 +1505,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1265
1505
  const qtaSlug = qtaParsed['slug'] || undefined;
1266
1506
  const qtaDirectory = qtaParsed['directory']
1267
1507
  || (qtaQuickId && qtaSlug ? `[${qtaQuickId}-${qtaSlug}](./quick/${qtaQuickId}-${qtaSlug}/)` : undefined);
1508
+ const qtaStatus = qtaParsed['status'] || undefined;
1268
1509
 
1269
1510
  const statePath = path.join(cwd, '.planning', 'STATE.md');
1270
1511
  if (!fs.existsSync(statePath)) {
@@ -1302,6 +1543,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1302
1543
  description: qtaTask,
1303
1544
  date,
1304
1545
  commit,
1546
+ status: qtaStatus,
1305
1547
  quickId: qtaQuickId,
1306
1548
  directory: qtaDirectory,
1307
1549
  });
@@ -3006,6 +3248,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
3006
3248
  worktreeSafety.cmdWorktreeRecordAgent(cwd, args.slice(2));
3007
3249
  } else if (subcommand === 'reap-orphans') {
3008
3250
  worktreeSafety.cmdWorktreeReapOrphans(cwd);
3251
+ } else if (subcommand === 'worker-record') {
3252
+ worktreeSafety.cmdWorktreeWorkerRecord(cwd, args.slice(2));
3253
+ } else if (subcommand === 'worker-status') {
3254
+ worktreeSafety.cmdWorktreeWorkerStatus(cwd, args.slice(2));
3255
+ } else if (subcommand === 'worker-complete') {
3256
+ worktreeSafety.cmdWorktreeWorkerComplete(cwd, args.slice(2));
3009
3257
  } else if (subcommand === 'base-check') {
3010
3258
  require('./lib/worktree-base-ref.cjs').cmdWorktreeBaseCheck(cwd, args.slice(2));
3011
3259
  } else if (subcommand === 'set-baseref') {
@@ -3013,7 +3261,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
3013
3261
  } else if (subcommand === 'create') {
3014
3262
  worktreeSafety.cmdWorktreeCreate(cwd, args.slice(2));
3015
3263
  } else {
3016
- error('Unknown worktree subcommand. Available: cleanup-wave, record-agent, reap-orphans, base-check, set-baseref, create', ERROR_REASON.SDK_UNKNOWN_COMMAND);
3264
+ error('Unknown worktree subcommand. Available: cleanup-wave, record-agent, reap-orphans, base-check, set-baseref, create, worker-record, worker-status, worker-complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
3017
3265
  }
3018
3266
  }
3019
3267
 
@@ -4407,6 +4655,8 @@ const HOST_COMMAND_ROUTERS = {
4407
4655
  'smart-entry': routeSmartEntry,
4408
4656
  'check': routeCheck,
4409
4657
  'find-phase': routeFindPhase,
4658
+ // #4906 Phase 5 (#4661): commit-scope selection for /gsd:undo --phase/--plan.
4659
+ 'select-revert-commits': routeSelectRevertCommits,
4410
4660
  'commit': routeCommit,
4411
4661
  'check-commit': routeCheckCommit,
4412
4662
  'commit-docs-guard': routeCommitDocsGuard,
@@ -4484,7 +4734,7 @@ const HOST_COMMAND_ROUTERS = {
4484
4734
  // through. Prototype-pollution-safe: own-property lookup rejects
4485
4735
  // `__proto__`/`constructor`/`prototype` command keys (same guard as
4486
4736
  // dispatchCapabilityCommand).
4487
- async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext }) {
4737
+ async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd }) {
4488
4738
  if (
4489
4739
  command === '__proto__' ||
4490
4740
  command === 'constructor' ||
@@ -4499,7 +4749,13 @@ async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValu
4499
4749
  if (typeof router !== 'function') return false;
4500
4750
  // `await` so async host routers (e.g. capability's install/upgrade ops)
4501
4751
  // complete before runCommand returns; sync routers pass through unchanged.
4502
- await router({ args, cwd, raw, error, defaultValue, workstreamContext });
4752
+ // `invokingCwd` (#4906/#4465): the pre-worktree-remap cwd, for the rare
4753
+ // router (currently only `select-revert-commits`) whose own git operation
4754
+ // must see the INVOKING worktree's HEAD, never the main-worktree remap
4755
+ // every other router legitimately wants via `cwd`. Optional — callers/tests
4756
+ // that omit it (every existing one) leave routers that don't read it
4757
+ // byte-unaffected.
4758
+ await router({ args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd });
4503
4759
  return true; // consumed — don't emit "Unknown command"
4504
4760
  }
4505
4761
 
@@ -4532,7 +4788,7 @@ async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValu
4532
4788
  // keep working. No shell is spawned (argv array) — no injection surface beyond
4533
4789
  // the old `timeout … bash -c "$CMD"`.
4534
4790
  function runWithTimeout(argv) {
4535
- const { spawn } = require('node:child_process');
4791
+ const { spawn, spawnSync } = require('node:child_process');
4536
4792
  const os = require('node:os');
4537
4793
 
4538
4794
  const USAGE = 'Usage: gsd_run run-with-timeout <seconds> [--] <command> [args...]';
@@ -4557,9 +4813,11 @@ function runWithTimeout(argv) {
4557
4813
 
4558
4814
  const isWin = process.platform === 'win32';
4559
4815
  // Detached (own process group) on POSIX so a timeout can reap the WHOLE tree —
4560
- // a bare child.kill() misses grandchildren (e.g. a test runner's workers) and
4561
- // would not actually bound the wall clock. Windows has no POSIX process
4562
- // groups; a direct kill is the best portable option there.
4816
+ // a bare child.kill() misses grandchildren (e.g. a test runner's workers).
4817
+ // Windows has no POSIX process groups and process.kill(-pid) is unsupported
4818
+ // there, so EVERY killTree attempt on Windows tree-kills via
4819
+ // `taskkill /PID <pid> /T /F` while the root is alive (see killTree) — by the
4820
+ // time the direct child exits, its descendants are already orphaned.
4563
4821
  const detached = !isWin && secs > 0;
4564
4822
  const spawnFailureCode = (err) =>
4565
4823
  (err && err.code === 'ENOENT' ? 127 : err && err.code === 'EACCES' ? 126 : 125);
@@ -4583,9 +4841,26 @@ function runWithTimeout(argv) {
4583
4841
  // cap's process-group kill (the wrapped child escapes reap → exit 124 never
4584
4842
  // fires) and risks cmd.exe mis-parsing an arg like `-e "setTimeout(()=>{})"`.
4585
4843
  // Only .cmd/.bat are the CVE-2024-27980 EINVAL cases that require mediation.
4844
+ // #4797: the mediation is NOT hand-rolled here — the private `/d /s /c <cmd>
4845
+ // ...args` copy broke on any shim path containing a space (Node quotes the
4846
+ // argv token; `/s` strips the FIRST and LAST quote of the /c string, so
4847
+ // cmd.exe took the pre-space fragment as the program). projectSpawnInvocation
4848
+ // (the declared single owner, #3411/#3617) wraps the WHOLE command line in
4849
+ // one extra quote pair with windowsVerbatimArguments — the shape that
4850
+ // survives spaces. One behavior delta, accepted: the seam DECLINES mediation
4851
+ // when the target or an arg carries CR/LF (the old block mediated anyway) —
4852
+ // the unmediated spawn of a .cmd then fails EINVAL, loud, at the catch below.
4586
4853
  const winShim = isWin && /\.(cmd|bat)$/i.test(path.basename(cmd));
4587
- const spawnCmd = winShim ? (process.env.ComSpec || 'cmd.exe') : cmd;
4588
- const spawnArgs = winShim ? ['/d', '/s', '/c', cmd, ...cmdArgs] : cmdArgs;
4854
+ let spawnCmd = cmd;
4855
+ let spawnArgs = cmdArgs;
4856
+ let spawnOpts;
4857
+ if (winShim) {
4858
+ const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
4859
+ const inv = projectSpawnInvocation(cmd, cmdArgs);
4860
+ spawnCmd = inv.command;
4861
+ spawnArgs = inv.args;
4862
+ if (inv.windowsVerbatimArguments) spawnOpts = { windowsVerbatimArguments: true };
4863
+ }
4589
4864
  // Node's setTimeout delay is a 32-bit signed ms int; a larger value silently
4590
4865
  // clamps to 1ms → a spurious immediate timeout. Cap the budget (~24.8 days).
4591
4866
  const timerMs = Math.min(Math.round(secs * 1000), 2 ** 31 - 1);
@@ -4596,11 +4871,13 @@ function runWithTimeout(argv) {
4596
4871
  return new Promise((resolve) => {
4597
4872
  let child;
4598
4873
  try {
4599
- // #2667: on win32 `.cmd`/`.bat`/`.exe`, spawn cmd.exe with an explicit argv
4600
- // array (spawnCmd/spawnArgs) rather than the shim directly — preserves the
4601
- // array-only, no-shell-string argv contract. `detached` is always false on
4602
- // win32, so it never co-occurs with the cmd.exe mediation.
4603
- child = spawn(spawnCmd, spawnArgs, { stdio: 'inherit', detached });
4874
+ // #2667: on win32 `.cmd`/`.bat` shims, spawn cmd.exe with an explicit
4875
+ // argv ARRAY rather than the shim directly — preserves the array-only,
4876
+ // no-shell-string argv contract. #4797: the exact argv shape (quote
4877
+ // wrapping, verbatim arguments) is projected by projectSpawnInvocation —
4878
+ // see the block above. `detached` is always false on win32, so it never
4879
+ // co-occurs with the cmd.exe mediation.
4880
+ child = spawn(spawnCmd, spawnArgs, { stdio: 'inherit', detached, ...spawnOpts });
4604
4881
  } catch (err) {
4605
4882
  process.stderr.write(`run-with-timeout: ${cmd}: ${err && err.message ? err.message : 'failed to start'}\n`);
4606
4883
  resolve(spawnFailureCode(err));
@@ -4612,6 +4889,27 @@ function runWithTimeout(argv) {
4612
4889
  if (detached && child.pid) {
4613
4890
  try { process.kill(-child.pid, signal); return; } catch { /* group already gone */ }
4614
4891
  }
4892
+ if (isWin && child.pid) {
4893
+ // #4601: Windows has no POSIX process groups, so the tree kill rides
4894
+ // on `taskkill /T`, which walks the child's descendants the way
4895
+ // `process.kill(-pid)` reaches a POSIX group — this is what bounds
4896
+ // the wall clock when the direct child mediates (cmd.exe /c shim) or
4897
+ // spawns its own children. Deliberately NOT gated on the SIGKILL
4898
+ // stage: child.kill on Windows is TerminateProcess regardless of
4899
+ // signal, so by the time the direct child exits its descendants are
4900
+ // orphaned and no taskkill can reach them — the tree kill must ride
4901
+ // the FIRST attempt, while the root is still alive. /F is required:
4902
+ // without it taskkill posts WM_CLOSE, which a headless CLI never
4903
+ // pumps. Spawned as an argv array per the no-shell-for-argv-array
4904
+ // contract, and bounded — a non-zero/absent status means taskkill
4905
+ // lost a race with an exiting process, and we fall through to the
4906
+ // direct kill so the attempt is never weaker than before.
4907
+ const reap = spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
4908
+ encoding: 'utf8',
4909
+ timeout: 15000, // taskkill /T is sub-second in practice; bounded so a wedged taskkill can't hang the gate
4910
+ });
4911
+ if (reap.status === 0) return;
4912
+ }
4615
4913
  child.kill(signal);
4616
4914
  } catch { /* already exited */ }
4617
4915
  };
@@ -4694,7 +4992,7 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
4694
4992
  'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, planning, profile-questionnaire, ' +
4695
4993
  '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, ' +
4696
4994
  '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, ' +
4697
- 'resolve-execution, review-lane, skill-manifest, skills-root, stamp-codebase-map, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
4995
+ 'resolve-execution, review-lane, select-revert-commits, skill-manifest, skills-root, stamp-codebase-map, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
4698
4996
  'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
4699
4997
  'Global flags:\n' +
4700
4998
  ' --raw Emit raw output without post-processing\n' +
@@ -4916,10 +5214,25 @@ async function main() {
4916
5214
  cwd = resolvedProjectDir;
4917
5215
  projectDirExplicit = true;
4918
5216
  }
5217
+ // #4894: verification derives its root from a phase directory, not `cwd`, so
5218
+ // hand it the validated explicit root. Always (re)set — `null` when the flag is
5219
+ // absent — so a prior in-process main() call can never leak an override.
5220
+ projectRoot.setExplicitProjectRoot(projectDirExplicit ? cwd : null);
4919
5221
 
4920
5222
  // Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
4921
5223
  // However, in monorepo worktrees where the subdirectory itself owns .planning/,
4922
5224
  // skip worktree resolution — the CWD is already the correct project root.
5225
+ //
5226
+ // #4906/#4465: this remap is a BLANKET rewrite applied to every command, before
5227
+ // `command` is even known — most commands need it (they read `.planning/`, which
5228
+ // in a linked worktree lives only in the main one). A command whose own git
5229
+ // operation must run against the INVOKING worktree's OWN HEAD/branch (never the
5230
+ // main worktree's) needs the PRE-remap value instead — preserved here so it can be
5231
+ // threaded through dispatchHostCommand as `invokingCwd` (see routeSelectRevertCommits,
5232
+ // the first consumer: its `git log` must see the caller's own reachable history, not
5233
+ // main's, or a linked-worktree revert range silently resolves against the wrong repo
5234
+ // state — reproduced in tests/undo-commit-selection-4465.test.cjs's linked-worktree case).
5235
+ const preWorktreeRemapCwd = cwd;
4923
5236
  cwd = resolveMainWorktreeCwd(cwd);
4924
5237
 
4925
5238
  // Optional workstream override for parallel milestone work.
@@ -5045,7 +5358,7 @@ async function main() {
5045
5358
  // false "output was not JSON" (negative space N8).
5046
5359
  if (pickField) {
5047
5360
  const captured = await captureStdoutSyncWrites(async () => {
5048
- await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
5361
+ await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd);
5049
5362
  });
5050
5363
  const resolved = resolveAtFileOutput(captured);
5051
5364
  let obj;
@@ -5077,7 +5390,7 @@ async function main() {
5077
5390
  // every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
5078
5391
  // that breaks on PowerShell and other non-bash shells.
5079
5392
  const captured = await captureStdoutSyncWrites(async () => {
5080
- await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
5393
+ await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd);
5081
5394
  });
5082
5395
  fs.writeSync(1, resolveAtFileOutput(captured));
5083
5396
  }
@@ -5193,7 +5506,7 @@ function extractField(obj, fieldPath) {
5193
5506
  return { found: true, value: current };
5194
5507
  }
5195
5508
 
5196
- async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) {
5509
+ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null, invokingCwd) {
5197
5510
  switch (command) {
5198
5511
 
5199
5512
  default: {
@@ -5214,7 +5527,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
5214
5527
  // commands (state, …) routed via their `route*Command` router instead of
5215
5528
  // a hardcoded `case` arm. Tried after capability/overlay dispatch and
5216
5529
  // before the unknown-command error.
5217
- if (await dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext })) break;
5530
+ if (await dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd })) break;
5218
5531
 
5219
5532
  // #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
5220
5533
  // above split it so `command` here is the head ("foo"). Use
@@ -52,6 +52,7 @@ const CANONICAL_HEADERS = {
52
52
  'recommendation',
53
53
  'strategy',
54
54
  'decision outcome',
55
+ 'locked decisions',
55
56
  ],
56
57
  considered_options: [
57
58
  'considered options',
@@ -190,7 +191,8 @@ function normalizeAdrHeader(raw) {
190
191
  .toLowerCase()
191
192
  .replace(/[\s:._-]+/g, ' ')
192
193
  .replace(/[^\w\s]/g, '')
193
- .trim();
194
+ .trim()
195
+ .replace(/^\d+(?:\s+\d+)*\s+/, '');
194
196
  }
195
197
  // Normalized synonym index (audit M7). classifyHeader receives an ALREADY-normalized
196
198
  // header (via normalizeAdrHeader), but historically compared it against the RAW synonym