@opengsd/gsd-core 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (441) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-advisor-researcher.compact.md +85 -0
  8. package/agents/gsd-ai-researcher.compact.md +96 -0
  9. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  10. package/agents/gsd-code-fixer.compact.md +459 -0
  11. package/agents/gsd-code-fixer.md +9 -8
  12. package/agents/gsd-code-reviewer.compact.md +269 -0
  13. package/agents/gsd-code-reviewer.md +15 -3
  14. package/agents/gsd-codebase-mapper.compact.md +760 -0
  15. package/agents/gsd-debug-session-manager.compact.md +360 -0
  16. package/agents/gsd-debug-session-manager.md +17 -2
  17. package/agents/gsd-debugger.md +2 -2
  18. package/agents/gsd-doc-classifier.compact.md +192 -0
  19. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  20. package/agents/gsd-doc-verifier.compact.md +143 -0
  21. package/agents/gsd-doc-writer.compact.md +440 -0
  22. package/agents/gsd-dom-verifier.compact.md +138 -0
  23. package/agents/gsd-domain-researcher.compact.md +141 -0
  24. package/agents/gsd-eval-auditor.compact.md +160 -0
  25. package/agents/gsd-eval-auditor.md +1 -1
  26. package/agents/gsd-eval-planner.compact.md +137 -0
  27. package/agents/gsd-executor.md +13 -8
  28. package/agents/gsd-framework-selector.compact.md +82 -0
  29. package/agents/gsd-integration-checker.compact.md +245 -0
  30. package/agents/gsd-intel-updater.compact.md +226 -0
  31. package/agents/gsd-intel-updater.md +1 -1
  32. package/agents/gsd-mempalace-curator.compact.md +45 -0
  33. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  34. package/agents/gsd-pattern-mapper.compact.md +275 -0
  35. package/agents/gsd-phase-researcher.md +19 -11
  36. package/agents/gsd-plan-checker.md +8 -7
  37. package/agents/gsd-planner.md +12 -8
  38. package/agents/gsd-project-researcher.compact.md +587 -0
  39. package/agents/gsd-project-researcher.md +1 -1
  40. package/agents/gsd-research-synthesizer.compact.md +212 -0
  41. package/agents/gsd-research-synthesizer.md +1 -1
  42. package/agents/gsd-roadmapper.compact.md +454 -0
  43. package/agents/gsd-roadmapper.md +13 -0
  44. package/agents/gsd-security-auditor.compact.md +162 -0
  45. package/agents/gsd-ui-auditor.compact.md +404 -0
  46. package/agents/gsd-ui-auditor.md +155 -17
  47. package/agents/gsd-ui-checker.compact.md +277 -0
  48. package/agents/gsd-ui-researcher.compact.md +282 -0
  49. package/agents/gsd-ui-researcher.md +1 -1
  50. package/agents/gsd-user-profiler.compact.md +108 -0
  51. package/agents/gsd-verifier.md +10 -9
  52. package/bin/install.js +848 -163
  53. package/commands/gsd/autonomous.md +2 -2
  54. package/commands/gsd/capture.md +1 -1
  55. package/commands/gsd/cleanup.md +1 -0
  56. package/commands/gsd/code-review.md +2 -1
  57. package/commands/gsd/complete-milestone.md +1 -0
  58. package/commands/gsd/config.md +1 -0
  59. package/commands/gsd/debug.md +1 -0
  60. package/commands/gsd/graphify.md +1 -0
  61. package/commands/gsd/health.md +1 -0
  62. package/commands/gsd/mempalace-capture.md +8 -3
  63. package/commands/gsd/mempalace-recall.md +1 -0
  64. package/commands/gsd/new-milestone.md +1 -0
  65. package/commands/gsd/new-project.md +1 -0
  66. package/commands/gsd/next.md +1 -0
  67. package/commands/gsd/pause-work.md +1 -0
  68. package/commands/gsd/phase.md +1 -0
  69. package/commands/gsd/plan-review-convergence.md +6 -6
  70. package/commands/gsd/pr-branch.md +1 -0
  71. package/commands/gsd/progress.md +1 -1
  72. package/commands/gsd/quick-batch.md +1 -1
  73. package/commands/gsd/resume-work.md +1 -0
  74. package/commands/gsd/review-backlog.md +1 -0
  75. package/commands/gsd/review.md +2 -3
  76. package/commands/gsd/settings.md +2 -1
  77. package/commands/gsd/stats.md +1 -0
  78. package/commands/gsd/thread.md +1 -0
  79. package/commands/gsd/workspace.md +1 -0
  80. package/commands/gsd/workstreams.md +1 -0
  81. package/gsd-core/bin/check-latest-version.cjs +8 -3
  82. package/gsd-core/bin/gsd-tools.cjs +672 -146
  83. package/gsd-core/bin/lib/adr-parser.cjs +4 -2
  84. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  85. package/gsd-core/bin/lib/audit.cjs +119 -34
  86. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  87. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  88. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  89. package/gsd-core/bin/lib/capability-registry.cjs +96 -189
  90. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  91. package/gsd-core/bin/lib/capability-validator.cjs +14 -2
  92. package/gsd-core/bin/lib/check-command-router.cjs +213 -49
  93. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  94. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  95. package/gsd-core/bin/lib/commands.cjs +823 -112
  96. package/gsd-core/bin/lib/config-loader.cjs +66 -4
  97. package/gsd-core/bin/lib/config.cjs +186 -45
  98. package/gsd-core/bin/lib/coverage.cjs +1 -1
  99. package/gsd-core/bin/lib/decisions.cjs +164 -45
  100. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  101. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  102. package/gsd-core/bin/lib/graphify.cjs +10 -2
  103. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  104. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  105. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  106. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  107. package/gsd-core/bin/lib/init.cjs +614 -86
  108. package/gsd-core/bin/lib/install-engine.cjs +29 -3
  109. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  110. package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
  111. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  112. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  113. package/gsd-core/bin/lib/milestone.cjs +37 -13
  114. package/gsd-core/bin/lib/model-resolver.cjs +253 -53
  115. package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
  116. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  117. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  118. package/gsd-core/bin/lib/phase-id.cjs +268 -27
  119. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  120. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  121. package/gsd-core/bin/lib/phase.cjs +393 -88
  122. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  123. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  126. package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
  127. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  128. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  129. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  130. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  131. package/gsd-core/bin/lib/project-root.cjs +41 -2
  132. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  133. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  134. package/gsd-core/bin/lib/research-store.cjs +11 -12
  135. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  136. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  137. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  138. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  139. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  140. package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
  141. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  142. package/gsd-core/bin/lib/roadmap.cjs +356 -42
  143. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
  144. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
  145. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  146. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  147. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
  148. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  149. package/gsd-core/bin/lib/security.cjs +126 -7
  150. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  151. package/gsd-core/bin/lib/state-document.cjs +130 -28
  152. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  153. package/gsd-core/bin/lib/state-transition.cjs +181 -30
  154. package/gsd-core/bin/lib/state.cjs +265 -27
  155. package/gsd-core/bin/lib/surface.cjs +77 -3
  156. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  157. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  158. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  159. package/gsd-core/bin/lib/uat.cjs +9 -1
  160. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  161. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  162. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  163. package/gsd-core/bin/lib/update-context.cjs +30 -24
  164. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  165. package/gsd-core/bin/lib/verification.cjs +315 -30
  166. package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
  167. package/gsd-core/bin/lib/verify.cjs +320 -48
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  169. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  170. package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
  171. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  172. package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
  173. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  174. package/gsd-core/references/checkpoints.md +5 -3
  175. package/gsd-core/references/compact-content-gate.md +66 -0
  176. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  177. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  178. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  179. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  180. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  181. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  182. package/gsd-core/references/edge-probe.md +195 -21
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  184. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  185. package/gsd-core/references/gsd-run-resolver.md +1 -1
  186. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  187. package/gsd-core/references/model-profiles.md +13 -4
  188. package/gsd-core/references/phase-argument-parsing.md +9 -7
  189. package/gsd-core/references/phase-id-convention.md +28 -0
  190. package/gsd-core/references/planner-gap-closure.md +2 -0
  191. package/gsd-core/references/planner-load-graph-context.md +24 -13
  192. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  193. package/gsd-core/references/planning-config.md +14 -2
  194. package/gsd-core/references/tdd.md +30 -4
  195. package/gsd-core/references/thinking-models-planning.md +18 -2
  196. package/gsd-core/references/ui-consideration-probe.md +10 -5
  197. package/gsd-core/references/verification-patterns.md +17 -4
  198. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  199. package/gsd-core/references/worktree-path-safety.md +433 -2
  200. package/gsd-core/templates/README.md +7 -1
  201. package/gsd-core/templates/state.md +6 -3
  202. package/gsd-core/templates/summary.compact.md +212 -0
  203. package/gsd-core/templates/user-setup.compact.md +199 -0
  204. package/gsd-core/templates/user-setup.md +0 -9
  205. package/gsd-core/templates/verification-report.md +1 -1
  206. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  207. package/gsd-core/workflows/add-backlog.md +1 -1
  208. package/gsd-core/workflows/add-phase.md +1 -1
  209. package/gsd-core/workflows/add-tests.md +2 -2
  210. package/gsd-core/workflows/add-todo.md +6 -5
  211. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  212. package/gsd-core/workflows/audit-fix.md +1 -1
  213. package/gsd-core/workflows/audit-milestone.md +1 -1
  214. package/gsd-core/workflows/audit-uat.md +1 -1
  215. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  216. package/gsd-core/workflows/autonomous.md +29 -16
  217. package/gsd-core/workflows/check-todos.md +6 -4
  218. package/gsd-core/workflows/cleanup.md +5 -3
  219. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  220. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
  221. package/gsd-core/workflows/code-review-fix.md +108 -22
  222. package/gsd-core/workflows/code-review.md +216 -73
  223. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  224. package/gsd-core/workflows/complete-milestone.md +41 -264
  225. package/gsd-core/workflows/debug.md +3 -3
  226. package/gsd-core/workflows/diagnose-issues.md +1 -1
  227. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  228. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  229. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  230. package/gsd-core/workflows/discuss-phase.md +1 -1
  231. package/gsd-core/workflows/do.md +2 -2
  232. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  233. package/gsd-core/workflows/docs-update.md +17 -158
  234. package/gsd-core/workflows/edit-phase.md +1 -1
  235. package/gsd-core/workflows/eval-review.md +10 -3
  236. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  237. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  238. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
  239. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  240. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
  241. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  242. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  243. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  244. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  245. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  246. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  247. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  248. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  249. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  250. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  251. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  252. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  253. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  254. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  255. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  256. package/gsd-core/workflows/execute-phase.md +83 -172
  257. package/gsd-core/workflows/execute-plan.md +24 -10
  258. package/gsd-core/workflows/explore.md +3 -3
  259. package/gsd-core/workflows/extract-learnings.md +2 -1
  260. package/gsd-core/workflows/fast.md +1 -1
  261. package/gsd-core/workflows/forensics.md +1 -1
  262. package/gsd-core/workflows/graduation.md +1 -1
  263. package/gsd-core/workflows/health.md +2 -2
  264. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  265. package/gsd-core/workflows/help/modes/full.md +5 -5
  266. package/gsd-core/workflows/help/modes/topic.md +15 -5
  267. package/gsd-core/workflows/help.md +1 -1
  268. package/gsd-core/workflows/import.md +2 -2
  269. package/gsd-core/workflows/inbox.md +2 -2
  270. package/gsd-core/workflows/ingest-docs.md +3 -3
  271. package/gsd-core/workflows/insert-phase.md +1 -1
  272. package/gsd-core/workflows/list-seeds.md +1 -1
  273. package/gsd-core/workflows/list-workspaces.md +1 -1
  274. package/gsd-core/workflows/manager.md +2 -2
  275. package/gsd-core/workflows/map-codebase.md +52 -5
  276. package/gsd-core/workflows/milestone-summary.md +1 -1
  277. package/gsd-core/workflows/mvp-phase.md +1 -1
  278. package/gsd-core/workflows/new-milestone.md +56 -14
  279. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  280. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  281. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  282. package/gsd-core/workflows/new-project.md +39 -209
  283. package/gsd-core/workflows/new-workspace.md +2 -2
  284. package/gsd-core/workflows/next.md +1 -1
  285. package/gsd-core/workflows/note.md +1 -1
  286. package/gsd-core/workflows/onboard.md +1 -1
  287. package/gsd-core/workflows/pause-work.md +1 -1
  288. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  289. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  290. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  291. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  292. package/gsd-core/workflows/plan-phase.md +45 -187
  293. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  294. package/gsd-core/workflows/plant-seed.md +62 -20
  295. package/gsd-core/workflows/pr-branch.md +132 -20
  296. package/gsd-core/workflows/profile-user.md +2 -2
  297. package/gsd-core/workflows/progress.md +1 -1
  298. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  299. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  300. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  301. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  302. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  303. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  304. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  305. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  306. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  307. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  308. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  309. package/gsd-core/workflows/quick-batch.md +1 -1
  310. package/gsd-core/workflows/quick.md +29 -10
  311. package/gsd-core/workflows/reapply-patches.md +86 -6
  312. package/gsd-core/workflows/remove-phase.md +1 -1
  313. package/gsd-core/workflows/remove-workspace.md +2 -2
  314. package/gsd-core/workflows/resume-project.md +1 -1
  315. package/gsd-core/workflows/review.md +31 -16
  316. package/gsd-core/workflows/scan.md +1 -1
  317. package/gsd-core/workflows/secure-phase.md +3 -2
  318. package/gsd-core/workflows/settings-advanced.md +30 -10
  319. package/gsd-core/workflows/settings-integrations.md +2 -3
  320. package/gsd-core/workflows/settings.md +22 -9
  321. package/gsd-core/workflows/ship.md +3 -2
  322. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  323. package/gsd-core/workflows/sketch.md +1 -1
  324. package/gsd-core/workflows/smart-entry.md +2 -2
  325. package/gsd-core/workflows/spec-phase.md +15 -5
  326. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  327. package/gsd-core/workflows/spike.md +1 -1
  328. package/gsd-core/workflows/stats.md +1 -1
  329. package/gsd-core/workflows/sync-skills.md +5 -5
  330. package/gsd-core/workflows/thread.md +1 -1
  331. package/gsd-core/workflows/transition.md +1 -1
  332. package/gsd-core/workflows/ui-phase.md +44 -8
  333. package/gsd-core/workflows/ui-review.md +18 -4
  334. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  335. package/gsd-core/workflows/undo.md +339 -20
  336. package/gsd-core/workflows/update.md +14 -12
  337. package/gsd-core/workflows/validate-phase.md +3 -2
  338. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  339. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  340. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  341. package/gsd-core/workflows/verify-work.md +101 -196
  342. package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
  343. package/hooks/dist/gsd-context-monitor.js +88 -15
  344. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  345. package/hooks/dist/gsd-secret-read-guard.js +71 -19
  346. package/hooks/dist/gsd-statusline.js +81 -20
  347. package/hooks/dist/gsd-validate-commit.sh +97 -8
  348. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  349. package/hooks/dist/gsd-write-guard.js +46 -1
  350. package/hooks/dist/lib/dispatch-identity.js +187 -0
  351. package/hooks/dist/lib/filename-classification.js +64 -0
  352. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  353. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  354. package/hooks/gsd-agent-isolation-guard.js +66 -16
  355. package/hooks/gsd-context-monitor.js +88 -15
  356. package/hooks/gsd-cursor-subagent-start.js +34 -14
  357. package/hooks/gsd-secret-read-guard.js +71 -19
  358. package/hooks/gsd-statusline.js +81 -20
  359. package/hooks/gsd-validate-commit.sh +97 -8
  360. package/hooks/gsd-worktree-path-guard.js +25 -14
  361. package/hooks/gsd-write-guard.js +46 -1
  362. package/hooks/lib/dispatch-identity.js +187 -0
  363. package/hooks/lib/filename-classification.js +64 -0
  364. package/hooks/lib/isolation-deny-reason.js +53 -1
  365. package/hooks/lib/isolation-sentinel.js +58 -19
  366. package/package.json +11 -6
  367. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  368. package/scripts/benchmark-compact-content.cjs +368 -0
  369. package/scripts/build-hooks.js +15 -6
  370. package/scripts/check-contract-drift.cjs +131 -12
  371. package/scripts/check-env.cjs +36 -8
  372. package/scripts/check-glossary-refs.cjs +25 -21
  373. package/scripts/ci-next-health.cjs +271 -0
  374. package/scripts/ci-prepare-test-scope.cjs +7 -7
  375. package/scripts/ci-test-scope.cjs +126 -20
  376. package/scripts/ci-timeout-report.cjs +1 -1
  377. package/scripts/command-contract-helpers.cjs +3 -0
  378. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  379. package/scripts/docs-guard-registry.cjs +35 -2
  380. package/scripts/gen-adr-index.cjs +8 -2
  381. package/scripts/gen-inventory-manifest.cjs +12 -0
  382. package/scripts/gen-loop-host-contract.cjs +69 -0
  383. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  384. package/scripts/lib/drift-scan.cjs +1 -1
  385. package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
  386. package/scripts/lib/ndjson-reporter.cjs +3 -2
  387. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  388. package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
  389. package/scripts/lib/suite-detection.cjs +32 -0
  390. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  391. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
  392. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  393. package/scripts/lint-phase-id-drift.cjs +623 -13
  394. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  395. package/scripts/lint-response-language-coverage.cjs +9 -3
  396. package/scripts/lint-retired-runtime-name.cjs +619 -0
  397. package/scripts/lint-source-test-name-collision.cjs +1 -1
  398. package/scripts/lint-state-write-path-drift.cjs +93 -0
  399. package/scripts/lint-test-file-count.allowlist.json +29 -9
  400. package/scripts/lint-vendored-deps.cjs +128 -17
  401. package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
  402. package/scripts/prompt-injection-scan.sh +18 -0
  403. package/scripts/release-tarball-smoke.cjs +194 -1
  404. package/scripts/workflow-size.cjs +139 -0
  405. package/skills/gsd-autonomous/SKILL.md +2 -2
  406. package/skills/gsd-capture/SKILL.md +1 -1
  407. package/skills/gsd-cleanup/SKILL.md +1 -0
  408. package/skills/gsd-code-review/SKILL.md +2 -1
  409. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  410. package/skills/gsd-config/SKILL.md +1 -0
  411. package/skills/gsd-debug/SKILL.md +1 -0
  412. package/skills/gsd-graphify/SKILL.md +1 -0
  413. package/skills/gsd-health/SKILL.md +1 -0
  414. package/skills/gsd-mempalace-capture/SKILL.md +8 -3
  415. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  416. package/skills/gsd-new-milestone/SKILL.md +1 -0
  417. package/skills/gsd-new-project/SKILL.md +1 -0
  418. package/skills/gsd-next/SKILL.md +1 -0
  419. package/skills/gsd-pause-work/SKILL.md +1 -0
  420. package/skills/gsd-phase/SKILL.md +1 -0
  421. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  422. package/skills/gsd-pr-branch/SKILL.md +1 -0
  423. package/skills/gsd-progress/SKILL.md +1 -1
  424. package/skills/gsd-quick-batch/SKILL.md +1 -1
  425. package/skills/gsd-resume-work/SKILL.md +1 -0
  426. package/skills/gsd-review/SKILL.md +2 -3
  427. package/skills/gsd-review-backlog/SKILL.md +1 -0
  428. package/skills/gsd-settings/SKILL.md +2 -1
  429. package/skills/gsd-stats/SKILL.md +1 -0
  430. package/skills/gsd-thread/SKILL.md +1 -0
  431. package/skills/gsd-workspace/SKILL.md +1 -0
  432. package/skills/gsd-workstreams/SKILL.md +1 -0
  433. package/vscode/package.json +1 -1
  434. package/gsd-core/templates/claude-md.md +0 -145
  435. package/gsd-core/templates/codebase/concerns.md +0 -310
  436. package/gsd-core/templates/codebase/conventions.md +0 -307
  437. package/gsd-core/templates/codebase/integrations.md +0 -280
  438. package/gsd-core/templates/codebase/structure.md +0 -285
  439. package/gsd-core/templates/codebase/testing.md +0 -480
  440. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  441. package/gsd-core/templates/discovery.md +0 -146
