@opengsd/gsd-core 1.14.0 → 1.16.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 (551) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +85 -5
  4. package/README.ja-JP.md +3 -3
  5. package/README.ko-KR.md +3 -3
  6. package/README.pt-BR.md +3 -3
  7. package/README.zh-CN.md +3 -3
  8. package/agents/gsd-code-fixer.compact.md +7 -6
  9. package/agents/gsd-code-fixer.md +9 -8
  10. package/agents/gsd-code-reviewer.compact.md +5 -3
  11. package/agents/gsd-code-reviewer.md +8 -6
  12. package/agents/gsd-debug-session-manager.compact.md +17 -2
  13. package/agents/gsd-debug-session-manager.md +17 -2
  14. package/agents/gsd-debugger.md +3 -3
  15. package/agents/gsd-eval-auditor.compact.md +1 -1
  16. package/agents/gsd-eval-auditor.md +1 -1
  17. package/agents/gsd-executor.md +17 -12
  18. package/agents/gsd-intel-updater.compact.md +1 -1
  19. package/agents/gsd-intel-updater.md +1 -1
  20. package/agents/gsd-mempalace-curator.md +2 -2
  21. package/agents/gsd-phase-researcher.md +19 -11
  22. package/agents/gsd-plan-checker.md +15 -9
  23. package/agents/gsd-planner.md +15 -11
  24. package/agents/gsd-project-researcher.compact.md +1 -1
  25. package/agents/gsd-project-researcher.md +1 -1
  26. package/agents/gsd-research-synthesizer.compact.md +1 -1
  27. package/agents/gsd-research-synthesizer.md +1 -1
  28. package/agents/gsd-ui-auditor.compact.md +21 -30
  29. package/agents/gsd-ui-auditor.md +166 -37
  30. package/agents/gsd-ui-researcher.compact.md +1 -1
  31. package/agents/gsd-ui-researcher.md +1 -1
  32. package/agents/gsd-verifier.md +37 -14
  33. package/bin/install.js +764 -287
  34. package/commands/gsd/add-tests.md +6 -1
  35. package/commands/gsd/ai-integration-phase.md +6 -1
  36. package/commands/gsd/audit-fix.md +5 -0
  37. package/commands/gsd/audit-milestone.md +6 -1
  38. package/commands/gsd/autonomous.md +7 -2
  39. package/commands/gsd/capture.md +9 -5
  40. package/commands/gsd/code-review.md +7 -2
  41. package/commands/gsd/complete-milestone.md +4 -0
  42. package/commands/gsd/config.md +7 -3
  43. package/commands/gsd/debug.md +11 -7
  44. package/commands/gsd/discuss-phase.md +7 -3
  45. package/commands/gsd/docs-update.md +12 -7
  46. package/commands/gsd/eval-review.md +6 -1
  47. package/commands/gsd/execute-phase.md +12 -7
  48. package/commands/gsd/extract-learnings.md +5 -0
  49. package/commands/gsd/fast.md +4 -0
  50. package/commands/gsd/forensics.md +5 -1
  51. package/commands/gsd/graphify.md +10 -6
  52. package/commands/gsd/health.md +5 -0
  53. package/commands/gsd/help.md +7 -2
  54. package/commands/gsd/import.md +7 -3
  55. package/commands/gsd/inbox.md +5 -0
  56. package/commands/gsd/ingest-docs.md +5 -1
  57. package/commands/gsd/manager.md +6 -1
  58. package/commands/gsd/map-codebase.md +7 -3
  59. package/commands/gsd/mempalace-capture.md +12 -4
  60. package/commands/gsd/mempalace-recall.md +5 -1
  61. package/commands/gsd/milestone-summary.md +5 -1
  62. package/commands/gsd/mvp-phase.md +8 -3
  63. package/commands/gsd/new-milestone.md +6 -1
  64. package/commands/gsd/new-project.md +5 -0
  65. package/commands/gsd/next.md +6 -1
  66. package/commands/gsd/ns-context.md +4 -0
  67. package/commands/gsd/ns-ideate.md +4 -0
  68. package/commands/gsd/ns-manage.md +4 -0
  69. package/commands/gsd/ns-project.md +4 -0
  70. package/commands/gsd/ns-review.md +4 -0
  71. package/commands/gsd/ns-workflow.md +4 -0
  72. package/commands/gsd/onboard.md +6 -1
  73. package/commands/gsd/pause-work.md +5 -1
  74. package/commands/gsd/phase.md +8 -4
  75. package/commands/gsd/plan-phase.md +6 -1
  76. package/commands/gsd/plan-review-convergence.md +11 -7
  77. package/commands/gsd/pr-branch.md +4 -0
  78. package/commands/gsd/profile-user.md +5 -1
  79. package/commands/gsd/progress.md +7 -2
  80. package/commands/gsd/quick-batch.md +21 -9
  81. package/commands/gsd/quick.md +12 -7
  82. package/commands/gsd/review.md +7 -4
  83. package/commands/gsd/secure-phase.md +6 -1
  84. package/commands/gsd/ship.md +5 -0
  85. package/commands/gsd/sketch.md +7 -2
  86. package/commands/gsd/spec-phase.md +5 -1
  87. package/commands/gsd/spike.md +8 -3
  88. package/commands/gsd/surface.md +5 -1
  89. package/commands/gsd/thread.md +4 -0
  90. package/commands/gsd/ui-phase.md +6 -1
  91. package/commands/gsd/ui-review.md +6 -1
  92. package/commands/gsd/ultraplan-phase.md +5 -1
  93. package/commands/gsd/undo.md +5 -1
  94. package/commands/gsd/update.md +6 -2
  95. package/commands/gsd/validate-phase.md +6 -1
  96. package/commands/gsd/verify-work.md +6 -1
  97. package/commands/gsd/workspace.md +7 -3
  98. package/gsd-core/bin/gsd-tools.cjs +477 -78
  99. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  100. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  101. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  102. package/gsd-core/bin/lib/audit.cjs +144 -42
  103. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  104. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  105. package/gsd-core/bin/lib/capability-registry.cjs +197 -222
  106. package/gsd-core/bin/lib/capability-validator.cjs +16 -1
  107. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  108. package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
  109. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  110. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  111. package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
  112. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  113. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  114. package/gsd-core/bin/lib/commands.cjs +343 -207
  115. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  116. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  117. package/gsd-core/bin/lib/config.cjs +76 -19
  118. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  119. package/gsd-core/bin/lib/coverage.cjs +4 -8
  120. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  121. package/gsd-core/bin/lib/decisions.cjs +30 -14
  122. package/gsd-core/bin/lib/drift.cjs +177 -42
  123. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  124. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  125. package/gsd-core/bin/lib/frontmatter.cjs +426 -234
  126. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  127. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  128. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  129. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  130. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  131. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  132. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  133. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  134. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  135. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  136. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  137. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  138. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  139. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  140. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  141. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  142. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  143. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  144. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  145. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  146. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  147. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  148. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  149. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  150. package/gsd-core/bin/lib/graphify.cjs +10 -2
  151. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  152. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  153. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  154. package/gsd-core/bin/lib/init.cjs +364 -132
  155. package/gsd-core/bin/lib/install-engine.cjs +30 -33
  156. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  157. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  158. package/gsd-core/bin/lib/io.cjs +122 -3
  159. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  160. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  161. package/gsd-core/bin/lib/milestone.cjs +37 -6
  162. package/gsd-core/bin/lib/model-resolver.cjs +171 -62
  163. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  164. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  165. package/gsd-core/bin/lib/pattern.cjs +10 -0
  166. package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
  167. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  168. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  169. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  170. package/gsd-core/bin/lib/phase-id.cjs +110 -8
  171. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  172. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  173. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  174. package/gsd-core/bin/lib/phase.cjs +489 -88
  175. package/gsd-core/bin/lib/plan-document.cjs +142 -20
  176. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  177. package/gsd-core/bin/lib/planning-document.cjs +692 -0
  178. package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
  179. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  180. package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
  181. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  182. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  183. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  184. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  185. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  186. package/gsd-core/bin/lib/project-root.cjs +41 -2
  187. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  188. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  189. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  190. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  191. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  192. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  193. package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
  194. package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
  195. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
  196. package/gsd-core/bin/lib/roadmap.cjs +405 -88
  197. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
  198. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
  199. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  200. package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
  201. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
  202. package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
  203. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  204. package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
  205. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  206. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  207. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  208. package/gsd-core/bin/lib/state-document.cjs +100 -22
  209. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  210. package/gsd-core/bin/lib/state.cjs +256 -105
  211. package/gsd-core/bin/lib/surface.cjs +19 -2
  212. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
  213. package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
  214. package/gsd-core/bin/lib/uat.cjs +432 -7
  215. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  216. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
  217. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  218. package/gsd-core/bin/lib/vendor/README.md +31 -9
  219. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  220. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  221. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  222. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  223. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  224. package/gsd-core/bin/lib/verification.cjs +1378 -274
  225. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  226. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  227. package/gsd-core/bin/lib/verify.cjs +357 -531
  228. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  229. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  230. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  231. package/gsd-core/bin/lib/workstream.cjs +11 -2
  232. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  233. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  234. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
  235. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  236. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  237. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  238. package/gsd-core/references/checkpoints.md +5 -3
  239. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  240. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  241. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  242. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  243. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  244. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  245. package/gsd-core/references/edge-probe.md +195 -21
  246. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  247. package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
  248. package/gsd-core/references/execute-phase-response-language.md +1 -1
  249. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  250. package/gsd-core/references/gsd-run-resolver.md +1 -1
  251. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  252. package/gsd-core/references/model-profiles.md +1 -1
  253. package/gsd-core/references/offer-next.md +1 -1
  254. package/gsd-core/references/phase-argument-parsing.md +9 -7
  255. package/gsd-core/references/phase-id-convention.md +28 -0
  256. package/gsd-core/references/planner-gap-closure.md +2 -0
  257. package/gsd-core/references/planner-load-graph-context.md +24 -13
  258. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  259. package/gsd-core/references/planning-config.md +12 -3
  260. package/gsd-core/references/spidr-splitting.md +1 -1
  261. package/gsd-core/references/tdd.md +37 -8
  262. package/gsd-core/references/ui-consideration-probe.md +10 -5
  263. package/gsd-core/references/verifier-phase-gates.md +5 -2
  264. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  265. package/gsd-core/references/verify-mvp-mode.md +2 -2
  266. package/gsd-core/references/workstream-flag.md +33 -3
  267. package/gsd-core/references/worktree-path-safety.md +321 -0
  268. package/gsd-core/templates/README.md +1 -1
  269. package/gsd-core/templates/UAT.md +17 -1
  270. package/gsd-core/templates/config.json +2 -11
  271. package/gsd-core/templates/verification-report.md +1 -1
  272. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  273. package/gsd-core/workflows/add-backlog.md +1 -1
  274. package/gsd-core/workflows/add-phase.md +8 -7
  275. package/gsd-core/workflows/add-tests.md +4 -3
  276. package/gsd-core/workflows/add-todo.md +6 -5
  277. package/gsd-core/workflows/ai-integration-phase.md +13 -4
  278. package/gsd-core/workflows/audit-fix.md +1 -1
  279. package/gsd-core/workflows/audit-milestone.md +4 -3
  280. package/gsd-core/workflows/audit-uat.md +1 -1
  281. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  282. package/gsd-core/workflows/autonomous.md +43 -23
  283. package/gsd-core/workflows/check-todos.md +7 -6
  284. package/gsd-core/workflows/cleanup.md +2 -2
  285. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  286. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  287. package/gsd-core/workflows/code-review-fix.md +112 -25
  288. package/gsd-core/workflows/code-review.md +146 -135
  289. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  290. package/gsd-core/workflows/complete-milestone.md +13 -8
  291. package/gsd-core/workflows/debug.md +32 -7
  292. package/gsd-core/workflows/diagnose-issues.md +3 -2
  293. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  294. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  295. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  296. package/gsd-core/workflows/discuss-phase.md +4 -3
  297. package/gsd-core/workflows/do.md +2 -2
  298. package/gsd-core/workflows/docs-update.md +6 -5
  299. package/gsd-core/workflows/edit-phase.md +4 -3
  300. package/gsd-core/workflows/eval-review.md +14 -5
  301. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  302. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
  303. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  304. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  305. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  306. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  307. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  308. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  309. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  310. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  311. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  312. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  313. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  314. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  315. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  316. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  317. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  318. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  319. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  320. package/gsd-core/workflows/execute-phase.md +96 -178
  321. package/gsd-core/workflows/execute-plan.md +18 -23
  322. package/gsd-core/workflows/explore.md +4 -4
  323. package/gsd-core/workflows/extract-learnings.md +4 -2
  324. package/gsd-core/workflows/fast.md +1 -1
  325. package/gsd-core/workflows/forensics.md +1 -1
  326. package/gsd-core/workflows/graduation.md +1 -1
  327. package/gsd-core/workflows/health.md +3 -2
  328. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  329. package/gsd-core/workflows/help/modes/full.md +5 -5
  330. package/gsd-core/workflows/help/modes/topic.md +15 -5
  331. package/gsd-core/workflows/import.md +4 -3
  332. package/gsd-core/workflows/inbox.md +2 -2
  333. package/gsd-core/workflows/ingest-docs.md +3 -3
  334. package/gsd-core/workflows/insert-phase.md +4 -3
  335. package/gsd-core/workflows/list-seeds.md +1 -1
  336. package/gsd-core/workflows/list-workspaces.md +1 -1
  337. package/gsd-core/workflows/manager.md +6 -4
  338. package/gsd-core/workflows/map-codebase.md +5 -4
  339. package/gsd-core/workflows/milestone-summary.md +3 -2
  340. package/gsd-core/workflows/mvp-phase.md +14 -14
  341. package/gsd-core/workflows/new-milestone.md +11 -11
  342. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  343. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  344. package/gsd-core/workflows/new-project.md +7 -7
  345. package/gsd-core/workflows/new-workspace.md +2 -2
  346. package/gsd-core/workflows/next.md +1 -1
  347. package/gsd-core/workflows/note.md +1 -1
  348. package/gsd-core/workflows/onboard.md +1 -1
  349. package/gsd-core/workflows/pause-work.md +2 -2
  350. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  351. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  352. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  353. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  354. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  355. package/gsd-core/workflows/plan-phase.md +50 -24
  356. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  357. package/gsd-core/workflows/plant-seed.md +62 -20
  358. package/gsd-core/workflows/pr-branch.md +113 -13
  359. package/gsd-core/workflows/profile-user.md +2 -2
  360. package/gsd-core/workflows/progress.md +19 -49
  361. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
  362. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  363. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  364. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  365. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  366. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  367. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  368. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  369. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  370. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  371. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  372. package/gsd-core/workflows/quick-batch.md +15 -10
  373. package/gsd-core/workflows/quick.md +62 -39
  374. package/gsd-core/workflows/reapply-patches.md +9 -3
  375. package/gsd-core/workflows/remove-phase.md +3 -2
  376. package/gsd-core/workflows/remove-workspace.md +2 -2
  377. package/gsd-core/workflows/resume-project.md +3 -2
  378. package/gsd-core/workflows/review.md +33 -17
  379. package/gsd-core/workflows/scan.md +3 -2
  380. package/gsd-core/workflows/secure-phase.md +13 -13
  381. package/gsd-core/workflows/settings-advanced.md +30 -10
  382. package/gsd-core/workflows/settings-integrations.md +2 -3
  383. package/gsd-core/workflows/settings.md +4 -4
  384. package/gsd-core/workflows/ship.md +14 -13
  385. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  386. package/gsd-core/workflows/sketch.md +1 -1
  387. package/gsd-core/workflows/smart-entry.md +2 -2
  388. package/gsd-core/workflows/spec-phase.md +15 -5
  389. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  390. package/gsd-core/workflows/spike.md +1 -1
  391. package/gsd-core/workflows/stats.md +1 -1
  392. package/gsd-core/workflows/sync-skills.md +5 -5
  393. package/gsd-core/workflows/thread.md +2 -2
  394. package/gsd-core/workflows/transition.md +13 -23
  395. package/gsd-core/workflows/ui-phase.md +48 -11
  396. package/gsd-core/workflows/ui-review.md +21 -6
  397. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  398. package/gsd-core/workflows/undo.md +339 -20
  399. package/gsd-core/workflows/update.md +7 -7
  400. package/gsd-core/workflows/validate-phase.md +12 -13
  401. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  402. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  403. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  404. package/gsd-core/workflows/verify-work.md +129 -55
  405. package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
  406. package/hooks/dist/gsd-check-update-worker.js +8 -0
  407. package/hooks/dist/gsd-check-update.js +8 -0
  408. package/hooks/dist/gsd-context-monitor.js +31 -8
  409. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  410. package/hooks/dist/gsd-secret-read-guard.js +161 -4
  411. package/hooks/dist/gsd-statusline.js +85 -20
  412. package/hooks/dist/gsd-update-banner.js +8 -0
  413. package/hooks/dist/gsd-validate-commit.sh +63 -4
  414. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  415. package/hooks/dist/gsd-workflow-guard.js +5 -4
  416. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  417. package/hooks/dist/lib/cli-exit.js +12 -0
  418. package/hooks/dist/lib/git-probe.js +17 -1
  419. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  420. package/hooks/gsd-agent-isolation-guard.js +32 -0
  421. package/hooks/gsd-check-update-worker.js +8 -0
  422. package/hooks/gsd-check-update.js +8 -0
  423. package/hooks/gsd-context-monitor.js +31 -8
  424. package/hooks/gsd-cursor-subagent-start.js +8 -0
  425. package/hooks/gsd-secret-read-guard.js +161 -4
  426. package/hooks/gsd-statusline.js +85 -20
  427. package/hooks/gsd-update-banner.js +8 -0
  428. package/hooks/gsd-validate-commit.sh +63 -4
  429. package/hooks/gsd-windsurf-pre-write.js +11 -2
  430. package/hooks/gsd-workflow-guard.js +5 -4
  431. package/hooks/gsd-worktree-path-guard.js +6 -2
  432. package/hooks/hooks.json +5 -5
  433. package/hooks/lib/cli-exit.js +12 -0
  434. package/hooks/lib/git-probe.js +17 -1
  435. package/hooks/lib/isolation-sentinel.js +2 -2
  436. package/package.json +22 -4
  437. package/scripts/build-hooks.js +15 -6
  438. package/scripts/changeset/parse.cjs +52 -4
  439. package/scripts/check-contract-drift.cjs +127 -11
  440. package/scripts/ci-timeout-report.cjs +770 -4
  441. package/scripts/command-contract-helpers.cjs +15 -8
  442. package/scripts/docs-guard-registry.cjs +34 -0
  443. package/scripts/gen-features.cjs +13 -8
  444. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  445. package/scripts/gen-loop-host-contract.cjs +79 -1
  446. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  447. package/scripts/gen-plugin-skills.cjs +87 -1
  448. package/scripts/gen-research-agents.cjs +24 -31
  449. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  450. package/scripts/gen-test-timings.cjs +32 -7
  451. package/scripts/lib/cli-exit.cjs +12 -0
  452. package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
  453. package/scripts/lib/ndjson-reporter.cjs +31 -5
  454. package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
  455. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  456. package/scripts/lib/vendor-bundle.cjs +59 -0
  457. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  458. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  459. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  460. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  461. package/scripts/lint-descriptions.cjs +7 -3
  462. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
  463. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  464. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  465. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  466. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  467. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  468. package/scripts/lint-phase-id-drift.cjs +319 -5
  469. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  470. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  471. package/scripts/lint-response-language-coverage.cjs +3 -0
  472. package/scripts/lint-retired-runtime-name.cjs +619 -0
  473. package/scripts/lint-skill-deps.cjs +7 -3
  474. package/scripts/lint-state-write-path-drift.cjs +93 -0
  475. package/scripts/lint-test-file-count.allowlist.json +38 -9
  476. package/scripts/lint-test-file-count.cjs +34 -1
  477. package/scripts/lint-vendored-deps.cjs +41 -5
  478. package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
  479. package/scripts/mutation-matrix.cjs +50 -5
  480. package/scripts/prompt-injection-scan.sh +4 -0
  481. package/scripts/release-tarball-smoke.cjs +194 -1
  482. package/scripts/require-issue-link-policy.cjs +6 -2
  483. package/scripts/sync-runtime-launcher.cjs +184 -2
  484. package/scripts/verify-npm-publish.cjs +76 -20
  485. package/skills/gsd-add-tests/SKILL.md +6 -1
  486. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  487. package/skills/gsd-audit-fix/SKILL.md +5 -0
  488. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  489. package/skills/gsd-autonomous/SKILL.md +7 -2
  490. package/skills/gsd-capture/SKILL.md +9 -5
  491. package/skills/gsd-code-review/SKILL.md +7 -2
  492. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  493. package/skills/gsd-config/SKILL.md +8 -4
  494. package/skills/gsd-debug/SKILL.md +11 -7
  495. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  496. package/skills/gsd-docs-update/SKILL.md +12 -7
  497. package/skills/gsd-eval-review/SKILL.md +6 -1
  498. package/skills/gsd-execute-phase/SKILL.md +12 -7
  499. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  500. package/skills/gsd-fast/SKILL.md +4 -0
  501. package/skills/gsd-forensics/SKILL.md +5 -1
  502. package/skills/gsd-graphify/SKILL.md +10 -6
  503. package/skills/gsd-health/SKILL.md +5 -0
  504. package/skills/gsd-help/SKILL.md +7 -2
  505. package/skills/gsd-import/SKILL.md +7 -3
  506. package/skills/gsd-inbox/SKILL.md +5 -0
  507. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  508. package/skills/gsd-manager/SKILL.md +6 -1
  509. package/skills/gsd-map-codebase/SKILL.md +7 -3
  510. package/skills/gsd-mempalace-capture/SKILL.md +12 -4
  511. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  512. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  513. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  514. package/skills/gsd-new-milestone/SKILL.md +6 -1
  515. package/skills/gsd-new-project/SKILL.md +5 -0
  516. package/skills/gsd-next/SKILL.md +6 -1
  517. package/skills/gsd-ns-context/SKILL.md +4 -0
  518. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  519. package/skills/gsd-ns-manage/SKILL.md +4 -0
  520. package/skills/gsd-ns-project/SKILL.md +4 -0
  521. package/skills/gsd-ns-review/SKILL.md +4 -0
  522. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  523. package/skills/gsd-onboard/SKILL.md +6 -1
  524. package/skills/gsd-pause-work/SKILL.md +5 -1
  525. package/skills/gsd-phase/SKILL.md +8 -4
  526. package/skills/gsd-plan-phase/SKILL.md +6 -1
  527. package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
  528. package/skills/gsd-pr-branch/SKILL.md +4 -0
  529. package/skills/gsd-profile-user/SKILL.md +5 -1
  530. package/skills/gsd-progress/SKILL.md +7 -2
  531. package/skills/gsd-quick/SKILL.md +16 -10
  532. package/skills/gsd-quick-batch/SKILL.md +21 -9
  533. package/skills/gsd-review/SKILL.md +7 -4
  534. package/skills/gsd-review-backlog/SKILL.md +3 -2
  535. package/skills/gsd-secure-phase/SKILL.md +6 -1
  536. package/skills/gsd-ship/SKILL.md +5 -0
  537. package/skills/gsd-sketch/SKILL.md +7 -2
  538. package/skills/gsd-spec-phase/SKILL.md +5 -1
  539. package/skills/gsd-spike/SKILL.md +8 -3
  540. package/skills/gsd-surface/SKILL.md +5 -1
  541. package/skills/gsd-thread/SKILL.md +4 -0
  542. package/skills/gsd-ui-phase/SKILL.md +6 -1
  543. package/skills/gsd-ui-review/SKILL.md +6 -1
  544. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  545. package/skills/gsd-undo/SKILL.md +5 -1
  546. package/skills/gsd-update/SKILL.md +6 -2
  547. package/skills/gsd-validate-phase/SKILL.md +6 -1
  548. package/skills/gsd-verify-work/SKILL.md +6 -1
  549. package/skills/gsd-workspace/SKILL.md +7 -3
  550. package/skills/gsd-workstreams/SKILL.md +6 -6
  551. package/vscode/package.json +1 -1