@@ -45,10 +45,19 @@ const { normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, matchPhas
45
45
  const pattern_cjs_1 = require("./pattern.cjs");
46
46
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
47
47
  const phaseLocatorMod = require("./phase-locator.cjs");
48
- const { findPhaseInternal, getArchivedPhaseDirs, listMilestonePhaseDirs } = phaseLocatorMod;
48
+ const { findPhaseInternal, getArchivedPhaseDirs, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocatorMod;
49
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
50
+ const planningScopeMod = require("./planning-scope.cjs");
51
+ const { SCOPE } = planningScopeMod;
49
52
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- roadmap-parser.cjs is an export= CommonJS module
50
53
  const roadmapParserMod = require("./roadmap-parser.cjs");
51
54
  const { stripShippedMilestones, extractCurrentMilestone, currentMilestoneRawRanges, withPhaseSection, findMilestoneScopeHeadingLines } = roadmapParserMod;
55
+ // #4129: the single owner of "count the ROADMAP's milestone Complete rows"
56
+ // (pure computation, no I/O — no cycle on this path) for the intent-first
57
+ // progress counters the phase-complete transaction passes downstream.
58
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-lifecycle.cjs is an export= CommonJS module
59
+ const phaseLifecycleMod = require("./phase-lifecycle.cjs");
60
+ const { deriveProgressFromRoadmap: deriveProgressFromRoadmapForIntent, clampPercent: clampPercentForIntent } = phaseLifecycleMod;
52
61
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
53
62
  const planningWorkspace = require("./planning-workspace.cjs");
54
63
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
@@ -56,6 +65,7 @@ const frontmatterMod = require("./frontmatter.cjs");
56
65
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- state.cjs is an export= CommonJS module
57
66
  const stateMod = require("./state.cjs");
58
67
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
68
+ const planning_document_cjs_1 = require("./planning-document.cjs");
59
69
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
60
70
  const clock_cjs_1 = require("./clock.cjs");
61
71
  const state_transition_cjs_1 = require("./state-transition.cjs");
@@ -77,7 +87,7 @@ const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
77
87
  const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSummaryFileBlocked } = planDependencyGraphMod;
78
88
  // #612: `resolvePhaseIdConvention` selects the write-time milestone-scope
79
89
  // guard's terminator vocabulary (see assertDescriptionPreservesMilestoneScope).
80
- const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolvePhaseIdConvention, } = planningWorkspace;
90
+ const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolveEnvWorkstream, resolvePhaseIdConvention, } = planningWorkspace;
81
91
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- milestone-lock.cjs is an export= CommonJS module
82
92
  const milestoneLockMod = require("./milestone-lock.cjs");