@@ -22,6 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  const node_fs_1 = __importDefault(require("node:fs"));
23
23
  const node_path_1 = __importDefault(require("node:path"));
24
24
  const node_child_process_1 = require("node:child_process");
25
+ const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
25
26
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
26
27
  const ioMod = require("./io.cjs");
27
28
  const { output, error, ERROR_REASON, formatDiagnosticToken } = ioMod;
@@ -38,11 +39,13 @@ const coreUtilsMod = require("./core-utils.cjs");
38
39
  // generative-fix divergence CLAUDE.md warns about. Collapsed onto core-utils'
39
40
  // copy, which was already the leaf owner, so there is no second surface left to
40
41
  // drift and no parity test needed to police one.
41
- const { toPosixPath, generateSlugInternal, readSubdirectories, extractCanonicalPlanId, findUnsummarizedPlans, normalizeLineEndings, } = coreUtilsMod;
42
+ const { toPosixPath, generateSlugInternal, readSubdirectories, extractCanonicalPlanId, findUnsummarizedPlans, } = coreUtilsMod;
42
43
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
43
44
  const phaseIdMod = require("./phase-id.cjs");
44
- const { normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, matchPhaseDirs, isSentinelPhaseId, scopeToPhase, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, PHASE_NUMBER_TOKEN_SOURCE, } = phaseIdMod;
45
+ const { normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, matchPhaseDirs, isSentinelPhaseId, scopeToPhase, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, PHASE_NUMBER_TOKEN_SOURCE, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseIdMod;
45
46
  const pattern_cjs_1 = require("./pattern.cjs");
47
+ const gate_exit_cjs_1 = require("./gate-exit.cjs");
48
+ const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
46
49
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
47
50
  const phaseLocatorMod = require("./phase-locator.cjs");
48
51
  const { findPhaseInternal, getArchivedPhaseDirs, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocatorMod;
@@ -65,11 +68,17 @@ const frontmatterMod = require("./frontmatter.cjs");
65
68
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- state.cjs is an export= CommonJS module
66
69
  const stateMod = require("./state.cjs");
67
70
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
71
+ const planning_document_cjs_1 = require("./planning-document.cjs");
68
72
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
69
73
  const clock_cjs_1 = require("./clock.cjs");
70
74
  const state_transition_cjs_1 = require("./state-transition.cjs");
71
75
  const markdown_table_cjs_1 = require("./markdown-table.cjs");
76
+ // #5060: the Phase Status Module owns the ROADMAP Status-cell token vocabulary.
77
+ const phase_status_cjs_1 = require("./phase-status.cjs");
72
78
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
79
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- roadmap.cjs is an export= CommonJS module
80
+ const roadmapMod = require("./roadmap.cjs");
81
+ const { buildPhaseHeadingRegex } = roadmapMod;
73
82
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
74
83
  const uatPredicate = require("./uat-predicate.cjs");
75
84
  const { evaluateUatPassed } = uatPredicate;
@@ -80,20 +89,19 @@ const verificationMod = require("./verification.cjs");
80
89
  // cycle (the reverse edge, `state.cts → verify.cjs`, would).
81
90
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- verify.cjs is an export= CommonJS module
82
91
  const verifyMod = require("./verify.cjs");
83
- const { readVerificationStatus } = verificationMod;
92
+ const { readVerificationStatus, VERIFICATION_STATUS, findVerificationStatusError } = verificationMod;
84
93
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-dependency-graph.cjs is an export= CommonJS module
85
94
  const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
86
95
  const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSummaryFileBlocked } = planDependencyGraphMod;
87
96
  // #612: `resolvePhaseIdConvention` selects the write-time milestone-scope
88
97
  // guard's terminator vocabulary (see assertDescriptionPreservesMilestoneScope).
89
- const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolvePhaseIdConvention, } = planningWorkspace;
98
+ const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolveEnvWorkstream, resolvePhaseIdConvention, } = planningWorkspace;
90
99
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- milestone-lock.cjs is an export= CommonJS module
91
100
  const milestoneLockMod = require("./milestone-lock.cjs");
92
101
  // eslint-disable-next-line @typescript-eslint/no-require-imports
93
102
  const planDocumentMod = require("./plan-document.cjs");
94
103
  const { parsePlanDocument, planIdFromFile } = planDocumentMod;
95
- const { extractFrontmatter } = frontmatterMod;
96
- const { readModifyWriteStateMd, stateExtractField, stateReplaceField, syncAndPreserveStateMd, withStateLock, updatePerformanceMetricsSection, } = stateMod;
104
+ const { readModifyWriteStateMd, stateExtractField, stateReplaceField, syncAndPreserveStateMd, assertVerificationReportsReadable, withStateLock, updatePerformanceMetricsSection, } = stateMod;
97
105
  // Any .md file with PLAN anywhere in the basename — diagnostic net
98
106
  const PLAN_OUTLINE_RE = /-PLAN-OUTLINE\.md$/i;
99
107
  const PLAN_PRE_BOUNCE_RE = /-PLAN.*\.pre-bounce\.md$/i;
@@ -348,16 +356,25 @@ function getRoadmapModeForPhase(cwd, phaseNum) {
348
356
  const milestoneContent = extractCurrentMilestone(rawContent, cwd);
349
357
  const fullContent = stripShippedMilestones(rawContent);
350
358
  const escapedPhase = phaseMarkdownRegexSource(phaseNum);
351
- const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'i');
359
+ // #5007 (Phase 6 / ADR-4910 §8): buildPhaseHeadingRegex (src/roadmap.cts) is
360
+ // anchored `^...$` with no 'm' flag — it is designed to test ONE
361
+ // already-tokenized heading LINE (mirroring searchPhaseInContent's own
362
+ // usage), not to `.match()` against multi-line content directly. Routing
363
+ // through tokenizeHeadings first is therefore required here, not a pure
364
+ // regex-literal swap (the design doc's "direct swap" plan does not hold
365
+ // once the anchor is accounted for).
366
+ const phaseHeader = buildPhaseHeadingRegex(escapedPhase);
352
367
  for (const content of [milestoneContent, fullContent]) {
353
- const headerMatch = content.match(phaseHeader);
354
- if (!headerMatch || headerMatch.index === undefined)
368
+ const heading = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content).find((h) => phaseHeader.test(h.text));
369
+ if (!heading)
355
370
  continue;
356
- const sectionStart = headerMatch.index;
371
+ const headingLineEnd = content.indexOf('\n', heading.offset);
372
+ const headerMatchLength = (headingLineEnd === -1 ? content.length : headingLineEnd) - heading.offset;
373
+ const sectionStart = heading.offset;
357
374
  const rest = content.slice(sectionStart);
358
- const nextHeader = rest.slice(headerMatch[0].length).match(/\n#{2,4}\s+Phase\s+\S/i);
375
+ const nextHeader = rest.slice(headerMatchLength).match(/\n#{2,4}\s+Phase\s+\S/i);
359
376
  const sectionEnd = nextHeader
360
- ? sectionStart + headerMatch[0].length + nextHeader.index
377
+ ? sectionStart + headerMatchLength + nextHeader.index
361
378
  : content.length;
362
379
  const section = content.slice(sectionStart, sectionEnd);
363
380
  const modeMatch = section.match(/\*\*Mode(?::\*\*|\*\*:)\s*([^\n]+)/i);
@@ -592,8 +609,16 @@ function cmdFindPhase(cwd, phase, raw) {
592
609
  * none left in this file) degrades to the pre-#3897 two-tier behavior rather
593
610
  * than throwing on a missing argument.
594
611
  */
612
+ /**
613
+ * The dependency resolver's one comparison normalization. Callers that must
614
+ * predict whether a token names a plan reuse this seam instead of copying its
615
+ * case-folding rule.
616
+ */
617
+ function normalizeDependencyToken(token) {
618
+ return String(token).toLowerCase();
619
+ }
595
620
  function resolveDependencyId(dep, planMap, canonicalToId, shortFormToId) {
596
- const lower = dep.toLowerCase();
621
+ const lower = normalizeDependencyToken(dep);
597
622
  if (planMap.has(lower))
598
623
  return planMap.get(lower).id;
599
624
  if (canonicalToId.has(lower))
@@ -634,7 +659,7 @@ function buildShortFormToId(rawPlans) {
634
659
  const canonical = extractCanonicalPlanId(p.id);
635
660
  const lastDash = canonical.lastIndexOf('-');
636
661
  if (lastDash > 0 && lastDash < canonical.length - 1) {
637
- const shortForm = canonical.slice(lastDash + 1).toLowerCase();
662
+ const shortForm = normalizeDependencyToken(canonical.slice(lastDash + 1));
638
663
  if (/^\d+$/.test(shortForm) && !shortFormToId.has(shortForm)) {
639
664
  shortFormToId.set(shortForm, p.id);
640
665
  }
@@ -744,12 +769,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
744
769
  phase: normalized,
745
770
  error: `Phase ${normalized} is ambiguous: ${ambiguousMatches.length} directories match (${ambiguousMatches.map((m) => `"${m}"`).join(', ')}).`,
746
771
  ambiguous_matches: ambiguousMatches,
747
- plans: [], waves: {}, incomplete: [], has_checkpoints: false,
772
+ plans: [], waves: {}, incomplete: [], runnable: [], ready_plans: [], has_checkpoints: false,
748
773
  }, raw);
749
774
  return;
750
775
  }
751
776
  if (!phaseDir) {
752
- output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], has_checkpoints: false }, raw);
777
+ output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], ready_plans: [], has_checkpoints: false }, raw);
753
778
  return;