83
93
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -287,25 +297,26 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
287
297
  const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
288
298
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
289
299
  baseExists = matchPhaseDirs(dirs, normalized).matches.length > 0;
290
- const dirPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${(0, pattern_cjs_1.escapeRegex)(normalized)}\\.(\\d+)`);
291
- for (const dir of dirs) {
292
- const match = dir.match(dirPattern);
293
- if (match)
294
- decimalSet.add(parseInt(match[1], 10));
295
- }
296
300
  }
297
301
  const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
298
302
  if (node_fs_1.default.existsSync(roadmapPath)) {
299
303
  try {
300
304
  const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
301
- const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalized)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
302
- let pm;
303
- while ((pm = phasePattern.exec(roadmapContent)) !== null) {
304
- decimalSet.add(parseInt(pm[1], 10));
305
+ for (const n of scanExistingDecimalPhaseNumbers(phasesDir, roadmapContent, normalized)) {
306
+ decimalSet.add(n);
305
307
  }
306
308
  }
307
309
  catch {
308
- /* ROADMAP.md read failure is non-fatal */
310
+ // ROADMAP.md read failure is non-fatal — fall back to the directory-only
311
+ // scan (empty rawContent) so on-disk decimal directories are still counted.
312
+ for (const n of scanExistingDecimalPhaseNumbers(phasesDir, '', normalized)) {
313
+ decimalSet.add(n);
314
+ }
315
+ }
316
+ }
317
+ else {
318
+ for (const n of scanExistingDecimalPhaseNumbers(phasesDir, '', normalized)) {
319
+ decimalSet.add(n);
309
320
  }
310
321
  }
311
322
  const existingDecimals = Array.from(decimalSet)
@@ -338,6 +349,7 @@ function getRoadmapModeForPhase(cwd, phaseNum) {
338
349
  const milestoneContent = extractCurrentMilestone(rawContent, cwd);
339
350
  const fullContent = stripShippedMilestones(rawContent);
340
351
  const escapedPhase = phaseMarkdownRegexSource(phaseNum);
352
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
341
353
  const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'i');
342
354
  for (const content of [milestoneContent, fullContent]) {
343
355
  const headerMatch = content.match(phaseHeader);
@@ -582,8 +594,16 @@ function cmdFindPhase(cwd, phase, raw) {
582
594
  * none left in this file) degrades to the pre-#3897 two-tier behavior rather
583
595
  * than throwing on a missing argument.
584
596
  */
597
+ /**
598
+ * The dependency resolver's one comparison normalization. Callers that must
599
+ * predict whether a token names a plan reuse this seam instead of copying its
600
+ * case-folding rule.
601
+ */
602
+ function normalizeDependencyToken(token) {
603
+ return String(token).toLowerCase();
604
+ }
585
605
  function resolveDependencyId(dep, planMap, canonicalToId, shortFormToId) {
586
- const lower = dep.toLowerCase();
606
+ const lower = normalizeDependencyToken(dep);
587
607
  if (planMap.has(lower))
588
608
  return planMap.get(lower).id;
589
609
  if (canonicalToId.has(lower))
@@ -624,7 +644,7 @@ function buildShortFormToId(rawPlans) {
624
644
  const canonical = extractCanonicalPlanId(p.id);
625
645
  const lastDash = canonical.lastIndexOf('-');
626
646
  if (lastDash > 0 && lastDash < canonical.length - 1) {
627
- const shortForm = canonical.slice(lastDash + 1).toLowerCase();
647
+ const shortForm = normalizeDependencyToken(canonical.slice(lastDash + 1));
628
648
  if (/^\d+$/.test(shortForm) && !shortFormToId.has(shortForm)) {
629
649
  shortFormToId.set(shortForm, p.id);
630
650
  }
@@ -734,12 +754,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
734
754
  phase: normalized,
735
755
  error: `Phase ${normalized} is ambiguous: ${ambiguousMatches.length} directories match (${ambiguousMatches.map((m) => `"${m}"`).join(', ')}).`,
736
756
  ambiguous_matches: ambiguousMatches,
737
- plans: [], waves: {}, incomplete: [], has_checkpoints: false,
757
+ plans: [], waves: {}, incomplete: [], runnable: [], ready_plans: [], has_checkpoints: false,
738
758
  }, raw);
739
759
  return;
740
760
  }
741
761
  if (!phaseDir) {
742
- output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], has_checkpoints: false }, raw);
762
+ output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], ready_plans: [], has_checkpoints: false }, raw);
743
763
  return;
744
764
  }
745
765
  void phaseDirName; // used only to set phaseDir above
@@ -834,7 +854,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
834
854
  // ── Pass 2: topological level assignment via depends_on DAG ──────────────
835
855
  const seenLower = new Map();
836
856
  for (const p of rawPlans) {
837
- const lower = p.id.toLowerCase();
857
+ const lower = normalizeDependencyToken(p.id);
838
858
  const existing = seenLower.get(lower);
839
859
  if (existing !== undefined) {
840
860
  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.`);