754
779
  }
755
780
  void phaseDirName; // used only to set phaseDir above
@@ -836,6 +861,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
836
861
  filesModified: planDoc.filesModified,
837
862
  filesDeleted: planDoc.filesDeleted,
838
863
  agentHint: planDoc.agentHint,
864
+ gapClosure: planDoc.gapClosure,
839
865
  taskCount: planDoc.taskCount,
840
866
  hasSummary,
841
867
  halted,
@@ -844,7 +870,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
844
870
  // ── Pass 2: topological level assignment via depends_on DAG ──────────────
845
871
  const seenLower = new Map();
846
872
  for (const p of rawPlans) {
847
- const lower = p.id.toLowerCase();
873
+ const lower = normalizeDependencyToken(p.id);
848
874
  const existing = seenLower.get(lower);
849
875
  if (existing !== undefined) {
850
876
  error(`depends_on index collision in phase ${normalized}: plan IDs '${existing}' and '${p.id}' are identical when case-folded. Rename one file to avoid ambiguous dependency resolution.`);
@@ -852,8 +878,8 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
852
878
  }
853
879
  seenLower.set(lower, p.id);
854
880
  }
855
- const planMap = new Map(rawPlans.map((p) => [p.id.toLowerCase(), p]));
856
- const canonicalToId = new Map(rawPlans.map((p) => [extractCanonicalPlanId(p.id).toLowerCase(), p.id]));
881
+ const planMap = new Map(rawPlans.map((p) => [normalizeDependencyToken(p.id), p]));
882
+ const canonicalToId = new Map(rawPlans.map((p) => [normalizeDependencyToken(extractCanonicalPlanId(p.id)), p.id]));
857
883
  // #3897 rung 4 (ADR-3473 §8.9) — the third depends_on resolution tier.
858
884
  // Resolves a bare in-phase plan-number short form (e.g. "01") to its owning
859
885
  // plan id. In-phase only by construction (T49): the map is built from THIS
@@ -888,6 +914,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
888
914
  const waves = {};
889
915
  const incomplete = [];
890
916
  const runnable = [];
917
+ // #4628: DAG-ready view — see the per-plan emission below. Keyed by id so
918
+ // the per-plan pass can resolve each plan's dependency edges.
919
+ const resolvedDepsByPlan = new Map(haltNodes.map((n) => [n.id, n.resolvedDependsOn]));
920
+ const readyPlans = [];
891
921
  let hasCheckpoints = false;
892
922
  const warnings = [];
893
923
  // #3427 / ADR-3473 §8.5: name every dropped depends_on edge (plan AND
@@ -897,8 +927,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
897
927
  // plan with no dropped edges and a genuinely wrong `wave:` still warns
898
928
  // (N3, D6, T25).
899
929
  const plansWithUnresolvedTokens = new Set();
930
+ const unresolvedTokensByPlan = new Map();
900
931
  for (const { plan, token } of unresolved) {
901
932
  plansWithUnresolvedTokens.add(plan);
933
+ const tokens = unresolvedTokensByPlan.get(plan) ?? [];
934
+ tokens.push(formatDiagnosticToken(token));
935
+ unresolvedTokensByPlan.set(plan, tokens);
902
936
  warnings.push(`Plan ${plan}: depends_on token ${formatDiagnosticToken(token)} does not resolve to any plan in this phase — edge dropped, wave placement for this plan may be unreliable`);
903
937
  }
904
938
  for (const rawPlan of rawPlans) {
@@ -906,14 +940,34 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
906
940
  hasCheckpoints = true;
907
941
  }
908
942
  const blockedByIds = blockedBy.get(rawPlan.id) ?? [];
943
+ // #4628: readiness fails closed — every resolved dependency must have
944
+ // completion evidence (has_summary), and a depends_on edge that never
945
+ // resolved (#3427 — a dropped edge) carries no evidence to check. A
946
+ // single evaluation feeds both the ready_plans list and the per-plan
947
+ // `ready` / `unresolved_dependencies` fields below.
948
+ // Case-fold the dep before the planMap lookup: planMap is lowercase-keyed
949
+ // (#2237) while resolveDependencyId returns the raw id — a mixed-case id
950
+ // must not read as 'no evidence' (over-blocking a ready plan).
951
+ const missingEvidence = (resolvedDepsByPlan.get(rawPlan.id) ?? [])
952
+ .filter((dep) => planMap.get(dep.toLowerCase())?.hasSummary !== true);
953
+ if (plansWithUnresolvedTokens.has(rawPlan.id)) {
954
+ missingEvidence.push(...(unresolvedTokensByPlan.get(rawPlan.id) ?? []));
955
+ }
956
+ const isReady = blockedByIds.length === 0 && missingEvidence.length === 0;
909
957
  if (!rawPlan.hasSummary) {
910
958
  incomplete.push(rawPlan.id);
911
959
  // #2830: the runnable-only view — incomplete AND not transitively
912
960
  // blocked by a halted upstream plan. Additive alongside `incomplete`,
913
961
  // which keeps its existing "no SUMMARY yet" meaning unchanged.
962
+ // #4628: runnable says NOTHING about completion evidence — a runnable
963
+ // plan whose dependencies lack a SUMMARY is not DAG-ready. Consumers
964
+ // dispatch from `ready_plans`.
914
965
  if (blockedByIds.length === 0) {
915
966
  runnable.push(rawPlan.id);
916
967
  }
968
+ if (isReady) {
969
+ readyPlans.push(rawPlan.id);
970
+ }
917
971
  }
918
972
  const computedWave = (level.get(rawPlan.id) ?? 0) + levelOffset;
919
973
  const effectiveWave = computedWave;
@@ -943,6 +997,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
943
997
  return planMap.has(lower) ? planMap.get(lower).id : dep;
944
998
  }),
945
999
  autonomous: rawPlan.autonomous,
1000
+ // #4924: execute-phase's `--gaps-only` filter reads this field off the
1001
+ // index. Before it was emitted, every plan read as non-gap-closure and the
1002
+ // filter selected nothing — a silent, successful-looking empty run.
1003
+ gap_closure: rawPlan.gapClosure,
946
1004
  objective: rawPlan.objective,
947
1005
  files_modified: rawPlan.filesModified,
948
1006
  files_deleted: rawPlan.filesDeleted,
@@ -955,6 +1013,15 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
955
1013
  halted: rawPlan.halted,
956
1014
  blocked_by: blockedByIds,
957
1015
  };
1016
+ if (!rawPlan.hasSummary) {
1017
+ // #4628: readiness is a property of INCOMPLETE plans (a summarized plan
1018
+ // is filtered by has_summary before readiness is ever consulted). The
1019
+ // unresolved_dependencies list names exactly which predecessors lack
1020
+ // completion evidence, for the named-skip report.
1021
+ plan['ready'] = isReady;
1022
+ if (missingEvidence.length > 0)
1023
+ plan['unresolved_dependencies'] = missingEvidence;
1024
+ }
958
1025
  plans.push(plan);
959
1026
  const waveKey = String(effectiveWave);
960
1027
  if (!waves[waveKey]) {
@@ -968,6 +1035,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
968
1035
  waves,
969
1036
  incomplete,
970
1037
  runnable,
1038
+ ready_plans: readyPlans,
971
1039
  has_checkpoints: hasCheckpoints,
972
1040
  };
973
1041
  if (planNamingWarning)
@@ -1115,7 +1183,14 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
1115
1183
  const siblingPlanningDir = (wt) => planningDir(wt, ws);
1116
1184
  const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
1117
1185
  // Same header shape the allocators scan locally (#1729 tag tolerance).
1118
- const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
1186
+ // #5007 (Phase 6 / ADR-4910 §8): migrated onto buildPhaseHeadingScanRegex
1187
+ // (src/phase-id.cjs), the shared "which phases exist in this content" scan
1188
+ // owner. LABEL_ONLY baseline with no convention argument reproduces the
1189
+ // prior literal `Phase\s+` (no bracket tolerance) byte-for-byte — this is a
1190
+ // counter, matching the LABEL_ONLY docstring guidance exactly, so it is
1191
+ // deliberately NOT widened onto the bracket grammar the way the real-heading
1192
+ // readers (getRoadmapModeForPhase, phaseDisplayNameFromRoadmap) are.
1193
+ const { regex: headerPattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.LABEL_ONLY);
1119
1194
  for (const line of porcelain.split('\n')) {
1120
1195
  if (!line.startsWith('worktree '))
1121
1196
  continue;
@@ -1140,7 +1215,7 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
1140
1215
  let m;
1141
1216
  headerPattern.lastIndex = 0;
1142
1217
  while ((m = headerPattern.exec(content)) !== null) {
1143
- const num = parseInt(m[1], 10);
1218
+ const num = parseInt(m[phaseNumGroup], 10);
1144
1219
  if (!isSentinelPhaseId(num))
1145
1220
  used.add(num);
1146
1221
  }
@@ -1180,7 +1255,13 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
1180
1255
  // (section header, roadmap bullet, or on-disk directory) is counted:
1181
1256
  // 1) Section headers: ### Phase N: / ## Phase N: / #### Phase N:
1182
1257
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1183
- const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
1258
+ // #5007 (Phase 6 / ADR-4910 §8): migrated onto buildPhaseHeadingScanRegex
1259
+ // (src/phase-id.cjs) with the LABEL_ONLY baseline and no convention
1260
+ // argument — a counter site, matching phase-id.cts's own LABEL_ONLY
1261
+ // docstring guidance, reproducing the prior `Phase\s+` literal (no
1262
+ // bracket tolerance) byte-for-byte. See collectSiblingWorktreePhaseNums
1263
+ // (above) for the identical migration on the same regex shape.
1264
+ const { regex: headerPattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.LABEL_ONLY);
1184
1265
  // 2) Roadmap bullet entries: - [ ] **Phase N: ...** (all checkbox variants)
1185
1266
  // The lookahead accepts colon, decimal-dot, whitespace, bold-close asterisk,
1186
1267
  // or end-of-line so titleless forms ("- [ ] **Phase 11**", "- [ ] Phase 11")
@@ -1189,7 +1270,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
1189
1270
  const usedPhaseNums = new Set();
1190
1271
  let m;
1191
1272
  while ((m = headerPattern.exec(content)) !== null) {
1192
- const num = parseInt(m[1], 10);
1273
+ const num = parseInt(m[phaseNumGroup], 10);
1193
1274
  // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
1194
1275
  if (!isSentinelPhaseId(num))
1195
1276
  usedPhaseNums.add(num);
@@ -1289,11 +1370,15 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
1289
1370
  // bullets, on-disk dirs. The bullet scan was missing here — a bullet-only
1290
1371
  // `Phase N` row was invisible to batch allocation (#3849 secondary).
1291
1372
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1292
- const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
1373
+ // #5007 (Phase 6 / ADR-4910 §8): migrated onto buildPhaseHeadingScanRegex
1374
+ // (src/phase-id.cjs) with the LABEL_ONLY baseline and no convention
1375
+ // argument — same counter-site migration as cmdPhaseAdd's identical
1376
+ // regex above.
1377
+ const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.LABEL_ONLY);
1293
1378
  const bulletPattern = /^[ \t]*-[ \t]*\[[^\]]{0,200}\][ \t]*\*{0,2}Phase[ \t]+(\d+)(?=[:.\s*]|$)/gim;
1294
1379
  let m;
1295
1380
  while ((m = phasePattern.exec(content)) !== null) {
1296
- const num = parseInt(m[1], 10);
1381
+ const num = parseInt(m[phaseNumGroup], 10);
1297
1382
  // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
1298
1383
  if (isSentinelPhaseId(num))
1299
1384
  continue;
@@ -1416,7 +1501,19 @@ function scanExistingDecimalPhaseNumbers(phasesDir, rawContent, base) {
1416
1501
  decimalSet.add(parseInt(dm[1], 10));
1417
1502
  }
1418
1503
  }
1419
- const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
1504
+ // #5007 (Phase 6 / ADR-4910 §8): composes phaseHeadingPrefixSrcFor directly
1505
+ // rather than routing through buildPhaseHeadingScanRegex — the owner has no
1506
+ // decimal-subphase capture group. The design doc's suggested composition
1507
+ // passed `capturing=true` to phaseHeadingPrefixSrcFor, but this site has no
1508
+ // use for the bracket-id capture it would add, and doing so would silently
1509
+ // shift `rmMatch[1]` (the decimal digits) to group 2 — a real correctness
1510
+ // hazard. `capturing` is left at its default (false), and the `#{2,4}\s*`
1511
+ // heading marker (also omitted from the design's snippet) is kept explicit,
1512
+ // since phaseHeadingPrefixSrcFor never includes it — callers always add it
1513
+ // themselves (see buildPhaseHeadingScanRegex's own usage). No convention
1514
+ // argument: LABEL_ONLY with convention=undefined reproduces the prior
1515
+ // `Phase\s+` literal byte-for-byte.
1516
+ const rmPhasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY)}${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
1420
1517
  let rmMatch;
1421
1518
  while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
1422
1519
  decimalSet.add(parseInt(rmMatch[1], 10));
@@ -1443,10 +1540,27 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
1443
1540
  const content = extractCurrentMilestone(rawContent, cwd);
1444
1541
  const normalizedAfter = normalizePhaseName(afterPhase);
1445
1542
  const afterPhaseEscaped = phaseMarkdownRegexSource(normalizedAfter);
1446
- const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
1447
- const headingMatch = targetPattern.test(content);
1543
+ const insertConvention = resolvePhaseIdConvention(cwd);
1544
+ // #5007 (Phase 6 / ADR-4910 §8): buildPhaseHeadingRegex (src/roadmap.cts)
1545
+ // is anchored `^...$` with no 'm' flag — designed to test ONE
1546
+ // already-tokenized heading LINE, not to `.test()` against multi-line
1547
+ // `content` directly (the design doc's "no anchoring change needed"
1548
+ // claim for this site does not hold once the anchor is accounted for —
1549
+ // same discrepancy as getRoadmapModeForPhase and phaseDisplayNameFromRoadmap
1550
+ // above/below). Routed through tokenizeHeadings, mirroring
1551
+ // searchPhaseInContent's own usage.
1552
+ const targetPattern = buildPhaseHeadingRegex(afterPhaseEscaped, insertConvention);
1553
+ const headingMatch = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content).some((h) => targetPattern.test(h.text));
1448
1554
  const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
1449
- const anyHeadingPattern = /#{2,4}\s*Phase\s+\d/i;
1555
+ // #5007 (Phase 6 / ADR-4910 §8): a pure "does ANY phase heading exist"
1556
+ // boolean — neither shared owner exposes this shape (buildPhaseHeadingRegex
1557
+ // is phase-N-specific; buildPhaseHeadingScanRegex always captures a phase
1558
+ // number). Composed directly from phaseHeadingPrefixSrcFor with the
1559
+ // `#{2,4}\s*` marker kept explicit (phaseHeadingPrefixSrcFor never
1560
+ // includes it). Unanchored `.test()` on multi-line content, same as the
1561
+ // pre-migration literal — no tokenize-first refactor needed here since
1562
+ // there is no anchor in this source.
1563
+ const anyHeadingPattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, insertConvention)}\\d`, 'i');
1450
1564
  const roadmapHasHeadingPhases = anyHeadingPattern.test(content);
1451
1565
  const isBulletStyle = !headingMatch && bulletPattern.test(content) && !roadmapHasHeadingPhases;
1452
1566
  if (!headingMatch && !isBulletStyle) {
@@ -1522,14 +1636,42 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
1522
1636
  }
1523
1637
  else {
1524
1638
  const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_decimalPhase} to break down)\n`;
1525
- const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
1639
+ // #5007 (Phase 6 / ADR-4910 §8): this site needs the WHOLE matched
1640
+ // heading line plus its trailing newline (for splicing the new entry
1641
+ // in immediately after it) — neither shared owner returns that shape
1642
+ // (buildPhaseHeadingRegex captures only the title). Composed directly
1643
+ // from phaseHeadingPrefixSrcFor, keeping this site's own full-line
1644
+ // capture; audited together with the targetPattern/anyHeadingPattern
1645
+ // migration above (same describe block, same afterPhaseEscaped/
1646
+ // insertConvention inputs).
1647
+ const headerPattern = new RegExp(`(#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, insertConvention)}${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
1526
1648
  const headerMatch = rawContent.match(headerPattern);
1527
1649
  if (!headerMatch) {
1528
1650
  error(`Could not find Phase ${afterPhase} header`);
1529
1651
  }
1530
1652
  const headerIdx = rawContent.indexOf(headerMatch[0]);
1531
1653
  const afterHeader = rawContent.slice(headerIdx + headerMatch[0].length);
1532
- const nextPhaseMatch = afterHeader.match(/\r?\n#{2,4}\s+Phase\s+\d[\d.]*/i);
1654
+ // #5007 (Phase 6 / ADR-4910 §8): this next-phase-boundary lookup was NOT
1655
+ // one of the 15 originally-marked grandfathered sites, but it hand-rolls
1656
+ // the identical `#{2,4}...Phase\s+` heading-marker shape and sits right
1657
+ // next to headerPattern above, which this same commit series just
1658
+ // widened (via insertConvention) to recognize a bracket-tagged target
1659
+ // heading. Left bracket-blind, this regex would silently defeat that
1660
+ // widening: under the bracket convention a freshly-recognized
1661
+ // bracket-tagged target could still fail to find the NEXT heading as a
1662
+ // boundary (`[CODE.MM] Phase N+1:` never matches a bare `Phase\s+`
1663
+ // literal), so the newly inserted entry falls through to the
1664
+ // rawContent.length branch and lands appended at end-of-file instead of
1665
+ // directly after the target section. Found while migrating the
1666
+ // neighboring sites, not deferred: this is the same defect class the
1667
+ // whole Phase 6 effort exists to drain, just an unmarked instance of it.
1668
+ // Composed with the SAME baseline/convention as headerPattern above so
1669
+ // target-discovery and next-boundary-discovery agree. The base
1670
+ // `#{2,4}\s+` (mandatory whitespace after the hashes, not `\s*`) is
1671
+ // preserved exactly as the prior literal had it — a pre-existing,
1672
+ // deliberately-unchanged asymmetry with the other sites in this file.
1673
+ const nextPhaseHeadingPrefix = `#{2,4}\\s+${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, insertConvention)}`;
1674
+ const nextPhaseMatch = afterHeader.match(new RegExp(`\\r?\\n${nextPhaseHeadingPrefix}\\d[\\d.]*`, 'i'));
1533
1675
  let insertIdx;
1534
1676
  if (nextPhaseMatch) {
1535
1677
  insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
@@ -1844,7 +1986,30 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
1844
1986
  if (!isDecimal) {
1845
1987
  // #1729: fold an optional pre-colon ( ) tag into the suffix capture so it
1846
1988
  // is re-emitted verbatim — a tagged later phase still gets renumbered.
1847
- content = content.replace(/(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)((?:\s*\([^)\r\n]{0,200}\))?\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
1989
+ // #5007 (Phase 6 / ADR-4910 §8): composes phaseHeadingPrefixSrcFor
1990
+ // directly (needs prefix/number/suffix as three separate capture groups
1991
+ // for a verbatim in-place rewrite — neither shared owner returns that
1992
+ // shape). Two deliberate departures from the design doc's suggested
1993
+ // composition, both kept for zero behavior change on this
1994
+ // highest-blast-radius site (feeds phase remove's renumber path):
1995
+ // 1. The `#{2,4}\s*` heading marker is kept explicit and folded INTO
1996
+ // the prefix capture group — the design's snippet omitted it
1997
+ // entirely (phaseHeadingPrefixSrcFor never includes it; every
1998
+ // caller adds it, e.g. buildPhaseHeadingScanRegex), and the
1999
+ // original group 1 here captured the marker too.
2000
+ // 2. No convention argument is threaded (LABEL_ONLY with
2001
+ // convention=undefined), even though phaseHeadingPrefixSrcFor
2002
+ // COULD accept one — this keeps the composed source byte-identical
2003
+ // to the prior `Phase\s+` literal, a deliberate non-widening for
2004
+ // this specific site (unlike cmdPhaseInsert's targetPattern).
2005
+ // The number group widens from `\d+(?:\.\d+)?` to the full
2006
+ // PHASE_NUMBER_TOKEN_SOURCE (letter suffix + multi-segment decimals) —
2007
+ // this is SAFE, not a behavior change: decrementRoadmapPhaseToken's own
2008
+ // regex (`^(\d+)(\.\d+)?$`) rejects anything wider and returns the
2009
+ // token unchanged, so a heading this widening newly matches (e.g.
2010
+ // "Phase 12A:") is written back byte-for-byte unchanged, identical to
2011
+ // the pre-migration outcome of the whole regex simply not matching it.
2012
+ content = content.replace(new RegExp(`(#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY)})(${PHASE_NUMBER_TOKEN_SOURCE})(${OPTIONAL_PHASE_TAG_SOURCE}\\s*:)`, 'gi'), (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
1848
2013
  content = content.replace(/(-\s*\[[ x]\]\s*.*?Phase\s+)(\d+)(\s*:|\s+)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
1849
2014
  // ORDINAL-RENUMBER — CELL EDIT (not row-deletion) — migrated onto
1850
2015
  // updateTableCell (ADR-2143 §7, sibling of the deleteTableRow scoping
@@ -1933,8 +2098,52 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
1933
2098
  content = ordinalBefore + ordinalSection + ordinalRest;
1934
2099
  }
1935
2100
  content = content.replace(/(?<![0-9-])(\d{2})-(\d{2})(?=(?:(?:-[A-Za-z][A-Za-z0-9-]*)?-(?:PLAN|SUMMARY)\.md)|(?![0-9-]))/g, (_match, phaseNum, planNum) => `${decrementRoadmapPaddedPhaseNumber(phaseNum, removedInt)}-${planNum}`);
1936
- content = content.replace(/(\*\*Depends on\*\*\s*:\s*Phase\s+)(\d+(?:\.\d+)?)\b/gi, (_match, prefix, num) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}`);
1937
- content = content.replace(/(Depends on:\*\*\s*Phase\s+)(\d+(?:\.\d+)?)\b/gi, (_match, prefix, num) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}`);
2101
+ // #5007 (Phase 6 / ADR-4910): migrated the "Depends on" bold-field
2102
+ // decrement off two hand-rolled regexes — one per bold-placement
2103
+ // variant, `**Depends on**: Phase N` and `**Depends on:** Phase N` —
2104
+ // onto the PlanningDoc `boldField` read/write seam (`writePlansField`
2105
+ // above is the established precedent for this migration shape).
2106
+ // `BOLD_FIELD_RE` (planning-document.cts) recognizes BOTH placements
2107
+ // as the SAME field label ("Depends on") — `parseBoldFieldLine` strips
2108
+ // the token down to its inner text regardless of which side of `**`
2109
+ // the colon lands on — so this single loop naturally replaces both
2110
+ // regexes with one call site; no variant-specific branching remains.
2111
+ // Unlike `writePlansField`'s single `findField` lookup (scoped to one
2112
+ // already-isolated phase section), a whole ROADMAP.md can carry a
2113
+ // "Depends on" field in EVERY phase's detail section, so this walks
2114
+ // every `boldField` node in the whole parsed document whose label is
2115
+ // "Depends on" and rewrites each one independently — a
2116
+ // representability refusal or absent match on one field leaves that
2117
+ // field untouched and never blocks any other field, mirroring the
2118
+ // prior regexes' per-occurrence independence. A parse failure or an
2119
+ // unreadable-nodes refusal on `serialize` leaves `content` byte-for-
2120
+ // byte untouched, the same silent no-op the prior regexes had for any
2121
+ // input they didn't match (neither ever threw or warned).
2122
+ const dependsOnParsed = (0, planning_document_cjs_1.parsePlanningDoc)(content, 'ROADMAP.md');
2123
+ if (dependsOnParsed.ok) {
2124
+ let dependsOnDoc = dependsOnParsed.value;
2125
+ const dependsOnFieldIds = dependsOnDoc.nodes
2126
+ .filter((n) => n.kind === 'boldField' && n.label === 'Depends on')
2127
+ .map((n) => n.id);
2128
+ for (const fieldId of dependsOnFieldIds) {
2129
+ const currentRead = (0, planning_document_cjs_1.readNode)(dependsOnDoc, fieldId);
2130
+ if (!currentRead.ok)
2131
+ continue;
2132
+ const depMatch = /^(Phase\s+)(\d+(?:\.\d+)?)\b/i.exec(currentRead.value);
2133
+ if (!depMatch)
2134
+ continue;
2135
+ const [wholeMatch, prefix, num] = depMatch;
2136
+ const newValue = `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}` + currentRead.value.slice(wholeMatch.length);
2137
+ if (newValue === currentRead.value)
2138
+ continue;
2139
+ const staged = (0, planning_document_cjs_1.setFieldValue)(dependsOnDoc, fieldId, newValue);
2140
+ if (staged.ok)
2141
+ dependsOnDoc = staged.value;
2142
+ }
2143
+ const dependsOnOut = (0, planning_document_cjs_1.serialize)(dependsOnDoc);
2144
+ if (dependsOnOut.ok)
2145
+ content = dependsOnOut.value;
2146
+ }
1938
2147
  }
1939
2148
  (0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, content);
1940
2149
  // #3685 / #3691: compare NORMALIZED bytes (what platformWriteSync actually
@@ -1958,21 +2167,22 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
1958
2167
  * phase, a stray 'Total Phases: 0' between fences). A file with no leading
1959
2168
  * frontmatter is all body: the field goes to content start, preserving the
1960
2169
  * former behavior for that shape.
2170
+ *
2171
+ * The block is the one `locateFrontmatterFence` finds (the one fence owner), so this writer
2172
+ * and every STATE.md reader agree on where the body starts. The blank line and the field go
2173
+ * right after the closing fence line, ended by that line's own line ending — so a CRLF file
2174
+ * gains CRLF lines only (#3572 review: a blanket re-join on '\r\n' doubled every carriage
2175
+ * return; joining the new lines on a bare '\n' mixed line endings).
1961
2176
  */
1962
2177
  function insertStateBodyFieldAtTop(content, fieldLine) {
1963
- // Split AND join on bare '\n' so CRLF line endings stay attached to their
1964
- // own lines — each '\r' remains the tail of the line it terminated, where
1965
- // the trimmed fence compare still matches it. (#3572 review: splitting on
1966
- // '\n' but re-joining on a detected '\r\n' doubled every carriage return.)
1967
- const lines = content.split('\n');
1968
- if ((lines[0] ?? '').trim() === '---') {
1969
- const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === '---');
1970
- if (closeIdx !== -1) {
1971
- lines.splice(closeIdx + 1, 0, '', fieldLine);
1972
- return lines.join('\n');
1973
- }
1974
- }
1975
- return fieldLine + '\n' + content;
2178
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content);
2179
+ if (fence?.closed) {
2180
+ const nl = content[fence.closingFenceEnd] === '\r' ? '\r\n' : '\n';
2181
+ return `${content.slice(0, fence.closingFenceEnd)}${nl}${nl}${fieldLine}${content.slice(fence.closingFenceEnd)}`;
2182
+ }
2183
+ // All body: the field is the new first line, ended like the document's first line.
2184
+ const firstNewline = content.indexOf('\n');
2185
+ return fieldLine + (firstNewline > 0 && content[firstNewline - 1] === '\r' ? '\r\n' : '\n') + content;
1976
2186
  }
1977
2187
  function cmdPhaseRemove(cwd, targetPhase, options, raw) {
1978
2188
  if (!targetPhase)
@@ -2017,6 +2227,21 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
2017
2227
  error(`Phase ${targetPhase} has ${summaryCount} executed plan(s). Use --force to remove anyway.`);
2018
2228
  }
2019
2229
  }