@@ -842,8 +862,8 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
842
862
  }
843
863
  seenLower.set(lower, p.id);
844
864
  }
845
- const planMap = new Map(rawPlans.map((p) => [p.id.toLowerCase(), p]));
846
- const canonicalToId = new Map(rawPlans.map((p) => [extractCanonicalPlanId(p.id).toLowerCase(), p.id]));
865
+ const planMap = new Map(rawPlans.map((p) => [normalizeDependencyToken(p.id), p]));
866
+ const canonicalToId = new Map(rawPlans.map((p) => [normalizeDependencyToken(extractCanonicalPlanId(p.id)), p.id]));
847
867
  // #3897 rung 4 (ADR-3473 §8.9) — the third depends_on resolution tier.
848
868
  // Resolves a bare in-phase plan-number short form (e.g. "01") to its owning
849
869
  // plan id. In-phase only by construction (T49): the map is built from THIS
@@ -878,6 +898,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
878
898
  const waves = {};
879
899
  const incomplete = [];
880
900
  const runnable = [];
901
+ // #4628: DAG-ready view — see the per-plan emission below. Keyed by id so
902
+ // the per-plan pass can resolve each plan's dependency edges.
903
+ const resolvedDepsByPlan = new Map(haltNodes.map((n) => [n.id, n.resolvedDependsOn]));
904
+ const readyPlans = [];
881
905
  let hasCheckpoints = false;
882
906
  const warnings = [];
883
907
  // #3427 / ADR-3473 §8.5: name every dropped depends_on edge (plan AND
@@ -887,8 +911,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
887
911
  // plan with no dropped edges and a genuinely wrong `wave:` still warns
888
912
  // (N3, D6, T25).
889
913
  const plansWithUnresolvedTokens = new Set();
914
+ const unresolvedTokensByPlan = new Map();
890
915
  for (const { plan, token } of unresolved) {
891
916
  plansWithUnresolvedTokens.add(plan);
917
+ const tokens = unresolvedTokensByPlan.get(plan) ?? [];
918
+ tokens.push(formatDiagnosticToken(token));
919
+ unresolvedTokensByPlan.set(plan, tokens);
892
920
  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`);
893
921
  }
894
922
  for (const rawPlan of rawPlans) {
@@ -896,14 +924,34 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
896
924
  hasCheckpoints = true;
897
925
  }
898
926
  const blockedByIds = blockedBy.get(rawPlan.id) ?? [];
927
+ // #4628: readiness fails closed — every resolved dependency must have
928
+ // completion evidence (has_summary), and a depends_on edge that never
929
+ // resolved (#3427 — a dropped edge) carries no evidence to check. A
930
+ // single evaluation feeds both the ready_plans list and the per-plan
931
+ // `ready` / `unresolved_dependencies` fields below.
932
+ // Case-fold the dep before the planMap lookup: planMap is lowercase-keyed
933
+ // (#2237) while resolveDependencyId returns the raw id — a mixed-case id
934
+ // must not read as 'no evidence' (over-blocking a ready plan).
935
+ const missingEvidence = (resolvedDepsByPlan.get(rawPlan.id) ?? [])
936
+ .filter((dep) => planMap.get(dep.toLowerCase())?.hasSummary !== true);
937
+ if (plansWithUnresolvedTokens.has(rawPlan.id)) {
938
+ missingEvidence.push(...(unresolvedTokensByPlan.get(rawPlan.id) ?? []));
939
+ }
940
+ const isReady = blockedByIds.length === 0 && missingEvidence.length === 0;
899
941
  if (!rawPlan.hasSummary) {
900
942
  incomplete.push(rawPlan.id);
901
943
  // #2830: the runnable-only view — incomplete AND not transitively
902
944
  // blocked by a halted upstream plan. Additive alongside `incomplete`,
903
945
  // which keeps its existing "no SUMMARY yet" meaning unchanged.
946
+ // #4628: runnable says NOTHING about completion evidence — a runnable
947
+ // plan whose dependencies lack a SUMMARY is not DAG-ready. Consumers
948
+ // dispatch from `ready_plans`.
904
949
  if (blockedByIds.length === 0) {
905
950
  runnable.push(rawPlan.id);
906
951
  }
952
+ if (isReady) {
953
+ readyPlans.push(rawPlan.id);
954
+ }
907
955
  }
908
956
  const computedWave = (level.get(rawPlan.id) ?? 0) + levelOffset;
909
957
  const effectiveWave = computedWave;
@@ -945,6 +993,15 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
945
993
  halted: rawPlan.halted,
946
994
  blocked_by: blockedByIds,
947
995
  };
996
+ if (!rawPlan.hasSummary) {
997
+ // #4628: readiness is a property of INCOMPLETE plans (a summarized plan
998
+ // is filtered by has_summary before readiness is ever consulted). The
999
+ // unresolved_dependencies list names exactly which predecessors lack
1000
+ // completion evidence, for the named-skip report.
1001
+ plan['ready'] = isReady;
1002
+ if (missingEvidence.length > 0)
1003
+ plan['unresolved_dependencies'] = missingEvidence;
1004
+ }
948
1005
  plans.push(plan);
949
1006
  const waveKey = String(effectiveWave);
950
1007
  if (!waves[waveKey]) {
@@ -958,6 +1015,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
958
1015
  waves,
959
1016
  incomplete,
960
1017
  runnable,
1018
+ ready_plans: readyPlans,
961
1019
  has_checkpoints: hasCheckpoints,
962
1020
  };
963
1021
  if (planNamingWarning)
@@ -1061,12 +1119,24 @@ function assertDescriptionPreservesMilestoneScope(cwd, description, command) {
1061
1119
  * before any directory does; milestone-scoping is wrong here because a number
1062
1120
  * used under any milestone on another branch is still taken).
1063
1121
  *
1122
+ * #4225 — the horizon must track the ALLOCATION scope. When the allocation is
1123
+ * workstream-scoped (`--ws`/`GSD_WORKSTREAM`, resolved into the env before
1124
+ * dispatch), the sibling's copy of the SAME workstream is what carries that
1125
+ * scope's independent numbering; the sibling's ROOT roadmap and phases/
1126
+ * belong to a different numbering universe (docs/FEATURES.md §51 REQ-WS-01 —
1127
+ * workstream state is isolated in `.planning/workstreams/{name}/`) and must
1128
+ * not contribute. `planningDir(wt, ws)` reuses the canonical resolver, so the
1129
+ * sibling scope matches the local scope's own resolution (env workstream plus
1130
+ * env project segment) by construction; `ws === null` (no workstream active)
1131
+ * keeps the #3849 root-scope horizon byte-for-byte.
1132
+ *
1064
1133
  * Widen, never refuse: a missing `.planning/`, an unreadable sibling, a
1065
1134
  * non-git cwd, or an unavailable git binary each leave `used` untouched —
1066
1135
  * allocation then behaves exactly as it did before this horizon existed.
1067
- * Sentinels reuse the canonical `isSentinelPhaseId`; the dir pattern is the
1068
- * same one the on-disk scan uses, so decimal sub-phases (`411.1-foo`) are
1069
- * correctly not integers.
1136
+ * A sibling that simply lacks the active workstream's directory is the same
1137
+ * fail-open case: it contributes nothing. Sentinels reuse the canonical
1138
+ * `isSentinelPhaseId`; the dir pattern is the same one the on-disk scan uses,
1139
+ * so decimal sub-phases (`411.1-foo`) are correctly not integers.
1070
1140
  */
1071
1141
  function collectSiblingWorktreePhaseNums(cwd, used) {
1072
1142
  let porcelain;
@@ -1084,8 +1154,16 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
1084
1154
  catch {
1085
1155
  return; // not a git repo / git unavailable — unchanged behavior
1086
1156
  }
1157
+ // #4225: the env workstream, read once with planningDir's own discriminator
1158
+ // (`?? null` = deliberately no workstream — never re-derived per sibling).
1159
+ // A poisoned value would already have thrown at the local `planningDir(cwd)`
1160
+ // call every allocator makes before reaching this horizon; the per-sibling
1161
+ // try/catch below still keeps any resolution failure fail-open.
1162
+ const ws = process.env['GSD_WORKSTREAM'] ?? null;
1163
+ const siblingPlanningDir = (wt) => planningDir(wt, ws);
1087
1164
  const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
1088
1165
  // Same header shape the allocators scan locally (#1729 tag tolerance).
1166
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1089
1167
  const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
1090
1168
  for (const line of porcelain.split('\n')) {
1091
1169
  if (!line.startsWith('worktree '))
@@ -1094,7 +1172,7 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
1094
1172
  if (!wt || node_path_1.default.resolve(wt) === node_path_1.default.resolve(cwd))
1095
1173
  continue;
1096
1174
  try {
1097
- for (const entry of node_fs_1.default.readdirSync(node_path_1.default.join(wt, '.planning', 'phases'))) {
1175
+ for (const entry of node_fs_1.default.readdirSync(node_path_1.default.join(siblingPlanningDir(wt), 'phases'))) {
1098
1176
  const match = entry.match(dirNumPattern);
1099
1177
  if (!match)
1100
1178
  continue;
@@ -1104,10 +1182,10 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
1104
1182
  }
1105
1183
  }
1106
1184
  catch {
1107
- /* worktree has no .planning — normal, contributes nothing */
1185
+ /* worktree has no .planning (or no copy of this scope) — normal, contributes nothing */
1108
1186
  }
1109
1187
  try {
1110
- const content = node_fs_1.default.readFileSync(node_path_1.default.join(wt, '.planning', 'ROADMAP.md'), 'utf-8');
1188
+ const content = node_fs_1.default.readFileSync(node_path_1.default.join(siblingPlanningDir(wt), 'ROADMAP.md'), 'utf-8');
1111
1189
  let m;
1112
1190
  headerPattern.lastIndex = 0;
1113
1191
  while ((m = headerPattern.exec(content)) !== null) {
@@ -1117,7 +1195,7 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
1117
1195
  }
1118
1196
  }
1119
1197
  catch {
1120
- /* no roadmap in that worktree — normal, contributes nothing */
1198
+ /* no roadmap in that worktree (or scope) — normal, contributes nothing */
1121
1199
  }
1122
1200
  }
1123
1201
  }
@@ -1151,6 +1229,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
1151
1229
  // (section header, roadmap bullet, or on-disk directory) is counted:
1152
1230
  // 1) Section headers: ### Phase N: / ## Phase N: / #### Phase N:
1153
1231
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1232
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1154
1233
  const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
1155
1234
  // 2) Roadmap bullet entries: - [ ] **Phase N: ...** (all checkbox variants)
1156
1235
  // The lookahead accepts colon, decimal-dot, whitespace, bold-close asterisk,
@@ -1260,6 +1339,7 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
1260
1339
  // bullets, on-disk dirs. The bullet scan was missing here — a bullet-only
1261
1340
  // `Phase N` row was invisible to batch allocation (#3849 secondary).
1262
1341
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1342
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1263
1343
  const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
1264
1344
  const bulletPattern = /^[ \t]*-[ \t]*\[[^\]]{0,200}\][ \t]*\*{0,2}Phase[ \t]+(\d+)(?=[:.\s*]|$)/gim;
1265
1345
  let m;
@@ -1343,7 +1423,65 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
1343
1423
  // found')` above, which terminates the process and never reaches here.
1344
1424
  publishStateContract(cwd);
1345
1425
  }