2230
+ // #5118 (no write before the error): the STATE.md rewrite below rebuilds the
2231
+ // frontmatter from EVERY surviving phase's report (buildStateFrontmatter →
2232
+ // isPhaseComplete), and it runs AFTER the directory removal, the sibling
2233
+ // renames and the ROADMAP rewrite. Validate every surviving phase's report
2234
+ // here, BEFORE the first write, so a report whose `status` is outside the
2235
+ // closed set fails this command having written nothing (the removed phase's
2236
+ // own report is not read afterwards — it is excluded). The set is the one the
2237
+ // rebuild scans (`statePhaseDirsToScan`: milestone-scoped, deduped), so a
2238
+ // survivor OUTSIDE that set — another milestone's phase directory — does not
2239
+ // block the remove.
2240
+ if (node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md'))) {
2241
+ const survivorStatusError = findVerificationStatusError(stateMod.statePhaseDirsToScan(cwd).filter((d) => d !== targetDir).map((d) => node_path_1.default.join(phasesDir, d)), { convention: resolvePhaseIdConvention(cwd) });
2242
+ if (survivorStatusError)
2243
+ throw survivorStatusError;
2244
+ }
2020
2245
  if (targetDir)
2021
2246
  node_fs_1.default.rmSync(node_path_1.default.join(phasesDir, targetDir), { recursive: true, force: true });
2022
2247
  let renamedDirs = [];
@@ -2177,7 +2402,20 @@ function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
2177
2402
  if (!roadmapContent || !phaseNum)
2178
2403
  return null;
2179
2404
  const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
2180
- const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:\\s*([^\\n]+)`, 'im'));
2405
+ // #5007 (Phase 6 / ADR-4910 §8): buildPhaseHeadingRegex (src/roadmap.cts) is
2406
+ // anchored `^...$` with no 'm' flag by design — it expects one
2407
+ // already-tokenized heading LINE, not a `^`/`m` scan against multi-line
2408
+ // content directly. Routed through tokenizeHeadings first (the same real
2409
+ // refactor searchPhaseInContent's own usage already requires), not a pure
2410
+ // regex-literal swap. Adopts buildPhaseHeadingRegex's ANY_BRACKET baseline —
2411
+ // a deliberate widening, matching getRoadmapModeForPhase's identical choice
2412
+ // above: this is a real-heading reader, so it inherits the same bracket-tag
2413
+ // tolerance searchPhaseInContent already applies everywhere else.
2414
+ const headingPattern = buildPhaseHeadingRegex(phaseEscaped);
2415
+ const matchedHeading = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(roadmapContent).find((h) => headingPattern.test(h.text));
2416
+ if (!matchedHeading)
2417
+ return null;
2418
+ const heading = matchedHeading.text.match(headingPattern);
2181
2419
  if (!heading)
2182
2420
  return null;
2183
2421
  const name = heading[1].replace(/\(INSERTED\)/i, '').trim();
@@ -2838,7 +3076,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
2838
3076
  // non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
2839
3077
  // here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
2840
3078
  // which would misreport a present-but-bad marker as no marker at all.
2841
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
3079
+ const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
2842
3080
  if (availableWorkstreams.length > 0 && !resolvedWorkstream) {
2843
3081
  // #3579: getActiveWorkstream now inherits a pointer-less session's read
2844
3082
  // from the shared .planning/active-workstream marker, so reaching this
@@ -2985,27 +3223,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
2985
3223
  if (/status: diagnosed/.test(content))
2986
3224
  warnings.push(`${file}: has diagnosed gaps`);
2987
3225
  }
2988
- for (const file of scopeToPhase(phaseFiles.filter((f) => f.includes('-VERIFICATION') && f.endsWith('.md')), phaseFullDirBaseName)) {
2989
- const verificationFilePath = node_path_1.default.join(phaseFullDir, file);
2990
- // #3707-CR follow-up MINOR: normalize line endings at this read boundary
2991
- // (same fix as src/verification.cts's readVerificationStatus) so a
2992
- // lone-CR VERIFICATION.md's `---\r...\r---` frontmatter fence still
2993
- // matches extractFrontmatter's byte-0 check instead of silently
2994
- // dropping the human_needed/gaps_found advisory warning below.
2995
- const content = normalizeLineEndings(node_fs_1.default.readFileSync(verificationFilePath, 'utf-8'));
2996
- // #1159 (Defect A): read ONLY the frontmatter `status` key to avoid false positives
2997
- // from historical metadata in the file body (e.g. `previous_status: gaps_found`).
2998
- // A full-text regex like /status: gaps_found/ matches the substring inside
2999
- // `previous_status: gaps_found`, producing spurious warnings even when the
3000
- // current frontmatter status is `passed`.
3001
- const verFm = extractFrontmatter(content, verificationFilePath);
3002
- // Normalise to lower-case so `status: Passed` (title-case) is not missed.
3003
- const verStatus = typeof verFm['status'] === 'string' ? verFm['status'].trim().toLowerCase() : '';
3004
- if (verStatus === 'human_needed')
3005
- warnings.push(`${file}: needs human verification`);
3006
- if (verStatus === 'gaps_found')
3007
- warnings.push(`${file}: has unresolved gaps`);
3008
- }
3226
+ // #5118 (ADR-5057 Phase 4): the VERIFICATION report's `status` is no
3227
+ // longer read here. This pre-scan used to read each report's frontmatter
3228
+ // itself (case-folded, bypassing the owner) to warn on human_needed /
3229
+ // gaps_found — but the completion GATE below (readVerificationStatus)
3230
+ // already refuses those statuses and names the route, so the advisory
3231
+ // could only ever duplicate the gate's own answer. One reader: the gate.
3009
3232
  }
3010
3233
  catch {
3011
3234
  /* best-effort (#2245 audit): this is an ADVISORY pre-scan of UAT/
@@ -3084,6 +3307,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3084
3307
  // #2617: pass the project's runtime so the blocked-completion error below
3085
3308
  // suggests the command surface this runtime actually installs
3086
3309
  // ($gsd-… on Codex) rather than a hard-coded Claude-style string.
3310
+ // #5118: an out-of-set report status THROWS VerificationStatusError out
3311
+ // of here; withPlanningLock releases the lock on the way out and the CLI
3312
+ // seam reports `verification_status_invalid`.
3087
3313
  const verificationStatus = readVerificationStatus(phaseFullDir, {
3088
3314
  runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd),
3089
3315
  convention: resolvePhaseIdConvention(cwd),
@@ -3101,9 +3327,22 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3101
3327
  staleCheckIndeterminate = true;
3102
3328
  warnings.push(`verification staleness check could not complete for phase ${phaseNum} — routed as not-stale, but this was not actually verified (#3057)`);
3103
3329
  }
3104
- if (verificationStatus.status !== 'passed') {
3330
+ if (verificationStatus.status !== VERIFICATION_STATUS.PASSED) {
3105
3331
  return verificationStatus;
3106
3332
  }
3333
+ // #5118 (no write before the error): the transaction below writes ROADMAP,
3334
+ // REQUIREMENTS and STATE, and the STATE frontmatter rebuild reads EVERY
3335
+ // phase's report (buildStateFrontmatter → isPhaseComplete). Validate it
3336
+ // here, BEFORE the first write, over EXACTLY the set that rebuild scans
3337
+ // (the milestone-scoped, deduped phase set — the owner runs
3338
+ // buildStateFrontmatter itself, so `state sync` and this command refuse
3339
+ // for the same phases), so a report whose `status` is outside the closed
3340
+ // set fails this command having written nothing (withPlanningLock
3341
+ // releases the lock).
3342
+ const preflightStatePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
3343
+ if (node_fs_1.default.existsSync(preflightStatePath)) {
3344
+ assertVerificationReportsReadable(node_fs_1.default.readFileSync(preflightStatePath, 'utf-8'), cwd);
3345
+ }
3107
3346
  const runPhaseCompleteTransaction = () => {
3108
3347
  const writes = [];
3109
3348
  let roadmapContent = null;
@@ -3130,7 +3369,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3130
3369
  // whole-slice `.replace()` onto the seam. Applied per single physical
3131
3370
  // line by updateBullet, so the pattern no longer needs the `m` flag
3132
3371
  // (it never sees more than one line at a time); see
3133
- // planCountBodyPattern below for the sites that were migrated onto
3372
+ // writePlansField below for the sites that were migrated onto
3134
3373
  // withPhaseSection instead.
3135
3374
  //
3136
3375
  // #2245 review Fix 6: this is behaviour-preserving for GSD-GENERATED
@@ -3183,12 +3422,105 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3183
3422
  };
3184
3423
  // ADR-2143 §4: the plan-count write is now routed through
3185
3424
  // withPhaseSection (see mutateMilestonePhase below), which hands this
3186
- // pattern ONLY phase N's own detail-section body — so the pattern no
3187
- // longer needs its own `#{2,4}\s*Phase\s+N` anchor + skip-ahead-past-
3188
- // interior-headings lookahead; the section boundary itself confines
3189
- // the match (the #2067/#2200 boundary-crossing class is now
3190
- // structurally impossible for this site rather than regex-enforced).
3191
- const planCountBodyPattern = /(\*\*Plans:\*\*\s*)[^\n]+/i;
3425
+ // seam call ONLY phase N's own detail-section body — the section
3426
+ // boundary itself confines the write (the #2067/#2200 boundary-
3427
+ // crossing class is structurally impossible for this site).
3428
+ //
3429
+ // #4906 Phase 2 (#4917/ADR-4910): migrated off the one-capture-group
3430
+ // regex that replaced to end of line, dropping any hand-written
3431
+ // trailing prose after the count (#4852) — onto the PlanningDoc
3432
+ // `boldField` write seam, whose `valueSpan`/`trailingSpan` split
3433
+ // never touches the trailing annotation.
3434
+ const writePlansField = (body) => {
3435
+ const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(body, 'ROADMAP.md');
3436
+ if (!parsed.ok) {
3437
+ preservationWarnings.push({ field: 'Plans', reason: parsed.reason });
3438
+ return body;
3439
+ }
3440
+ const fieldId = (0, planning_document_cjs_1.findField)(parsed.value, 'Plans');
3441
+ if (!fieldId) {
3442
+ // #4906 regression (#1163 parity, caught by gsd-test against
3443
+ // roadmap.cts's sibling site): a hand-edited or pre-template
3444
+ // ROADMAP.md may carry a PLAIN (non-bold) `Plans:` line rather
3445
+ // than the canonical `**Plans**:`/`**Plans:**` bold field.
3446
+ // BOLD_FIELD_RE stays bold-only (widening it would register
3447
+ // ordinary prose as a spurious field seam-wide) — this fallback
3448
+ // mirrors roadmap.cts's identical one, kept in parity per
3449
+ // Decision 2 rather than letting the two sites diverge on which
3450
+ // legacy shapes they tolerate.
3451
+ const plainMatch = body.match(/^([ \t]*)Plans:([ \t]*)([^\r\n]*)$/m);
3452
+ if (!plainMatch) {
3453
+ // No `**Plans:**`/`**Plans**:`/plain `Plans:` line in this
3454
+ // phase's section — nothing to write; not a failure (mirrors
3455
+ // the old regex's silent no-match no-op).
3456
+ return body;
3457
+ }
3458
+ const [whole, indent, spacing, plainValue] = plainMatch;
3459
+ const plainCountPrefixMatch = plainValue.match(/^(?:\d+\s*\/\s*\d+\s+plans(?:\s+(?:complete|executed))?|\d+\s+plans?)/i);
3460
+ const plainIsTemplatePlaceholder = /^\[\s*Number of plans\b[\s\S]*\]$/i.test(plainValue.trim());
3461
+ if (!plainCountPrefixMatch && !plainIsTemplatePlaceholder) {
3462
+ // Arm 3: freeform prose, TBD, a bracketed human annotation, or
3463
+ // an empty value — leave the field exactly as it was.
3464
+ return body;
3465
+ }
3466
+ const plainNewCountText = `${summaryCount}/${planCount} plans complete`;
3467
+ const plainSuffix = plainCountPrefixMatch ? plainValue.slice(plainCountPrefixMatch[0].length) : '';
3468
+ const newPlainLine = `${indent}Plans:${spacing}${plainNewCountText}${plainSuffix}`;
3469
+ const start = plainMatch.index ?? body.indexOf(whole);
3470
+ return body.slice(0, start) + newPlainLine + body.slice(start + whole.length);
3471
+ }
3472
+ // #4906 regression fix: PREFIX-match the existing value's count
3473
+ // token and re-glue whatever follows it VERBATIM — a glued-on
3474
+ // annotation with no ` — ` separator (e.g. a parenthetical like
3475
+ // `0/1 plans executed (11-16 are gap closure from VERIFICATION)`)
3476
+ // lives entirely inside `value` (`TRAILING_SEPARATOR_RE` in
3477
+ // planning-document.cts only splits on ` — `, unchanged/correct),
3478
+ // so overwriting `value` outright previously destroyed it.
3479
+ //
3480
+ // #4906 review finding (isolated adversarial pass): the prior
3481
+ // version of this migration preserved this site's OLD
3482
+ // unconditional-overwrite behavior for the no-count-prefix case,
3483
+ // which clobbers arm 3 (freeform prose / TBD / a bracketed human
3484
+ // annotation like `[Deferred pending re-scope]`) — a real
3485
+ // regression against the design doc's own Behavior table row 4,
3486
+ // not an accepted trade-off. Fixed here by adopting the SAME
3487
+ // template-placeholder / arm-3-untouched classification
3488
+ // roadmap.cts's sibling site already uses (isTemplatePlaceholder +
3489
+ // "no count prefix and not a placeholder => leave untouched"),
3490
+ // rather than letting the two migrated sites diverge on this.
3491
+ const newCountText = `${summaryCount}/${planCount} plans complete`;
3492
+ const current = (0, planning_document_cjs_1.readNode)(parsed.value, fieldId);
3493
+ if (!current.ok) {
3494
+ return body;
3495
+ }
3496
+ const currentValue = current.value;
3497
+ const countPrefixMatch = currentValue.match(/^(?:\d+\s*\/\s*\d+\s+plans(?:\s+(?:complete|executed))?|\d+\s+plans?)/i);
3498
+ const isTemplatePlaceholder = /^\[\s*Number of plans\b[\s\S]*\]$/i.test(currentValue.trim());
3499
+ if (!countPrefixMatch && !isTemplatePlaceholder) {
3500
+ // Arm 3: freeform prose, TBD, a bracketed human annotation, or an
3501
+ // empty value — leave the field exactly as it was.
3502
+ return body;
3503
+ }
3504
+ const newValueToWrite = countPrefixMatch
3505
+ ? newCountText + currentValue.slice(countPrefixMatch[0].length)
3506
+ : newCountText;
3507
+ const staged = (0, planning_document_cjs_1.setFieldValue)(parsed.value, fieldId, newValueToWrite);
3508
+ if (!staged.ok) {
3509
+ preservationWarnings.push({ field: 'Plans', reason: staged.reason });
3510
+ return body;
3511
+ }
3512
+ const out = (0, planning_document_cjs_1.serialize)(staged.value);
3513
+ if (!out.ok) {
3514
+ // `hasUnreadableNodes` refusal (ADR-4910 amendment) — a ragged
3515
+ // SIBLING node elsewhere in this same section refuses the whole
3516
+ // splice. Never throw / crash the phase-complete transaction over
3517
+ // a node unrelated to this write; surface it and leave `body`
3518
+ // unchanged, same as any other preservation warning.
3519
+ preservationWarnings.push({ field: 'Plans', reason: out.reason });
3520
+ return body;
3521
+ }
3522
+ return out.value;
3523
+ };
3192
3524
  const phaseInfoSummaries = phaseInfo['summaries'];
3193
3525
  // #2200: apply the phase-checkbox flip, the plan-count write, and the
3194
3526
  // per-plan checkbox flips ONLY within the current milestone's region(s)
@@ -3204,7 +3536,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3204
3536
  const plansResult = (0, markdown_table_cjs_1.updateTableCell)(text, rowMatch, 'Plans Complete', ` ${summaryCount}/${planCount} `);
3205
3537
  if (plansResult.ok)
3206
3538
  text = plansResult.value;
3207
- const statusResult = (0, markdown_table_cjs_1.updateTableCell)(text, rowMatch, 'Status', ' Complete ');
3539
+ const statusResult = (0, markdown_table_cjs_1.updateTableCell)(text, rowMatch, 'Status', ` ${(0, phase_status_cjs_1.toRoadmapStatusCell)(phase_status_cjs_1.PHASE_STATUS.COMPLETE).padEnd(11)} `);
3208
3540
  if (statusResult.ok)
3209
3541
  text = statusResult.value;
3210
3542
  // Preserve only a valid ISO date (#1161: idempotent; self-heal
@@ -3225,14 +3557,18 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3225
3557
  // section's body, so neither regex can escape into a sibling
3226
3558
  // phase's section, a shipped milestone, or a Backlog entry.
3227
3559
  s = withPhaseSection(s, phaseNum, (body) => {
3228
- let b = body.replace(planCountBodyPattern, `$1${summaryCount}/${planCount} plans complete`);
3560
+ let b = writePlansField(body);
3229
3561
  for (const summaryFile of phaseInfoSummaries) {
3230
3562
  const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
3231
3563
  if (!planId)
3232
3564
  continue;
3233
3565
  const planEscaped = (0, pattern_cjs_1.escapeRegex)(planId);
3234
- const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
3235
- b = b.replace(planCheckboxPattern, '$1x$2');
3566
+ // ADR-5057 §6 (Phase 13, #5217): the per-plan flip is a bullet
3567
+ // mutation, so it goes through `updateBullet` like the phase
3568
+ // checkbox flip above — first matching bullet only, never a
3569
+ // fenced line, never a mid-line occurrence.
3570
+ const planCheckboxPattern = new RegExp(`^(\\s*-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
3571
+ b = (0, markdown_sectionizer_cjs_1.updateBullet)(b, (_bulletText, rawLine) => planCheckboxPattern.test(rawLine), (rawLine) => rawLine.replace(planCheckboxPattern, '$1x$2'));
3236
3572
  }
3237
3573
  return b;
3238
3574
  });
@@ -3270,14 +3606,38 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3270
3606
  if (node_fs_1.default.existsSync(reqPath)) {
3271
3607
  const phaseEsc = phaseMarkdownRegexSource(phaseNum);
3272
3608
  const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
3273
- const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
3609
+ // #5007 (Phase 6 / ADR-4910 §8): this site captures the entire
3610
+ // section BODY through to the next heading (via its own
3611
+ // `[\s\S]*?` lookahead-to-next-heading structure) — neither shared
3612
+ // owner returns that shape, so phaseHeadingPrefixSrcFor is composed
3613
+ // directly, keeping this site's own lookahead. The prior hand-rolled
3614
+ // heading-marker-plus-label literal appeared TWICE in this source
3615
+ // (the main capture prefix AND the lookahead) — both occurrences
3616
+ // were the literal-drift detector's target
3617
+ // (findPhaseHeadingScanLiteralDrift is a pure literal-text match,
3618
+ // blind to what surrounds it), so both are replaced with the same
3619
+ // composed prefix, built once via phaseHeadingPrefixSrcFor and
3620
+ // reused, so the two halves cannot drift from each other. No
3621
+ // convention argument: LABEL_ONLY with convention=undefined
3622
+ // reproduces the prior `Phase\s+` literal byte-for-byte — this
3623
+ // feeds cmdPhaseComplete, so no widening was introduced.
3624
+ const phaseSectionHeadingPrefix = `#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY)}`;
3625
+ const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(`(${phaseSectionHeadingPrefix}${phaseEsc}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][\\s\\S]*?)(?=${phaseSectionHeadingPrefix}|$)`, 'i'));
3274
3626
  const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
3275
- const reqMatch = sectionText.match(/\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]+)/i);
3627
+ // #4731: multiline-aware — hard-wrapped Requirements read past the
3628
+ // line break before the ID scan. The shared extractor also stops at
3629
+ // headings and table rows, so a Requirements field followed by the
3630
+ // Traceability table cannot bleed other phases' REQ-IDs into the
3631
+ // citation scan (isolated-review MEDIUM on the inline lookahead,
3632
+ // whose lazy capture swallowed everything to section end).
3633
+ const reqLine = sectionText
3634
+ ? roadmapParserMod.extractPhaseFieldMultiline(sectionText, 'Requirements')
3635
+ : null;
3276
3636
  const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
3277
3637
  let reqContent = originalReqContent;
3278
3638
  // #2316: `citedReqIds` — the REQ-IDs ROADMAP's own **Requirements:**
3279
3639
  // line for this phase actually cites — is hoisted out of the
3280
- // `if (reqMatch)` block (previously scoped only inside it) so the
3640
+ // `if (reqLine)` block (previously scoped only inside it) so the
3281
3641
  // ghost-ID cross-check below (~#2316-1) can consult it. `TBD` is the
3282
3642
  // literal placeholder `phase.add`/`-batch`/`-insert` seed
3283
3643
  // (`**Requirements**: TBD`, src/phase.cts:833,920,1078) — never a
@@ -3289,16 +3649,16 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3289
3649
  // otherwise) — the `if (reqUpdate.ok)` below previously had no
3290
3650
  // `else`, discarding this fact silently instead of surfacing it.
3291
3651
  const traceabilityWriteMisses = [];
3292
- if (reqMatch) {
3652
+ if (reqLine) {
3293
3653
  // #2334 HIGH 3 + #3697: selection and under-selection detection both
3294
3654
  // live in `analyzeRequirementsLine` (module scope, above), extracted in
3295
3655
  // round 3 so the parser is directly testable — a closure in here is
3296
3656
  // reachable only by spawning the CLI, which no fast-check property test
3297
3657
  // can do. `citedReqIds` is byte-identical to the expression that stood
3298
3658
  // here; nothing about what phase-complete MARKS has changed.
3299
- const reqLineAnalysis = analyzeRequirementsLine(reqMatch[1]);
3659
+ const reqLineAnalysis = analyzeRequirementsLine(reqLine);
3300
3660
  citedReqIds = reqLineAnalysis.citedReqIds;
3301
- const reqLineWarning = formatRequirementsLineWarning(phaseNum, reqMatch[1], reqLineAnalysis);
3661
+ const reqLineWarning = formatRequirementsLineWarning(phaseNum, reqLine, reqLineAnalysis);
3302
3662
  if (reqLineWarning) {
3303
3663
  warnings.push(reqLineWarning.message);
3304
3664
  // Carried out to the JSON result as its own field — see
@@ -3584,6 +3944,34 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3584
3944
  let diskNextName = null;
3585
3945
  let roadmapNextNum = null;
3586
3946
  let roadmapNextName = null;
3947
+ // #4699: a phase whose roadmap checkbox is `[x]` is already complete and
3948
+ // must never be selected as next_phase — out-of-order completion (a
3949
+ // reopened phase finished after later phases shipped) otherwise persists
3950
+ // the already-done phase as STATE.md current_phase. Collected from the
3951
+ // same milestone-scoped text the roadmap scan walks; membership is
3952
+ // comparePhaseNum-based so `02` and `2` dedupe. With no ROADMAP.md (or no
3953
+ // parseable rows) the set is empty and the scans behave exactly as
3954
+ // before.
3955
+ const roadmapCompleteNums = [];
3956
+ if (roadmapContent !== null) {
3957
+ try {
3958
+ const milestoneForComplete = extractCurrentMilestone(roadmapContent, cwd);
3959
+ const completePattern = new RegExp(`-\\s*\\[[xX]\\]\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'gi');
3960
+ let cm;
3961
+ while ((cm = completePattern.exec(milestoneForComplete)) !== null) {
3962
+ if (isSentinelPhaseId(cm[1]))
3963
+ continue;
3964
+ if (!roadmapCompleteNums.some((n) => comparePhaseNum(cm[1], n) === 0)) {
3965
+ roadmapCompleteNums.push(cm[1]);
3966
+ }
3967
+ }
3968
+ }
3969
+ catch {
3970
+ /* best-effort: an unreadable milestone section leaves the complete
3971
+ * set empty — the scans then behave exactly as they did pre-#4699. */
3972
+ }
3973
+ }
3974
+ const isCompletePhaseNum = (num) => roadmapCompleteNums.some((n) => comparePhaseNum(num, n) === 0);
3587
3975
  try {
3588
3976
  // #3185 (ADR-3180 Decision 1): "which phase directories belong to
3589
3977
  // the CURRENT milestone" — routed through the canonical owner
@@ -3599,6 +3987,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3599
3987
  // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
3600
3988
  if (isSentinelPhaseId(dm[1]))
3601
3989
  continue;
3990
+ // #4699: an already-complete phase (roadmap checkbox [x]) is never
3991
+ // a next_phase candidate — out-of-order completion must skip it.
3992
+ if (roadmapContent !== null && isCompletePhaseNum(dm[1]))
3993
+ continue;
3602
3994
  // Numeric MINIMUM above N, not "first encountered". `listMilestonePhaseDirs`
3603
3995
  // does sort by `comparePhaseNum`, so a `break` on the first hit happens to be
3604
3996
  // correct today — but that makes this scan's correctness depend on an
@@ -3666,6 +4058,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3666
4058
  // stage 2's heading scan must not advance into backlog headings either.
3667
4059
  if (isSentinelPhaseId(pmNum))
3668
4060
  continue;
4061
+ // #4699: skip complete phases — a `[x]` checkbox row and the
4062
+ // `## Phase Details` heading of an already-done phase both name a
4063
+ // phase that must never be next_phase.
4064
+ if (roadmapContent !== null && isCompletePhaseNum(pmNum))
4065
+ continue;
3669
4066
  // #3701 review: the numeric MINIMUM above N, not the first row above N in
3670
4067
  // DOCUMENT order. This scan walks raw roadmap text, and one global regex
3671
4068
  // sweeps both the `## Phases` checklist and the `## Phase Details`
@@ -4009,6 +4406,9 @@ function cmdPhaseUatPassed(cwd, phaseNum, raw, opts = {}) {
4009
4406
  const phaseFullDir = node_path_1.default.join(cwd, phaseInfo['directory']);
4010
4407
  const report = evaluateUatPassed(phaseFullDir, { policy: opts.policy });
4011
4408
  output({ phase: phaseNum, ...report }, raw);
4409
+ // #5170: the exit status follows the verdict (status mode: a failing verdict is exit 1). Declared
4410
+ // AFTER output(), which rewrites the pending-outcome cell. The JSON above is unchanged.
4411
+ (0, gate_exit_cjs_1.declareGateExit)((0, gate_verdict_cjs_1.gateVerdict)(report.passed ? 'pass' : 'block', !report.passed, { phase: phaseNum, ...report }), 'status');
4012
4412
  }
4013
4413
  // #1437 — phase.list-plans: list plan files for a given phase number.
4014
4414
  // Returns the full scan result from scanPhasePlans so callers can read plan
@@ -4057,4 +4457,5 @@ module.exports = {
4057
4457
  cmdPhaseListPlans,
4058
4458
  computeDependencyLevels,
4059
4459
  buildShortFormToId,
4460
+ normalizeDependencyToken,
4060
4461
  };