1346
- function cmdPhaseInsert(cwd, afterPhase, description, raw) {
1426
+ // #4569: scans all three representations of an existing decimal sub-phase
1427
+ // under `base` — on-disk `phases/` directories, `### Phase BASE.N:` headings,
1428
+ // and `- [ ] Phase BASE.N:` roadmap SUMMARY CHECKLIST bullets. A bullet-only
1429
+ // roadmap with no heading yet and no on-disk directory yet must still be
1430
+ // seen, or an allocator can silently reallocate an already-used decimal
1431
+ // number. Shared by `cmdPhaseInsert`'s normalized-base scan and its
1432
+ // sibling-allocation parent-base scan so the two never drift apart.
1433
+ function scanExistingDecimalPhaseNumbers(phasesDir, rawContent, base) {
1434
+ const decimalSet = new Set();
1435
+ // #2245 audit: existsSync-guarded, mirroring cmdPhaseNextDecimal's identical
1436
+ // scan above — a missing phasesDir (no decimal sub-phases yet) is the
1437
+ // expected, silent case (empty decimalSet). A readdirSync failure once the
1438
+ // dir is confirmed to EXIST is a genuine anomaly; swallowing it used to let
1439
+ // `phase insert` proceed with an incomplete decimalSet and risk writing a
1440
+ // decimal phase number that collides with an existing on-disk directory
1441
+ // the scan simply never saw — surfaced loud instead, like the sibling.
1442
+ //
1443
+ // #4634 (lint-phase-enumeration-drift): routed through the canonical
1444
+ // PHYSICAL-set owner (`listAllPhaseDirs`, phase-locator.cts) instead of a
1445
+ // hand-rolled `readdirSync`. This scan — like its sibling `cmdPhaseNextDecimal`
1446
+ // and its caller `cmdPhaseInsert` (both exempted in the drift guard for the
1447
+ // same reason) — must see EVERY on-disk decimal sub-phase directory
1448
+ // regardless of the current milestone window, so `listMilestonePhaseDirs`
1449
+ // (windowed) is the wrong owner here; `includeSentinels: true` preserves this
1450
+ // function's pre-existing behavior of never sentinel-filtering (the decimal
1451
+ // regex below only ever matches `base.N`-shaped names, so sentinel inclusion
1452
+ // is a no-op either way).
1453
+ if (node_fs_1.default.existsSync(phasesDir)) {
1454
+ const { value: dirs, scope } = listAllPhaseDirs(phasesDir, { includeSentinels: true });
1455
+ if (scope === SCOPE.UNREADABLE) {
1456
+ // The dir EXISTS but could not be read (EACCES/EIO) — a genuine anomaly,
1457
+ // not the expected empty-decimalSet case above. Surfaced loud, matching
1458
+ // this function's pre-migration `readdirSync` catch: swallowing it would
1459
+ // let `phase insert` proceed with an incomplete decimalSet and collide
1460
+ // with an existing on-disk decimal directory the scan never saw.
1461
+ error(`Failed to scan phase directories for existing decimal phases: unable to read ${phasesDir}`);
1462
+ }
1463
+ const decimalPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${(0, pattern_cjs_1.escapeRegex)(base)}\\.(\\d+)`);
1464
+ for (const dir of dirs) {
1465
+ const dm = dir.match(decimalPattern);
1466
+ if (dm)
1467
+ decimalSet.add(parseInt(dm[1], 10));
1468
+ }
1469
+ }
1470
+ const rmPhasePattern = new RegExp(
1471
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1472
+ `#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
1473
+ let rmMatch;
1474
+ while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
1475
+ decimalSet.add(parseInt(rmMatch[1], 10));
1476
+ }
1477
+ const checklistDecimalPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'gi');
1478
+ let clMatch;
1479
+ while ((clMatch = checklistDecimalPattern.exec(rawContent)) !== null) {
1480
+ decimalSet.add(parseInt(clMatch[1], 10));
1481
+ }
1482
+ return decimalSet;
1483
+ }
1484
+ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested') {
1347
1485
  if (!afterPhase || !description) {
1348
1486
  error('after-phase and description required for phase insert');
1349
1487
  }
@@ -1358,9 +1496,11 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
1358
1496
  const content = extractCurrentMilestone(rawContent, cwd);
1359
1497
  const normalizedAfter = normalizePhaseName(afterPhase);
1360
1498
  const afterPhaseEscaped = phaseMarkdownRegexSource(normalizedAfter);
1499
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1361
1500
  const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
1362
1501
  const headingMatch = targetPattern.test(content);
1363
1502
  const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
1503
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1364
1504
  const anyHeadingPattern = /#{2,4}\s*Phase\s+\d/i;
1365
1505
  const roadmapHasHeadingPhases = anyHeadingPattern.test(content);
1366
1506
  const isBulletStyle = !headingMatch && bulletPattern.test(content) && !roadmapHasHeadingPhases;
@@ -1373,43 +1513,20 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
1373
1513
  }
1374
1514
  const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
1375
1515
  const normalizedBase = normalizePhaseName(afterPhase);
1376
- const decimalSet = new Set();
1377
- // #2245 audit: existsSync-guarded, mirroring cmdPhaseNextDecimal's identical
1378
- // scan above — a missing phasesDir (no decimal sub-phases yet) is the
1379
- // expected, silent case (empty decimalSet). A readdirSync failure once the
1380
- // dir is confirmed to EXIST is a genuine anomaly; swallowing it used to let
1381
- // `phase insert` proceed with an incomplete decimalSet and risk writing a
1382
- // decimal phase number that collides with an existing on-disk directory
1383
- // the scan simply never saw — surfaced loud instead, like the sibling.
1384
- if (node_fs_1.default.existsSync(phasesDir)) {
1385
- // Initialized (not just declared) so TS's definite-assignment check is
1386
- // satisfied without relying on control-flow narrowing through error()'s
1387
- // `never` return, which TS does not propagate through a destructured
1388
- // module-property function reference — error() still halts the process
1389
- // before `dirs` below is ever computed from this placeholder value.
1390
- let entries = [];
1391
- try {
1392
- entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
1393
- }
1394
- catch (e) {
1395
- const msg = e instanceof Error ? e.message : String(e);
1396
- error(`Failed to scan phase directories for existing decimal phases: ${msg}`);
1397
- }
1398
- const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
1399
- const decimalPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${(0, pattern_cjs_1.escapeRegex)(normalizedBase)}\\.(\\d+)`);
1400
- for (const dir of dirs) {
1401
- const dm = dir.match(decimalPattern);
1402
- if (dm)
1403
- decimalSet.add(parseInt(dm[1], 10));
1404
- }
1405
- }
1406
- const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
1407
- let rmMatch;
1408
- while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
1409
- decimalSet.add(parseInt(rmMatch[1], 10));
1410
- }
1516
+ const decimalSet = scanExistingDecimalPhaseNumbers(phasesDir, rawContent, normalizedBase);
1411
1517
  const nextDecimal = decimalSet.size === 0 ? 1 : Math.max(...decimalSet) + 1;
1412
- const _decimalPhase = `${normalizedBase}.${nextDecimal}`;
1518
+ let _decimalPhase = `${normalizedBase}.${nextDecimal}`;
1519
+ // #4569: sibling allocation joins afterPhase's PARENT level instead of nesting
1520
+ // one level deeper under afterPhase itself. A top-level phase (no existing
1521
+ // decimal segment) has no sibling level to join; nested is the only sensible
1522
+ // allocation, so we silently fall back for that case.
1523
+ const lastDotIndex = normalizedBase.lastIndexOf('.');
1524
+ if (allocation === 'sibling' && lastDotIndex !== -1) {
1525
+ const parentBase = normalizedBase.slice(0, lastDotIndex);
1526
+ const siblingDecimalSet = scanExistingDecimalPhaseNumbers(phasesDir, rawContent, parentBase);
1527
+ const siblingNextDecimal = siblingDecimalSet.size === 0 ? 1 : Math.max(...siblingDecimalSet) + 1;
1528
+ _decimalPhase = `${parentBase}.${siblingNextDecimal}`;
1529
+ }
1413
1530
  const insertConfig = loadConfig(cwd);
1414
1531
  const projectCode = insertConfig.project_code || '';
1415
1532
  const pfx = projectCode ? `${projectCode}-` : '';
@@ -1460,7 +1577,9 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
1460
1577
  }
1461
1578
  else {
1462
1579
  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`;
1463
- const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
1580
+ const headerPattern = new RegExp(
1581
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1582
+ `(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
1464
1583
  const headerMatch = rawContent.match(headerPattern);
1465
1584
  if (!headerMatch) {
1466
1585
  error(`Could not find Phase ${afterPhase} header`);
@@ -1782,7 +1901,9 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
1782
1901
  if (!isDecimal) {
1783
1902
  // #1729: fold an optional pre-colon ( ) tag into the suffix capture so it
1784
1903
  // is re-emitted verbatim — a tagged later phase still gets renumbered.
1785
- 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}`);
1904
+ content = content.replace(
1905
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1906
+ /(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)((?:\s*\([^)\r\n]{0,200}\))?\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
1786
1907
  content = content.replace(/(-\s*\[[ x]\]\s*.*?Phase\s+)(\d+)(\s*:|\s+)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
1787
1908
  // ORDINAL-RENUMBER — CELL EDIT (not row-deletion) — migrated onto
1788
1909
  // updateTableCell (ADR-2143 §7, sibling of the deleteTableRow scoping
@@ -2115,6 +2236,7 @@ function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
2115
2236
  if (!roadmapContent || !phaseNum)
2116
2237
  return null;
2117
2238
  const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
2239
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
2118
2240
  const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:\\s*([^\\n]+)`, 'im'));
2119
2241
  if (!heading)
2120
2242
  return null;
@@ -2776,7 +2898,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
2776
2898
  // non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
2777
2899
  // here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
2778
2900
  // which would misreport a present-but-bad marker as no marker at all.
2779
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
2901
+ const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
2780
2902
  if (availableWorkstreams.length > 0 && !resolvedWorkstream) {
2781
2903
  // #3579: getActiveWorkstream now inherits a pointer-less session's read
2782
2904
  // from the shared .planning/active-workstream marker, so reaching this
@@ -3022,7 +3144,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3022
3144
  // #2617: pass the project's runtime so the blocked-completion error below
3023
3145
  // suggests the command surface this runtime actually installs
3024
3146
  // ($gsd-… on Codex) rather than a hard-coded Claude-style string.
3025
- const verificationStatus = readVerificationStatus(phaseFullDir, { runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd) });
3147
+ const verificationStatus = readVerificationStatus(phaseFullDir, {
3148
+ runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd),
3149
+ convention: resolvePhaseIdConvention(cwd),
3150
+ });
3026
3151
  // #3057 B3: the staleness check inside readVerificationStatus can itself
3027
3152
  // fail (fs / scanPhasePlans / clock error), in which case `status` above
3028
3153
  // was routed as if nothing were stale (unchanged fail-open routing) — but
@@ -3065,7 +3190,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3065
3190
  // whole-slice `.replace()` onto the seam. Applied per single physical
3066
3191
  // line by updateBullet, so the pattern no longer needs the `m` flag
3067
3192
  // (it never sees more than one line at a time); see
3068
- // planCountBodyPattern below for the sites that were migrated onto
3193
+ // writePlansField below for the sites that were migrated onto
3069
3194
  // withPhaseSection instead.
3070
3195
  //
3071
3196
  // #2245 review Fix 6: this is behaviour-preserving for GSD-GENERATED
@@ -3118,12 +3243,105 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3118
3243
  };
3119
3244
  // ADR-2143 §4: the plan-count write is now routed through
3120
3245
  // withPhaseSection (see mutateMilestonePhase below), which hands this
3121
- // pattern ONLY phase N's own detail-section body — so the pattern no
3122
- // longer needs its own `#{2,4}\s*Phase\s+N` anchor + skip-ahead-past-
3123
- // interior-headings lookahead; the section boundary itself confines
3124
- // the match (the #2067/#2200 boundary-crossing class is now
3125
- // structurally impossible for this site rather than regex-enforced).
3126
- const planCountBodyPattern = /(\*\*Plans:\*\*\s*)[^\n]+/i;
3246
+ // seam call ONLY phase N's own detail-section body — the section
3247
+ // boundary itself confines the write (the #2067/#2200 boundary-
3248
+ // crossing class is structurally impossible for this site).
3249
+ //
3250
+ // #4906 Phase 2 (#4917/ADR-4910): migrated off the one-capture-group
3251
+ // regex that replaced to end of line, dropping any hand-written
3252
+ // trailing prose after the count (#4852) — onto the PlanningDoc
3253
+ // `boldField` write seam, whose `valueSpan`/`trailingSpan` split
3254
+ // never touches the trailing annotation.
3255
+ const writePlansField = (body) => {
3256
+ const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(body, 'ROADMAP.md');
3257
+ if (!parsed.ok) {
3258
+ preservationWarnings.push({ field: 'Plans', reason: parsed.reason });
3259
+ return body;
3260
+ }
3261
+ const fieldId = (0, planning_document_cjs_1.findField)(parsed.value, 'Plans');
3262
+ if (!fieldId) {
3263
+ // #4906 regression (#1163 parity, caught by gsd-test against
3264
+ // roadmap.cts's sibling site): a hand-edited or pre-template
3265
+ // ROADMAP.md may carry a PLAIN (non-bold) `Plans:` line rather
3266
+ // than the canonical `**Plans**:`/`**Plans:**` bold field.
3267
+ // BOLD_FIELD_RE stays bold-only (widening it would register
3268
+ // ordinary prose as a spurious field seam-wide) — this fallback
3269
+ // mirrors roadmap.cts's identical one, kept in parity per
3270
+ // Decision 2 rather than letting the two sites diverge on which
3271
+ // legacy shapes they tolerate.
3272
+ const plainMatch = body.match(/^([ \t]*)Plans:([ \t]*)([^\r\n]*)$/m);
3273
+ if (!plainMatch) {
3274
+ // No `**Plans:**`/`**Plans**:`/plain `Plans:` line in this
3275
+ // phase's section — nothing to write; not a failure (mirrors
3276
+ // the old regex's silent no-match no-op).
3277
+ return body;
3278
+ }
3279
+ const [whole, indent, spacing, plainValue] = plainMatch;
3280
+ const plainCountPrefixMatch = plainValue.match(/^(?:\d+\s*\/\s*\d+\s+plans(?:\s+(?:complete|executed))?|\d+\s+plans?)/i);
3281
+ const plainIsTemplatePlaceholder = /^\[\s*Number of plans\b[\s\S]*\]$/i.test(plainValue.trim());
3282
+ if (!plainCountPrefixMatch && !plainIsTemplatePlaceholder) {
3283
+ // Arm 3: freeform prose, TBD, a bracketed human annotation, or
3284
+ // an empty value — leave the field exactly as it was.
3285
+ return body;
3286
+ }
3287
+ const plainNewCountText = `${summaryCount}/${planCount} plans complete`;
3288
+ const plainSuffix = plainCountPrefixMatch ? plainValue.slice(plainCountPrefixMatch[0].length) : '';
3289
+ const newPlainLine = `${indent}Plans:${spacing}${plainNewCountText}${plainSuffix}`;
3290
+ const start = plainMatch.index ?? body.indexOf(whole);
3291
+ return body.slice(0, start) + newPlainLine + body.slice(start + whole.length);
3292
+ }
3293
+ // #4906 regression fix: PREFIX-match the existing value's count
3294
+ // token and re-glue whatever follows it VERBATIM — a glued-on
3295
+ // annotation with no ` — ` separator (e.g. a parenthetical like
3296
+ // `0/1 plans executed (11-16 are gap closure from VERIFICATION)`)
3297
+ // lives entirely inside `value` (`TRAILING_SEPARATOR_RE` in
3298
+ // planning-document.cts only splits on ` — `, unchanged/correct),
3299
+ // so overwriting `value` outright previously destroyed it.
3300
+ //
3301
+ // #4906 review finding (isolated adversarial pass): the prior
3302
+ // version of this migration preserved this site's OLD
3303
+ // unconditional-overwrite behavior for the no-count-prefix case,
3304
+ // which clobbers arm 3 (freeform prose / TBD / a bracketed human
3305
+ // annotation like `[Deferred pending re-scope]`) — a real
3306
+ // regression against the design doc's own Behavior table row 4,
3307
+ // not an accepted trade-off. Fixed here by adopting the SAME
3308
+ // template-placeholder / arm-3-untouched classification
3309
+ // roadmap.cts's sibling site already uses (isTemplatePlaceholder +
3310
+ // "no count prefix and not a placeholder => leave untouched"),
3311
+ // rather than letting the two migrated sites diverge on this.
3312
+ const newCountText = `${summaryCount}/${planCount} plans complete`;
3313
+ const current = (0, planning_document_cjs_1.readNode)(parsed.value, fieldId);
3314
+ if (!current.ok) {
3315
+ return body;
3316
+ }
3317
+ const currentValue = current.value;
3318
+ const countPrefixMatch = currentValue.match(/^(?:\d+\s*\/\s*\d+\s+plans(?:\s+(?:complete|executed))?|\d+\s+plans?)/i);
3319
+ const isTemplatePlaceholder = /^\[\s*Number of plans\b[\s\S]*\]$/i.test(currentValue.trim());
3320
+ if (!countPrefixMatch && !isTemplatePlaceholder) {
3321
+ // Arm 3: freeform prose, TBD, a bracketed human annotation, or an
3322
+ // empty value — leave the field exactly as it was.
3323
+ return body;
3324
+ }
3325
+ const newValueToWrite = countPrefixMatch
3326
+ ? newCountText + currentValue.slice(countPrefixMatch[0].length)
3327
+ : newCountText;
3328
+ const staged = (0, planning_document_cjs_1.setFieldValue)(parsed.value, fieldId, newValueToWrite);
3329
+ if (!staged.ok) {
3330
+ preservationWarnings.push({ field: 'Plans', reason: staged.reason });
3331
+ return body;
3332
+ }
3333
+ const out = (0, planning_document_cjs_1.serialize)(staged.value);
3334
+ if (!out.ok) {
3335
+ // `hasUnreadableNodes` refusal (ADR-4910 amendment) — a ragged
3336
+ // SIBLING node elsewhere in this same section refuses the whole
3337
+ // splice. Never throw / crash the phase-complete transaction over
3338
+ // a node unrelated to this write; surface it and leave `body`
3339
+ // unchanged, same as any other preservation warning.
3340
+ preservationWarnings.push({ field: 'Plans', reason: out.reason });
3341
+ return body;
3342
+ }
3343
+ return out.value;
3344
+ };
3127
3345
  const phaseInfoSummaries = phaseInfo['summaries'];
3128
3346
  // #2200: apply the phase-checkbox flip, the plan-count write, and the
3129
3347
  // per-plan checkbox flips ONLY within the current milestone's region(s)
@@ -3160,7 +3378,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3160
3378
  // section's body, so neither regex can escape into a sibling
3161
3379
  // phase's section, a shipped milestone, or a Backlog entry.
3162
3380
  s = withPhaseSection(s, phaseNum, (body) => {
3163
- let b = body.replace(planCountBodyPattern, `$1${summaryCount}/${planCount} plans complete`);
3381
+ let b = writePlansField(body);
3164
3382
  for (const summaryFile of phaseInfoSummaries) {
3165
3383
  const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
3166
3384
  if (!planId)
@@ -3205,14 +3423,24 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3205
3423
  if (node_fs_1.default.existsSync(reqPath)) {
3206
3424
  const phaseEsc = phaseMarkdownRegexSource(phaseNum);
3207
3425
  const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
3208
- 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'));
3426
+ const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(
3427
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
3428
+ `(#{2,4}\\s*Phase\\s+${phaseEsc}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
3209
3429
  const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
3210
- const reqMatch = sectionText.match(/\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]+)/i);
3430
+ // #4731: multiline-aware — hard-wrapped Requirements read past the
3431
+ // line break before the ID scan. The shared extractor also stops at
3432
+ // headings and table rows, so a Requirements field followed by the
3433
+ // Traceability table cannot bleed other phases' REQ-IDs into the
3434
+ // citation scan (isolated-review MEDIUM on the inline lookahead,
3435
+ // whose lazy capture swallowed everything to section end).
3436
+ const reqLine = sectionText
3437
+ ? roadmapParserMod.extractPhaseFieldMultiline(sectionText, 'Requirements')
3438
+ : null;
3211
3439
  const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
3212
3440
  let reqContent = originalReqContent;
3213
3441
  // #2316: `citedReqIds` — the REQ-IDs ROADMAP's own **Requirements:**
3214
3442
  // line for this phase actually cites — is hoisted out of the
3215
- // `if (reqMatch)` block (previously scoped only inside it) so the
3443
+ // `if (reqLine)` block (previously scoped only inside it) so the
3216
3444
  // ghost-ID cross-check below (~#2316-1) can consult it. `TBD` is the
3217
3445
  // literal placeholder `phase.add`/`-batch`/`-insert` seed
3218
3446
  // (`**Requirements**: TBD`, src/phase.cts:833,920,1078) — never a
@@ -3224,16 +3452,16 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3224
3452
  // otherwise) — the `if (reqUpdate.ok)` below previously had no
3225
3453
  // `else`, discarding this fact silently instead of surfacing it.
3226
3454
  const traceabilityWriteMisses = [];
3227
- if (reqMatch) {
3455
+ if (reqLine) {
3228
3456
  // #2334 HIGH 3 + #3697: selection and under-selection detection both
3229
3457
  // live in `analyzeRequirementsLine` (module scope, above), extracted in
3230
3458
  // round 3 so the parser is directly testable — a closure in here is
3231
3459
  // reachable only by spawning the CLI, which no fast-check property test
3232
3460
  // can do. `citedReqIds` is byte-identical to the expression that stood
3233
3461
  // here; nothing about what phase-complete MARKS has changed.
3234
- const reqLineAnalysis = analyzeRequirementsLine(reqMatch[1]);
3462
+ const reqLineAnalysis = analyzeRequirementsLine(reqLine);
3235
3463
  citedReqIds = reqLineAnalysis.citedReqIds;
3236
- const reqLineWarning = formatRequirementsLineWarning(phaseNum, reqMatch[1], reqLineAnalysis);
3464
+ const reqLineWarning = formatRequirementsLineWarning(phaseNum, reqLine, reqLineAnalysis);
3237
3465
  if (reqLineWarning) {
3238
3466
  warnings.push(reqLineWarning.message);
3239
3467
  // Carried out to the JSON result as its own field — see
@@ -3519,6 +3747,34 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3519
3747
  let diskNextName = null;
3520
3748
  let roadmapNextNum = null;
3521
3749
  let roadmapNextName = null;
3750
+ // #4699: a phase whose roadmap checkbox is `[x]` is already complete and
3751
+ // must never be selected as next_phase — out-of-order completion (a
3752
+ // reopened phase finished after later phases shipped) otherwise persists
3753
+ // the already-done phase as STATE.md current_phase. Collected from the
3754
+ // same milestone-scoped text the roadmap scan walks; membership is
3755
+ // comparePhaseNum-based so `02` and `2` dedupe. With no ROADMAP.md (or no
3756
+ // parseable rows) the set is empty and the scans behave exactly as
3757
+ // before.
3758
+ const roadmapCompleteNums = [];
3759
+ if (roadmapContent !== null) {
3760
+ try {
3761
+ const milestoneForComplete = extractCurrentMilestone(roadmapContent, cwd);
3762
+ const completePattern = new RegExp(`-\\s*\\[[xX]\\]\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'gi');
3763
+ let cm;
3764
+ while ((cm = completePattern.exec(milestoneForComplete)) !== null) {
3765
+ if (isSentinelPhaseId(cm[1]))
3766
+ continue;
3767
+ if (!roadmapCompleteNums.some((n) => comparePhaseNum(cm[1], n) === 0)) {
3768
+ roadmapCompleteNums.push(cm[1]);
3769
+ }
3770
+ }
3771
+ }
3772
+ catch {
3773
+ /* best-effort: an unreadable milestone section leaves the complete
3774
+ * set empty — the scans then behave exactly as they did pre-#4699. */
3775
+ }
3776
+ }
3777
+ const isCompletePhaseNum = (num) => roadmapCompleteNums.some((n) => comparePhaseNum(num, n) === 0);
3522
3778
  try {
3523
3779
  // #3185 (ADR-3180 Decision 1): "which phase directories belong to
3524
3780
  // the CURRENT milestone" — routed through the canonical owner
@@ -3534,6 +3790,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3534
3790
  // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
3535
3791
  if (isSentinelPhaseId(dm[1]))
3536
3792
  continue;
3793
+ // #4699: an already-complete phase (roadmap checkbox [x]) is never
3794
+ // a next_phase candidate — out-of-order completion must skip it.
3795
+ if (roadmapContent !== null && isCompletePhaseNum(dm[1]))
3796
+ continue;
3537
3797
  // Numeric MINIMUM above N, not "first encountered". `listMilestonePhaseDirs`
3538
3798
  // does sort by `comparePhaseNum`, so a `break` on the first hit happens to be
3539
3799
  // correct today — but that makes this scan's correctness depend on an
@@ -3601,6 +3861,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3601
3861
  // stage 2's heading scan must not advance into backlog headings either.
3602
3862
  if (isSentinelPhaseId(pmNum))
3603
3863
  continue;
3864
+ // #4699: skip complete phases — a `[x]` checkbox row and the
3865
+ // `## Phase Details` heading of an already-done phase both name a
3866
+ // phase that must never be next_phase.
3867
+ if (roadmapContent !== null && isCompletePhaseNum(pmNum))
3868
+ continue;
3604
3869
  // #3701 review: the numeric MINIMUM above N, not the first row above N in
3605
3870
  // DOCUMENT order. This scan walks raw roadmap text, and one global regex
3606
3871
  // sweeps both the `## Phases` checklist and the `## Phase Details`
@@ -3767,14 +4032,53 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3767
4032
  const fmBody = frontmatterMod.stripFrontmatter(stateContent);
3768
4033
  const bodyHasPhaseField = stateExtractField(fmBody, 'Current Phase') != null ||
3769
4034
  stateExtractField(fmBody, 'Phase') != null;
3770
- const authoritativeFm = nextPhaseDisplayName
3771
- ? bodyHasPhaseField || !nextPhaseNum
3772
- ? { current_phase_name: nextPhaseDisplayName }
3773
- : {
3774
- current_phase: String(nextPhaseNum),
3775
- current_phase_name: nextPhaseDisplayName,
4035
+ // #4129: the POST-completion progress counters, derived from the very
4036
+ // ROADMAP this transaction just mutated (still in memory — it hits disk
4037
+ // only at writePlanningFileSet, AFTER this content was assembled).
4038
+ // buildStateFrontmatter's disk scan inside syncAndPreserveStateMd
4039
+ // reads the PRE-completion ROADMAP (and any stale-dated sibling
4040
+ // verification), so without this intent the persisted counter failed
4041
+ // to increment on the completing phase's own transaction. Routed
4042
+ // through the #2736 authoritativeFm seam's object direction: the
4043
+ // pre-preservation merge makes it the derived truth the ratchet
4044
+ // compares, and the post-preservation re-assert (completedOnlyRaise)
4045
+ // is a floor no preservation branch can drop below. clampPercent is
4046
+ // completePhaseCore's own percent formula (state-transition.cts),
4047
+ // reused so the frontmatter and the body `Progress:` line agree.
4048
+ const postCompletionRoadmapScope = roadmapContent !== null
4049
+ ? extractCurrentMilestone(roadmapContent, cwd)
4050
+ : null;
4051
+ const postCompletionRoadmapProgress = postCompletionRoadmapScope !== null
4052
+ ? deriveProgressFromRoadmapForIntent(postCompletionRoadmapScope)
4053
+ : null;
4054
+ const authoritativeProgress = postCompletionRoadmapProgress && postCompletionRoadmapProgress.completedPhases !== null
4055
+ ? postCompletionRoadmapProgress.totalPhases !== null && postCompletionRoadmapProgress.totalPhases > 0
4056
+ ? {
4057
+ completed_phases: postCompletionRoadmapProgress.completedPhases,
4058
+ percent: clampPercentForIntent(postCompletionRoadmapProgress.completedPhases, postCompletionRoadmapProgress.totalPhases),
3776
4059
  }
4060
+ : { completed_phases: postCompletionRoadmapProgress.completedPhases }
3777
4061
  : undefined;
4062
+ const authoritativeFm = authoritativeProgress
4063
+ ? {
4064
+ ...(nextPhaseDisplayName
4065
+ ? bodyHasPhaseField || !nextPhaseNum
4066
+ ? { current_phase_name: nextPhaseDisplayName }
4067
+ : {
4068
+ current_phase: String(nextPhaseNum),
4069
+ current_phase_name: nextPhaseDisplayName,
4070
+ }
4071
+ : {}),
4072
+ progress: authoritativeProgress,
4073
+ }
4074
+ : nextPhaseDisplayName
4075
+ ? bodyHasPhaseField || !nextPhaseNum
4076
+ ? { current_phase_name: nextPhaseDisplayName }
4077
+ : {
4078
+ current_phase: String(nextPhaseNum),
4079
+ current_phase_name: nextPhaseDisplayName,
4080
+ }
4081
+ : undefined;
3778
4082
  // ADR-3408 §8.3 / #3469: this deliberately bypasses
3779
4083
  // readModifyWriteStateMd (STATE.md is committed atomically with
3780
4084
  // ROADMAP/REQUIREMENTS), so it calls the single write-seam
@@ -3953,4 +4257,5 @@ module.exports = {
3953
4257
  cmdPhaseListPlans,
3954
4258
  computeDependencyLevels,
3955
4259
  buildShortFormToId,
4260
+ normalizeDependencyToken,
3956
4261
  };