@opengsd/gsd-core 1.15.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 (487) 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/agents/gsd-code-fixer.compact.md +1 -1
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-code-reviewer.compact.md +5 -3
  7. package/agents/gsd-code-reviewer.md +8 -6
  8. package/agents/gsd-debug-session-manager.compact.md +1 -1
  9. package/agents/gsd-debug-session-manager.md +1 -1
  10. package/agents/gsd-debugger.md +2 -2
  11. package/agents/gsd-eval-auditor.compact.md +1 -1
  12. package/agents/gsd-eval-auditor.md +1 -1
  13. package/agents/gsd-executor.md +4 -4
  14. package/agents/gsd-intel-updater.compact.md +1 -1
  15. package/agents/gsd-intel-updater.md +1 -1
  16. package/agents/gsd-mempalace-curator.md +2 -2
  17. package/agents/gsd-phase-researcher.md +1 -1
  18. package/agents/gsd-plan-checker.md +7 -2
  19. package/agents/gsd-planner.md +3 -3
  20. package/agents/gsd-project-researcher.compact.md +1 -1
  21. package/agents/gsd-project-researcher.md +1 -1
  22. package/agents/gsd-research-synthesizer.compact.md +1 -1
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-auditor.compact.md +21 -30
  25. package/agents/gsd-ui-auditor.md +42 -51
  26. package/agents/gsd-ui-researcher.compact.md +1 -1
  27. package/agents/gsd-ui-researcher.md +1 -1
  28. package/agents/gsd-verifier.md +31 -9
  29. package/bin/install.js +126 -196
  30. package/commands/gsd/add-tests.md +6 -1
  31. package/commands/gsd/ai-integration-phase.md +6 -1
  32. package/commands/gsd/audit-fix.md +5 -0
  33. package/commands/gsd/audit-milestone.md +6 -1
  34. package/commands/gsd/autonomous.md +5 -0
  35. package/commands/gsd/capture.md +8 -4
  36. package/commands/gsd/code-review.md +7 -2
  37. package/commands/gsd/complete-milestone.md +4 -0
  38. package/commands/gsd/config.md +7 -3
  39. package/commands/gsd/debug.md +11 -7
  40. package/commands/gsd/discuss-phase.md +7 -3
  41. package/commands/gsd/docs-update.md +12 -7
  42. package/commands/gsd/eval-review.md +6 -1
  43. package/commands/gsd/execute-phase.md +12 -7
  44. package/commands/gsd/extract-learnings.md +5 -0
  45. package/commands/gsd/fast.md +4 -0
  46. package/commands/gsd/forensics.md +5 -1
  47. package/commands/gsd/graphify.md +10 -6
  48. package/commands/gsd/health.md +5 -0
  49. package/commands/gsd/help.md +7 -2
  50. package/commands/gsd/import.md +7 -3
  51. package/commands/gsd/inbox.md +5 -0
  52. package/commands/gsd/ingest-docs.md +5 -1
  53. package/commands/gsd/manager.md +6 -1
  54. package/commands/gsd/map-codebase.md +7 -3
  55. package/commands/gsd/mempalace-capture.md +5 -1
  56. package/commands/gsd/mempalace-recall.md +5 -1
  57. package/commands/gsd/milestone-summary.md +5 -1
  58. package/commands/gsd/mvp-phase.md +8 -3
  59. package/commands/gsd/new-milestone.md +6 -1
  60. package/commands/gsd/new-project.md +5 -0
  61. package/commands/gsd/next.md +6 -1
  62. package/commands/gsd/ns-context.md +4 -0
  63. package/commands/gsd/ns-ideate.md +4 -0
  64. package/commands/gsd/ns-manage.md +4 -0
  65. package/commands/gsd/ns-project.md +4 -0
  66. package/commands/gsd/ns-review.md +4 -0
  67. package/commands/gsd/ns-workflow.md +4 -0
  68. package/commands/gsd/onboard.md +6 -1
  69. package/commands/gsd/pause-work.md +5 -1
  70. package/commands/gsd/phase.md +8 -4
  71. package/commands/gsd/plan-phase.md +6 -1
  72. package/commands/gsd/plan-review-convergence.md +5 -1
  73. package/commands/gsd/pr-branch.md +4 -0
  74. package/commands/gsd/profile-user.md +5 -1
  75. package/commands/gsd/progress.md +6 -1
  76. package/commands/gsd/quick-batch.md +20 -8
  77. package/commands/gsd/quick.md +12 -7
  78. package/commands/gsd/review.md +5 -1
  79. package/commands/gsd/secure-phase.md +6 -1
  80. package/commands/gsd/ship.md +5 -0
  81. package/commands/gsd/sketch.md +7 -2
  82. package/commands/gsd/spec-phase.md +5 -1
  83. package/commands/gsd/spike.md +8 -3
  84. package/commands/gsd/surface.md +5 -1
  85. package/commands/gsd/thread.md +4 -0
  86. package/commands/gsd/ui-phase.md +6 -1
  87. package/commands/gsd/ui-review.md +6 -1
  88. package/commands/gsd/ultraplan-phase.md +5 -1
  89. package/commands/gsd/undo.md +5 -1
  90. package/commands/gsd/update.md +6 -2
  91. package/commands/gsd/validate-phase.md +6 -1
  92. package/commands/gsd/verify-work.md +6 -1
  93. package/commands/gsd/workspace.md +7 -3
  94. package/gsd-core/bin/gsd-tools.cjs +142 -56
  95. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  96. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  97. package/gsd-core/bin/lib/audit.cjs +78 -44
  98. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  99. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  100. package/gsd-core/bin/lib/capability-registry.cjs +183 -103
  101. package/gsd-core/bin/lib/capability-validator.cjs +16 -0
  102. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  103. package/gsd-core/bin/lib/check-command-router.cjs +164 -1712
  104. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  105. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  106. package/gsd-core/bin/lib/codex-agent-toml.cjs +11 -8
  107. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  108. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  109. package/gsd-core/bin/lib/commands.cjs +173 -170
  110. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  111. package/gsd-core/bin/lib/config.cjs +43 -12
  112. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  113. package/gsd-core/bin/lib/coverage.cjs +4 -8
  114. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  115. package/gsd-core/bin/lib/drift.cjs +177 -42
  116. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  117. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  118. package/gsd-core/bin/lib/frontmatter.cjs +413 -234
  119. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  120. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  121. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  122. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  123. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  124. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  125. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  126. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  127. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  128. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  129. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  130. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  131. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  132. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  133. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  134. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  135. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  136. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  137. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  138. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  139. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  140. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  141. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  142. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  143. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  144. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  145. package/gsd-core/bin/lib/init.cjs +162 -96
  146. package/gsd-core/bin/lib/install-engine.cjs +19 -35
  147. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  148. package/gsd-core/bin/lib/io.cjs +122 -3
  149. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  150. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  151. package/gsd-core/bin/lib/milestone.cjs +19 -1
  152. package/gsd-core/bin/lib/model-resolver.cjs +18 -18
  153. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  154. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  155. package/gsd-core/bin/lib/pattern.cjs +10 -0
  156. package/gsd-core/bin/lib/phase-command-router.cjs +11 -4
  157. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  158. package/gsd-core/bin/lib/phase-id.cjs +1 -1
  159. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  160. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  161. package/gsd-core/bin/lib/phase.cjs +280 -80
  162. package/gsd-core/bin/lib/plan-document.cjs +93 -19
  163. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  164. package/gsd-core/bin/lib/planning-document.cjs +273 -40
  165. package/gsd-core/bin/lib/planning-inspect.cjs +42 -8
  166. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  167. package/gsd-core/bin/lib/planning-workspace.cjs +76 -53
  168. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  169. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  170. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  171. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  172. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  173. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  174. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  175. package/gsd-core/bin/lib/roadmap-command-router.cjs +13 -15
  176. package/gsd-core/bin/lib/roadmap-parser.cjs +82 -15
  177. package/gsd-core/bin/lib/roadmap-upgrade.cjs +127 -65
  178. package/gsd-core/bin/lib/roadmap.cjs +169 -72
  179. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +91 -157
  180. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -1
  181. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  182. package/gsd-core/bin/lib/runtime-homes.cjs +13 -10
  183. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +307 -591
  184. package/gsd-core/bin/lib/runtime-name-policy.cjs +142 -27
  185. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  186. package/gsd-core/bin/lib/shell-command-projection.cjs +37 -2
  187. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  188. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  189. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  190. package/gsd-core/bin/lib/state-document.cjs +100 -22
  191. package/gsd-core/bin/lib/state.cjs +214 -105
  192. package/gsd-core/bin/lib/surface.cjs +2 -1
  193. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -152
  194. package/gsd-core/bin/lib/uat-predicate.cjs +313 -35
  195. package/gsd-core/bin/lib/uat.cjs +424 -7
  196. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +94 -58
  197. package/gsd-core/bin/lib/vendor/README.md +31 -9
  198. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  199. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  200. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  201. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  202. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  203. package/gsd-core/bin/lib/verification.cjs +1165 -314
  204. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  205. package/gsd-core/bin/lib/verify.cjs +286 -567
  206. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  207. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  208. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  209. package/gsd-core/bin/lib/workstream.cjs +11 -2
  210. package/gsd-core/bin/shared/config-defaults.manifest.json +5 -0
  211. package/gsd-core/bin/shared/config-schema.manifest.json +4 -0
  212. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  213. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  214. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  215. package/gsd-core/references/execute-phase-between-wave-reset.md +3 -0
  216. package/gsd-core/references/execute-phase-response-language.md +1 -1
  217. package/gsd-core/references/gsd-run-resolver.md +1 -1
  218. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  219. package/gsd-core/references/offer-next.md +1 -1
  220. package/gsd-core/references/planning-config.md +1 -1
  221. package/gsd-core/references/spidr-splitting.md +1 -1
  222. package/gsd-core/references/tdd.md +10 -4
  223. package/gsd-core/references/verifier-phase-gates.md +5 -2
  224. package/gsd-core/references/verify-mvp-mode.md +2 -2
  225. package/gsd-core/references/workstream-flag.md +33 -3
  226. package/gsd-core/templates/README.md +1 -1
  227. package/gsd-core/templates/UAT.md +17 -1
  228. package/gsd-core/templates/config.json +2 -11
  229. package/gsd-core/templates/verification-report.md +1 -1
  230. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  231. package/gsd-core/workflows/add-backlog.md +1 -1
  232. package/gsd-core/workflows/add-phase.md +8 -7
  233. package/gsd-core/workflows/add-tests.md +3 -2
  234. package/gsd-core/workflows/add-todo.md +4 -3
  235. package/gsd-core/workflows/ai-integration-phase.md +3 -2
  236. package/gsd-core/workflows/audit-fix.md +1 -1
  237. package/gsd-core/workflows/audit-milestone.md +4 -3
  238. package/gsd-core/workflows/audit-uat.md +1 -1
  239. package/gsd-core/workflows/autonomous.md +28 -18
  240. package/gsd-core/workflows/check-todos.md +6 -5
  241. package/gsd-core/workflows/cleanup.md +1 -1
  242. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  243. package/gsd-core/workflows/code-review-fix.md +5 -4
  244. package/gsd-core/workflows/code-review.md +84 -90
  245. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  246. package/gsd-core/workflows/complete-milestone.md +12 -7
  247. package/gsd-core/workflows/debug.md +30 -5
  248. package/gsd-core/workflows/diagnose-issues.md +3 -2
  249. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  250. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  252. package/gsd-core/workflows/discuss-phase.md +4 -3
  253. package/gsd-core/workflows/do.md +1 -1
  254. package/gsd-core/workflows/docs-update.md +4 -3
  255. package/gsd-core/workflows/edit-phase.md +4 -3
  256. package/gsd-core/workflows/eval-review.md +5 -3
  257. package/gsd-core/workflows/execute-phase/detail/elaboration.md +1 -1
  258. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +4 -2
  259. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  260. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  261. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  263. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  264. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  265. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  266. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  267. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  268. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  269. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  270. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  271. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  272. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  273. package/gsd-core/workflows/execute-phase.md +78 -170
  274. package/gsd-core/workflows/execute-plan.md +13 -19
  275. package/gsd-core/workflows/explore.md +2 -2
  276. package/gsd-core/workflows/extract-learnings.md +3 -2
  277. package/gsd-core/workflows/fast.md +1 -1
  278. package/gsd-core/workflows/forensics.md +1 -1
  279. package/gsd-core/workflows/graduation.md +1 -1
  280. package/gsd-core/workflows/health.md +2 -1
  281. package/gsd-core/workflows/import.md +3 -2
  282. package/gsd-core/workflows/inbox.md +1 -1
  283. package/gsd-core/workflows/ingest-docs.md +1 -1
  284. package/gsd-core/workflows/insert-phase.md +4 -3
  285. package/gsd-core/workflows/list-seeds.md +1 -1
  286. package/gsd-core/workflows/list-workspaces.md +1 -1
  287. package/gsd-core/workflows/manager.md +5 -3
  288. package/gsd-core/workflows/map-codebase.md +4 -3
  289. package/gsd-core/workflows/milestone-summary.md +3 -2
  290. package/gsd-core/workflows/mvp-phase.md +14 -14
  291. package/gsd-core/workflows/new-milestone.md +10 -10
  292. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  293. package/gsd-core/workflows/new-project.md +1 -1
  294. package/gsd-core/workflows/new-workspace.md +1 -1
  295. package/gsd-core/workflows/next.md +1 -1
  296. package/gsd-core/workflows/pause-work.md +2 -2
  297. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  298. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  299. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  300. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  301. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +1 -1
  302. package/gsd-core/workflows/plan-phase.md +27 -18
  303. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  304. package/gsd-core/workflows/plant-seed.md +1 -1
  305. package/gsd-core/workflows/pr-branch.md +1 -1
  306. package/gsd-core/workflows/profile-user.md +1 -1
  307. package/gsd-core/workflows/progress.md +19 -49
  308. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -2
  309. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  310. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  311. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  312. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  313. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  314. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  315. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  316. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  317. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  318. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  319. package/gsd-core/workflows/quick-batch.md +15 -10
  320. package/gsd-core/workflows/quick.md +42 -31
  321. package/gsd-core/workflows/remove-phase.md +3 -2
  322. package/gsd-core/workflows/remove-workspace.md +1 -1
  323. package/gsd-core/workflows/resume-project.md +3 -2
  324. package/gsd-core/workflows/review.md +3 -2
  325. package/gsd-core/workflows/scan.md +3 -2
  326. package/gsd-core/workflows/secure-phase.md +11 -12
  327. package/gsd-core/workflows/settings-advanced.md +1 -1
  328. package/gsd-core/workflows/settings-integrations.md +1 -1
  329. package/gsd-core/workflows/settings.md +1 -1
  330. package/gsd-core/workflows/ship.md +12 -12
  331. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  332. package/gsd-core/workflows/sketch.md +1 -1
  333. package/gsd-core/workflows/smart-entry.md +1 -1
  334. package/gsd-core/workflows/spec-phase.md +1 -1
  335. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  336. package/gsd-core/workflows/spike.md +1 -1
  337. package/gsd-core/workflows/stats.md +1 -1
  338. package/gsd-core/workflows/sync-skills.md +1 -1
  339. package/gsd-core/workflows/thread.md +2 -2
  340. package/gsd-core/workflows/transition.md +13 -23
  341. package/gsd-core/workflows/ui-phase.md +5 -4
  342. package/gsd-core/workflows/ui-review.md +4 -3
  343. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  344. package/gsd-core/workflows/undo.md +1 -1
  345. package/gsd-core/workflows/validate-phase.md +10 -12
  346. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  347. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  348. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  349. package/gsd-core/workflows/verify-work.md +68 -59
  350. package/hooks/dist/gsd-agent-isolation-guard.js +8 -0
  351. package/hooks/dist/gsd-check-update-worker.js +8 -0
  352. package/hooks/dist/gsd-check-update.js +8 -0
  353. package/hooks/dist/gsd-context-monitor.js +31 -8
  354. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  355. package/hooks/dist/gsd-secret-read-guard.js +147 -16
  356. package/hooks/dist/gsd-statusline.js +15 -7
  357. package/hooks/dist/gsd-update-banner.js +8 -0
  358. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  359. package/hooks/dist/gsd-workflow-guard.js +5 -4
  360. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  361. package/hooks/dist/lib/cli-exit.js +12 -0
  362. package/hooks/dist/lib/git-probe.js +17 -1
  363. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  364. package/hooks/gsd-agent-isolation-guard.js +8 -0
  365. package/hooks/gsd-check-update-worker.js +8 -0
  366. package/hooks/gsd-check-update.js +8 -0
  367. package/hooks/gsd-context-monitor.js +31 -8
  368. package/hooks/gsd-cursor-subagent-start.js +8 -0
  369. package/hooks/gsd-secret-read-guard.js +147 -16
  370. package/hooks/gsd-statusline.js +15 -7
  371. package/hooks/gsd-update-banner.js +8 -0
  372. package/hooks/gsd-windsurf-pre-write.js +11 -2
  373. package/hooks/gsd-workflow-guard.js +5 -4
  374. package/hooks/gsd-worktree-path-guard.js +6 -2
  375. package/hooks/hooks.json +5 -5
  376. package/hooks/lib/cli-exit.js +12 -0
  377. package/hooks/lib/git-probe.js +17 -1
  378. package/hooks/lib/isolation-sentinel.js +2 -2
  379. package/package.json +21 -4
  380. package/scripts/changeset/parse.cjs +52 -4
  381. package/scripts/ci-timeout-report.cjs +770 -4
  382. package/scripts/command-contract-helpers.cjs +12 -8
  383. package/scripts/docs-guard-registry.cjs +6 -0
  384. package/scripts/gen-features.cjs +13 -8
  385. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  386. package/scripts/gen-loop-host-contract.cjs +10 -1
  387. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  388. package/scripts/gen-plugin-skills.cjs +87 -1
  389. package/scripts/gen-research-agents.cjs +24 -31
  390. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  391. package/scripts/gen-test-timings.cjs +32 -7
  392. package/scripts/lib/cli-exit.cjs +12 -0
  393. package/scripts/lib/macos-conformance-tier.generated.cjs +20 -2
  394. package/scripts/lib/ndjson-reporter.cjs +28 -3
  395. package/scripts/lib/platform-conformance-tier.generated.cjs +34 -5
  396. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  397. package/scripts/lib/vendor-bundle.cjs +59 -0
  398. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  399. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  400. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  401. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  402. package/scripts/lint-descriptions.cjs +7 -3
  403. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +10 -0
  404. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  405. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  406. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  407. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  408. package/scripts/lint-phase-id-drift.cjs +29 -0
  409. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  410. package/scripts/lint-response-language-coverage.cjs +3 -0
  411. package/scripts/lint-skill-deps.cjs +7 -3
  412. package/scripts/lint-test-file-count.allowlist.json +11 -1
  413. package/scripts/lint-test-file-count.cjs +34 -1
  414. package/scripts/lint-vendored-deps.cjs +41 -5
  415. package/scripts/lint-workflow-shellcheck-baseline.json +10 -10
  416. package/scripts/mutation-matrix.cjs +50 -5
  417. package/scripts/require-issue-link-policy.cjs +6 -2
  418. package/scripts/sync-runtime-launcher.cjs +184 -2
  419. package/scripts/verify-npm-publish.cjs +76 -20
  420. package/skills/gsd-add-tests/SKILL.md +6 -1
  421. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  422. package/skills/gsd-audit-fix/SKILL.md +5 -0
  423. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  424. package/skills/gsd-autonomous/SKILL.md +5 -0
  425. package/skills/gsd-capture/SKILL.md +8 -4
  426. package/skills/gsd-code-review/SKILL.md +7 -2
  427. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  428. package/skills/gsd-config/SKILL.md +8 -4
  429. package/skills/gsd-debug/SKILL.md +11 -7
  430. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  431. package/skills/gsd-docs-update/SKILL.md +12 -7
  432. package/skills/gsd-eval-review/SKILL.md +6 -1
  433. package/skills/gsd-execute-phase/SKILL.md +12 -7
  434. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  435. package/skills/gsd-fast/SKILL.md +4 -0
  436. package/skills/gsd-forensics/SKILL.md +5 -1
  437. package/skills/gsd-graphify/SKILL.md +10 -6
  438. package/skills/gsd-health/SKILL.md +5 -0
  439. package/skills/gsd-help/SKILL.md +7 -2
  440. package/skills/gsd-import/SKILL.md +7 -3
  441. package/skills/gsd-inbox/SKILL.md +5 -0
  442. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  443. package/skills/gsd-manager/SKILL.md +6 -1
  444. package/skills/gsd-map-codebase/SKILL.md +7 -3
  445. package/skills/gsd-mempalace-capture/SKILL.md +5 -1
  446. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  447. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  448. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  449. package/skills/gsd-new-milestone/SKILL.md +6 -1
  450. package/skills/gsd-new-project/SKILL.md +5 -0
  451. package/skills/gsd-next/SKILL.md +6 -1
  452. package/skills/gsd-ns-context/SKILL.md +4 -0
  453. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  454. package/skills/gsd-ns-manage/SKILL.md +4 -0
  455. package/skills/gsd-ns-project/SKILL.md +4 -0
  456. package/skills/gsd-ns-review/SKILL.md +4 -0
  457. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  458. package/skills/gsd-onboard/SKILL.md +6 -1
  459. package/skills/gsd-pause-work/SKILL.md +5 -1
  460. package/skills/gsd-phase/SKILL.md +8 -4
  461. package/skills/gsd-plan-phase/SKILL.md +6 -1
  462. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  463. package/skills/gsd-pr-branch/SKILL.md +4 -0
  464. package/skills/gsd-profile-user/SKILL.md +5 -1
  465. package/skills/gsd-progress/SKILL.md +6 -1
  466. package/skills/gsd-quick/SKILL.md +16 -10
  467. package/skills/gsd-quick-batch/SKILL.md +20 -8
  468. package/skills/gsd-review/SKILL.md +5 -1
  469. package/skills/gsd-review-backlog/SKILL.md +3 -2
  470. package/skills/gsd-secure-phase/SKILL.md +6 -1
  471. package/skills/gsd-ship/SKILL.md +5 -0
  472. package/skills/gsd-sketch/SKILL.md +7 -2
  473. package/skills/gsd-spec-phase/SKILL.md +5 -1
  474. package/skills/gsd-spike/SKILL.md +8 -3
  475. package/skills/gsd-surface/SKILL.md +5 -1
  476. package/skills/gsd-thread/SKILL.md +4 -0
  477. package/skills/gsd-ui-phase/SKILL.md +6 -1
  478. package/skills/gsd-ui-review/SKILL.md +6 -1
  479. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  480. package/skills/gsd-undo/SKILL.md +5 -1
  481. package/skills/gsd-update/SKILL.md +6 -2
  482. package/skills/gsd-validate-phase/SKILL.md +6 -1
  483. package/skills/gsd-verify-work/SKILL.md +6 -1
  484. package/skills/gsd-workspace/SKILL.md +7 -3
  485. package/skills/gsd-workstreams/SKILL.md +6 -6
  486. package/vscode/package.json +1 -1
  487. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +0 -24
@@ -30,6 +30,7 @@ const markdown_table_cjs_1 = require("./markdown-table.cjs");
30
30
  const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
31
31
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
32
32
  const pattern_cjs_1 = require("./pattern.cjs");
33
+ const planning_document_cjs_1 = require("./planning-document.cjs");
33
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
34
35
  const planningScopeMod = require("./planning-scope.cjs");
35
36
  const { SCOPE } = planningScopeMod;
@@ -536,29 +537,106 @@ function joinFieldReplacement(prefix, newValue) {
536
537
  function stateReplaceField(content, fieldName, newValue) {
537
538
  const escaped = (0, pattern_cjs_1.escapeRegex)(fieldName);
538
539
  // Bold inline format: **FieldName:** value
539
- // The label-to-value gap is same-line whitespace only (`[ \t]*`, mirroring the
540
- // read side at stateExtractField). `\s*` here matched `\n`, so on an empty field
541
- // `(.*)` captured the following line and the rebuild discarded it — the #4010
542
- // data-loss. ADR-3180 §7.7 makes stateExtractField the same-line-confined owner;
543
- // this aligns the writer to it.
544
540
  //
545
- // #4243: the bold form is also ANCHORED to line start, with same-line leading
546
- // whitespace only. The pre-fix pattern carried no `^` and no `m` flag, so a
547
- // bold label quoted MID-SENTENCE inside prose — an Accumulated Context bullet
548
- // mentioning `**Status:**` — captured the rewrite and destroyed the rest of
549
- // its line, silently, whenever a whole-body caller fed this function every
550
- // section (beginPhaseCore's tryField, advancePlanCore's Status/Current Plan
551
- // writes). The plain branch below was always line-anchored; only the bold
552
- // branch lagged. Anchoring reuses #4010's same-line confinement idiom (the
553
- // leading class is `[ \t]*`, deliberately NOT the `\s*` the issue suggested —
554
- // `^\s*\*\*` can consume the newlines before the label into the match and
555
- // drop them on rebuild) and #4186's recognition-by-anchoring discipline: a
556
- // write target must BE the whole declared line shape, never a substring
557
- // guess inside prose. `$` is explicit-and-inert (`.` never crosses line
558
- // terminators) and documents that the match ends at end-of-line.
559
- const boldPattern = new RegExp(`^([ \\t]*\\*\\*${escaped}:\\*\\*[ \\t]*)(.*)$`, 'im');
560
- if (boldPattern.test(content)) {
561
- return content.replace(boldPattern, (_match, prefix) => joinFieldReplacement(prefix, newValue));
541
+ // #5007 (Phase 6 / ADR-4910 amendment): migrated off the hand-rolled
542
+ // `^([ \t]*\*\*${escaped}:\*\*[ \t]*)(.*)$` regex (which, per #4010/#4243
543
+ // below, already existed to fix real same-line-confinement and mid-prose
544
+ // data-loss bugs) onto `parsePlanningDoc` for LOCATING the field — same-
545
+ // line confinement AND fence/frontmatter exclusion (parseBoldFieldLine
546
+ // never matches inside a fenced block or frontmatter, which the removed
547
+ // regex could not tell apart from prose) — so this migration keeps
548
+ // #4010/#4243's fix and extends it, rather than replacing it with a
549
+ // weaker check.
550
+ //
551
+ // This does NOT go through the seam's `setFieldValue`/`serialize` write
552
+ // path. An earlier version of this migration did, via a since-removed
553
+ // `setFieldValue({ allowSeparator: true })` option (#5007) — that option
554
+ // spliced the caller's value across the FULL rest-of-line span
555
+ // (`valueSpan.start`..`trailingSpan.end`) to permit a value containing the
556
+ // grammar's ` — ` trailing-separator token (needed here: see the
557
+ // `${currentPhase} — COMPLETE` value below). It was removed after a
558
+ // failing-first reproduction proved it only avoided the write-time
559
+ // refusal: the bytes it writes are correct, but `parseBoldFieldLine`
560
+ // splits on ` — ` unconditionally on every read, with no escaping
561
+ // convention in this grammar to tell "atomic value containing the token"
562
+ // apart from "value plus hand-annotation". So the NEXT fresh
563
+ // `parsePlanningDoc` of that exact text — not the in-memory doc the
564
+ // option's own tests checked — silently re-truncates the value and
565
+ // demotes the rest to `trailingSpan`, with `findField`/`readNode`
566
+ // reporting a confident, wrong `ok: true` and no error. That is exactly
567
+ // the #4917 finding-2 corruption `setFieldValue`'s round-trip check
568
+ // exists to prevent, just moved one parse cycle downstream. This call
569
+ // site never reads STATE.md fields back through `parsePlanningDoc`/
570
+ // `findField` (reads go through `stateExtractField`'s own non-splitting
571
+ // regex, below), so it is safe HERE — but making that a shared, public
572
+ // option on the seam's `setFieldValue` was an attractive nuisance for any
573
+ // future `findField`/`readNode` caller (this same module already serves
574
+ // ROADMAP.md's `Plans`/`Depends on` fields that way). The full-rest-of-
575
+ // line splice is done locally, directly against `content`, instead —
576
+ // `parsePlanningDoc` is used only to locate the field's spans.
577
+ //
578
+ // Two deliberate deviations from a bare findField() call, both
579
+ // PRESERVING this function's own prior contract rather than adopting the
580
+ // seam's stricter defaults:
581
+ //
582
+ // 1. Label lookup here is case-INSENSITIVE (not findField's exact
583
+ // match), mirroring the removed regex's `i` flag: callers in
584
+ // state.cts/state-transition.cts pass a fieldName spelling that can
585
+ // differ only in case from what a given template actually has (the
586
+ // explicit 'Last Activity' / 'Last activity' fallback-call pairs at
587
+ // those call sites), so a case-sensitive lookup would silently miss
588
+ // matches the removed regex used to find.
589
+ // 2. Every write below replaces the FULL rest-of-line span
590
+ // (`valueSpan.start`..`trailingSpan.end`) UNCONDITIONALLY, not only
591
+ // when `newValue` contains the grammar's ` — ` separator token,
592
+ // because `joinFieldReplacement` always discarded the OLD regex's
593
+ // entire captured tail (`(.*)$` — both what the seam calls
594
+ // `valueSpan` AND `trailingSpan`) and replaced it wholesale with the
595
+ // new value. This call site never had a "preserve a hand-written
596
+ // trailing annotation" contract, so writing anything narrower than
597
+ // the full rest-of-line span here would be a NEW, untested behavior
598
+ // this migration must not introduce as a side effect.
599
+ //
600
+ // Matching BOTH bold-placement spellings (`**Label:**` and `**Label**:`,
601
+ // `BOLD_FIELD_RE`) where the removed regex recognized only `**Label:**`
602
+ // is the same, already-precedented widening as the "Depends on"
603
+ // migration (src/phase.cts) — intentional, not scope creep.
604
+ //
605
+ // A parse failure, an absent/case-mismatched label, or a value containing
606
+ // a line break (\r/\n — would forge sibling structure on splice, the same
607
+ // hazard `setFieldValue` refuses unconditionally at #4917/ADR-4910
608
+ // Decision 2 & 4; this local splice has no seam call to inherit that
609
+ // refusal from, so it is re-checked here) falls through to the
610
+ // plain/pipe-table branches below unchanged — the same silent
611
+ // per-occurrence no-op-on-no-match contract the removed regex had for any
612
+ // input it didn't match.
613
+ const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(content, 'STATE.md');
614
+ if (parsed.ok) {
615
+ let boldField = null;
616
+ for (const node of parsed.value.nodes) {
617
+ if (node.kind !== 'boldField')
618
+ continue;
619
+ if (node.label.toLowerCase() !== fieldName.toLowerCase())
620
+ continue;
621
+ boldField = node;
622
+ break;
623
+ }
624
+ if (boldField) {
625
+ // Replicates joinFieldReplacement's own `` `${newValue}` `` coercion
626
+ // (a caller may pass a non-string, e.g. a number, at the JS boundary
627
+ // even though the type signature says `string`) before the
628
+ // missing-separator normalization below: insert a single space when
629
+ // the existing label-to-value gap has none, so `**Status:**value`
630
+ // still becomes `**Status:** value` rather than gluing the two
631
+ // together.
632
+ const spacingText = parsed.value.source.slice(boldField.labelSpan.end, boldField.valueSpan.start);
633
+ const newValueStr = `${newValue}`;
634
+ const needsSeparator = newValueStr.length > 0 && !/[ \t]$/.test(spacingText);
635
+ const writeValue = `${needsSeparator ? ' ' : ''}${newValueStr}`;
636
+ if (!/[\r\n]/.test(writeValue)) {
637
+ return (content.slice(0, boldField.valueSpan.start) + writeValue + content.slice(boldField.trailingSpan.end));
638
+ }
639
+ }
562
640
  }
563
641
  // Plain line-start format: FieldName: value (same same-line confinement as above)
564
642
  const plainPattern = new RegExp(`(^${escaped}:[ \\t]*)(.*)`, 'im');
@@ -32,7 +32,7 @@ bracketMilestoneIntroSrcFor, } = phaseIdMod;
32
32
  // eslint-disable-next-line @typescript-eslint/no-require-imports
33
33
  const roadmapParserMod = require("./roadmap-parser.cjs");
34
34
  // #3642: hasMilestoneSectioning no longer consumed here — its >=2 semantics answered sibling conflation, but this branch asks asserted-vs-section (>=1). It stays exported from roadmap-parser.cjs for its unit pins.
35
- const { getMilestoneInfo, extractCurrentMilestone, isMilestoneBoundedInRoadmap, hasAnyMilestoneSection } = roadmapParserMod;
35
+ const { getMilestoneInfo, extractCurrentMilestone, isMilestoneBoundedInRoadmap, hasAnyMilestoneSection, classifyMilestoneScalar } = roadmapParserMod;
36
36
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
37
37
  // eslint-disable-next-line @typescript-eslint/no-require-imports
38
38
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -60,7 +60,7 @@ const coreUtilsMod = require("./core-utils.cjs");
60
60
  const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
61
61
  // eslint-disable-next-line @typescript-eslint/no-require-imports
62
62
  const verificationMod = require("./verification.cjs");
63
- const { isPhaseComplete } = verificationMod;
63
+ const { isPhaseComplete, VerificationStatusError } = verificationMod;
64
64
  // #4129: the single owner of "count the ROADMAP's milestone Complete rows"
65
65
  // (phase-lifecycle.cts) — reused for the completed-phases numerator floor so
66
66
  // this scan cannot grow a second ROADMAP parser. Pure computation module (no
@@ -1080,7 +1080,7 @@ function computeUpdateProgressPreview(statePath, cwd) {
1080
1080
  const existingFm = extractFrontmatter(preContent, statePath);
1081
1081
  const preBody = stripFrontmatter(preContent);
1082
1082
  const storedMilestone = typeof existingFm['milestone'] === 'string' ? existingFm['milestone'] : null;
1083
- const builtFm = buildStateFrontmatter(preBody, cwd, storedMilestone, readStoredTotalPhases(existingFm), readStoredCompletedPhases(existingFm), readStoredTotalPlans(existingFm), readStoredCompletedPlans(existingFm));
1083
+ const builtFm = buildStateFrontmatter(preBody, cwd, storedMilestone, readStoredTotalPhases(existingFm), readStoredCompletedPhases(existingFm), readStoredTotalPlans(existingFm), readStoredCompletedPlans(existingFm), isMilestoneExplicitlyNull(existingFm));
1084
1084
  const progress = builtFm['progress'];
1085
1085
  const percent = progress && typeof progress['percent'] === 'number' ? progress['percent'] : null;
1086
1086
  const completedPlans = progress && typeof progress['completed_plans'] === 'number' ? progress['completed_plans'] : null;
@@ -2392,6 +2392,112 @@ function countRoadmapPhaseHeadings(scope, convention, retiredPhaseNums, applyCon
2392
2392
  }
2393
2393
  return count;
2394
2394
  }
2395
+ /**
2396
+ * The phase directories every STATE.md frontmatter rebuild scans, and the
2397
+ * ROADMAP facts derived alongside them — the ONE owner of that set (#5118).
2398
+ *
2399
+ * `phaseDirs` is milestone-scoped (`listMilestonePhaseDirs`), drops retired
2400
+ * phases (#1514) and holds exactly one directory per normalized phase key
2401
+ * (Bug #2445, deterministic tie-break #3355). `buildStateFrontmatter` counts
2402
+ * from it, and `statePhaseDirsToScan` hands it to `phase remove`'s pre-write
2403
+ * validation, so the two cannot disagree about which reports a rebuild reads.
2404
+ */
2405
+ function scanStatePhaseDirs(cwd, phasesDir, phaseConvention, storedMilestone,
2406
+ // `quiet` suppresses ONLY the duplicate-phase-key stderr warning. A pre-write
2407
+ // validation pass that is followed by the real rebuild scan sets it, so the
2408
+ // one command prints the warning once (the rebuild scan owns the warning).
2409
+ options = {}) {
2410
+ // Read the current-milestone ROADMAP scope once: it feeds both the
2411
+ // heading-based phase count and the retired/folded-phase
2412
+ // exclusion (#1514). Computed before the disk scan so retired phases
2413
+ // can be dropped from the dir set too.
2414
+ let roadmapScope = null;
2415
+ let roadmapRaw = null;
2416
+ let retiredPhaseNums = new Set();
2417
+ try {
2418
+ const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
2419
+ roadmapRaw = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
2420
+ if (roadmapRaw !== null) {
2421
+ roadmapScope = extractCurrentMilestone(roadmapRaw, cwd);
2422
+ retiredPhaseNums = extractRetiredPhaseNumbers(roadmapScope, phaseConvention);
2423
+ }
2424
+ }
2425
+ catch { /* fall through: no roadmap scope → no retired exclusion */ }
2426
+ // #3017: scope the milestone filter to the STORED milestone when available,
2427
+ // so a state.* write doesn't auto-derive (and mis-bind) to a different
2428
+ // milestone's heading and clobber the stored value + progress counts.
2429
+ // #3185 (ADR-3180 Decision 1): "which phase directories belong to the
2430
+ // CURRENT (stored) milestone" — routed through the canonical owner
2431
+ // instead of a hand-rolled readdirSync + isDirInMilestone filter
2432
+ // (which also never excluded sentinels, unlike the owner).
2433
+ const { value: allMatchingDirs, scope: phaseDirScope } = listMilestonePhaseDirs(phasesDir, {
2434
+ cwd,
2435
+ versionOverride: storedMilestone ?? null,
2436
+ phaseIdConvention: phaseConvention,
2437
+ });
2438
+ // Bug #2445: when stale phase dirs from a prior milestone remain in
2439
+ // .planning/phases/ alongside new dirs with the same phase number,
2440
+ // de-duplicate by normalized phase number keeping exactly one dir
2441
+ // per key (deterministic tie-break: see #3355 below). This prevents
2442
+ // double-counting (e.g. two "Phase 1" dirs).
2443
+ const seenPhaseNums = new Map(); // normalizedNum -> dirName
2444
+ for (const dir of allMatchingDirs) {
2445
+ // #1514: a retired/folded phase keeps a directory but no completion
2446
+ // artifact; drop it from the disk phase set so it counts toward
2447
+ // neither the denominator nor the numerator (mirrors the heading
2448
+ // exclusion below). Project-code-aware via phaseKeyFromDir.
2449
+ if (retiredPhaseNums.size > 0 && retiredPhaseNums.has(phaseKeyFromDir(dir, phaseConvention)))
2450
+ continue;
2451
+ // #3185: dedup grouping routed through the canonical phaseKeyFromDir
2452
+ // (src/phase-id.cts) instead of a local leading-digits regex that
2453
+ // diverged from extractPhaseToken/phaseKeyFromDir on
2454
+ // project-code-prefixed dirs (whole dirname fell through as the key,
2455
+ // so a `PROJ-05`/`PROJ-05-slug` pair never deduped) and on
2456
+ // multi-segment milestone dirs. Same key surface used two lines
2457
+ // above for the retiredPhaseNums exclusion, so both filters agree.
2458
+ const key = phaseKeyFromDir(dir, phaseConvention);
2459
+ if (!seenPhaseNums.has(key)) {
2460
+ seenPhaseNums.set(key, dir);
2461
+ }
2462
+ else {
2463
+ // #3355: the survivor of a same-milestone collision must be
2464
+ // chosen from repository CONTENT, never from filesystem state.
2465
+ // The pre-#3355 tie-break was `mtimeMs` — a checkout-order
2466
+ // signal — so two byte-identical checkouts of the same commit
2467
+ // that wrote the colliding dirs in a different order picked
2468
+ // different survivors, and progress.total_plans /
2469
+ // completed_plans drifted across clones and CI runs. The
2470
+ // directory NAME is git-tracked content and a total order, so
2471
+ // the lexicographically-first dir wins deterministically. The
2472
+ // collision is still a project-level defect (duplicate phase
2473
+ // number in scope), so it is surfaced on stderr instead of
2474
+ // being silently resolved. The Bug #2445 invariant — exactly
2475
+ // one survivor per normalized phase number — is unchanged.
2476
+ const incumbent = seenPhaseNums.get(key);
2477
+ const survivor = dir < incumbent ? dir : incumbent;
2478
+ seenPhaseNums.set(key, survivor);
2479
+ if (options.quiet !== true)
2480
+ process.stderr.write(`gsd: warning — phase directories '${incumbent}' and '${dir}' both normalize to phase key '${key}' (duplicate phase number in .planning/phases/); keeping '${survivor}' by deterministic lexicographic order. (#3355)\n`);
2481
+ }
2482
+ }
2483
+ return {
2484
+ phaseDirs: [...seenPhaseNums.values()],
2485
+ phaseDirScope,
2486
+ roadmapScope,
2487
+ roadmapRaw,
2488
+ retiredPhaseNums,
2489
+ };
2490
+ }
2491
+ /**
2492
+ * #5038: does existing frontmatter assert an explicit null milestone, as
2493
+ * opposed to omitting the `milestone:` key? `extractFrontmatter` parses under
2494
+ * FAILSAFE_SCHEMA, so YAML null arrives as a string; classification is
2495
+ * delegated to classifyMilestoneScalar.
2496
+ */
2497
+ function isMilestoneExplicitlyNull(existingFm) {
2498
+ const raw = existingFm['milestone'];
2499
+ return typeof raw === 'string' && classifyMilestoneScalar(raw).explicitNull;
2500
+ }
2395
2501
  /**
2396
2502
  * Extract machine-readable fields from STATE.md markdown body and build
2397
2503
  * a YAML frontmatter object. Allows hooks and scripts to read state
@@ -2403,7 +2509,10 @@ function buildStateFrontmatter(bodyContent, cwd, storedMilestone, storedTotalPha
2403
2509
  // #3354/#3573 withhold condition the disk scan returns null for all four
2404
2510
  // counters, and these stored values are what the progress block falls back
2405
2511
  // to (else the keys are omitted).
2406
- storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
2512
+ storedCompletedPhases, storedTotalPlans, storedCompletedPlans,
2513
+ // #5038: explicit `milestone: null` (vs an absent key); withholds progress
2514
+ // counters on a sectioned ROADMAP without the unbound warning.
2515
+ milestoneExplicitlyNull) {
2407
2516
  // #2956: scope `Phase` extraction to ## Current Position (mirrors the read
2408
2517
  // path in cmdStateSnapshot and the Stopped At / Paused At ## Session scoping
2409
2518
  // below). Phase canonically lives in ## Current Position (templates/state.md);
@@ -2505,79 +2614,12 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
2505
2614
  // on repeated buildStateFrontmatter invocations within the same process (#1967)
2506
2615
  let cached = _diskScanCache.get(cwd);
2507
2616
  if (!cached) {
2508
- // Read the current-milestone ROADMAP scope once: it feeds both the
2509
- // heading-based phase count below and the retired/folded-phase
2510
- // exclusion (#1514). Computed before the disk scan so retired phases
2511
- // can be dropped from the dir set too.
2512
- let roadmapScope = null;
2513
- let roadmapRaw = null;
2514
- let retiredPhaseNums = new Set();
2515
- try {
2516
- const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
2517
- roadmapRaw = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
2518
- if (roadmapRaw !== null) {
2519
- roadmapScope = extractCurrentMilestone(roadmapRaw, cwd);
2520
- retiredPhaseNums = extractRetiredPhaseNumbers(roadmapScope, phaseConvention);
2521
- }
2522
- }
2523
- catch { /* fall through: no roadmap scope → no retired exclusion */ }
2524
- // #3017: scope the milestone filter to the STORED milestone when available,
2525
- // so a state.* write doesn't auto-derive (and mis-bind) to a different
2526
- // milestone's heading and clobber the stored value + progress counts.
2527
- // #3185 (ADR-3180 Decision 1): "which phase directories belong to the
2528
- // CURRENT (stored) milestone" — routed through the canonical owner
2529
- // instead of a hand-rolled readdirSync + isDirInMilestone filter
2530
- // (which also never excluded sentinels, unlike the owner).
2531
- const { value: allMatchingDirs, scope: phaseDirScope } = listMilestonePhaseDirs(phasesDir, {
2532
- cwd,
2533
- versionOverride: storedMilestone ?? null,
2534
- phaseIdConvention: phaseConvention,
2535
- });
2536
- // Bug #2445: when stale phase dirs from a prior milestone remain in
2537
- // .planning/phases/ alongside new dirs with the same phase number,
2538
- // de-duplicate by normalized phase number keeping exactly one dir
2539
- // per key (deterministic tie-break: see #3355 below). This prevents
2540
- // double-counting (e.g. two "Phase 1" dirs).
2541
- const seenPhaseNums = new Map(); // normalizedNum -> dirName
2542
- for (const dir of allMatchingDirs) {
2543
- // #1514: a retired/folded phase keeps a directory but no completion
2544
- // artifact; drop it from the disk phase set so it counts toward
2545
- // neither the denominator nor the numerator (mirrors the heading
2546
- // exclusion below). Project-code-aware via phaseKeyFromDir.
2547
- if (retiredPhaseNums.size > 0 && retiredPhaseNums.has(phaseKeyFromDir(dir, phaseConvention)))
2548
- continue;
2549
- // #3185: dedup grouping routed through the canonical phaseKeyFromDir
2550
- // (src/phase-id.cts) instead of a local leading-digits regex that
2551
- // diverged from extractPhaseToken/phaseKeyFromDir on
2552
- // project-code-prefixed dirs (whole dirname fell through as the key,
2553
- // so a `PROJ-05`/`PROJ-05-slug` pair never deduped) and on
2554
- // multi-segment milestone dirs. Same key surface used two lines
2555
- // above for the retiredPhaseNums exclusion, so both filters agree.
2556
- const key = phaseKeyFromDir(dir, phaseConvention);
2557
- if (!seenPhaseNums.has(key)) {
2558
- seenPhaseNums.set(key, dir);
2559
- }
2560
- else {
2561
- // #3355: the survivor of a same-milestone collision must be
2562
- // chosen from repository CONTENT, never from filesystem state.
2563
- // The pre-#3355 tie-break was `mtimeMs` — a checkout-order
2564
- // signal — so two byte-identical checkouts of the same commit
2565
- // that wrote the colliding dirs in a different order picked
2566
- // different survivors, and progress.total_plans /
2567
- // completed_plans drifted across clones and CI runs. The
2568
- // directory NAME is git-tracked content and a total order, so
2569
- // the lexicographically-first dir wins deterministically. The
2570
- // collision is still a project-level defect (duplicate phase
2571
- // number in scope), so it is surfaced on stderr instead of
2572
- // being silently resolved. The Bug #2445 invariant — exactly
2573
- // one survivor per normalized phase number — is unchanged.
2574
- const incumbent = seenPhaseNums.get(key);
2575
- const survivor = dir < incumbent ? dir : incumbent;
2576
- seenPhaseNums.set(key, survivor);
2577
- process.stderr.write(`gsd: warning — phase directories '${incumbent}' and '${dir}' both normalize to phase key '${key}' (duplicate phase number in .planning/phases/); keeping '${survivor}' by deterministic lexicographic order. (#3355)\n`);
2578
- }
2579
- }
2580
- const phaseDirs = [...seenPhaseNums.values()];
2617
+ // The ROADMAP scope + the deduped, milestone-scoped disk phase set:
2618
+ // one owner (scanStatePhaseDirs, above buildStateFrontmatter) so the
2619
+ // #5118 pre-write validation in `phase remove` reads exactly this set.
2620
+ // #5038: an explicit-null `storedMilestone` is text, not a version;
2621
+ // pass a real null so it never reaches the phase-dir version filter.
2622
+ const { phaseDirs, phaseDirScope, roadmapScope, roadmapRaw, retiredPhaseNums, } = scanStatePhaseDirs(cwd, phasesDir, phaseConvention, milestoneExplicitlyNull ? null : storedMilestone);
2581
2623
  let diskTotalPlans = 0;
2582
2624
  let diskTotalSummaries = 0;
2583
2625
  let diskCompletedPhases = 0;
@@ -2597,7 +2639,15 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
2597
2639
  // #612: `phaseConvention` threaded so a bracket phase dir resolves
2598
2640
  // and scopes its verification report like its legacy twin — the
2599
2641
  // read-side half of the same thread cmdStateSync gets below.
2600
- if (isPhaseComplete(phaseDir, { convention: phaseConvention }).value.complete)
2642
+ const completion = isPhaseComplete(phaseDir, { convention: phaseConvention });
2643
+ // #5118 (no write before the error): every STATE.md write rebuilds
2644
+ // this frontmatter BEFORE its write, so a report whose `status` is
2645
+ // outside the closed set fails the write here — the owner's error,
2646
+ // rethrown past the best-effort catch below, nothing persisted and
2647
+ // nothing cached.
2648
+ if (completion.value.statusError)
2649
+ throw completion.value.statusError;
2650
+ if (completion.value.complete)
2601
2651
  diskCompletedPhases++;
2602
2652
  }
2603
2653
  // Count phase headings from ROADMAP — single source of truth for
@@ -2618,6 +2668,21 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
2618
2668
  // phase-dir count only, and mark unbounded so percent is skipped
2619
2669
  // downstream (mirrors the sync write-path guard).
2620
2670
  let milestoneBounded = true;
2671
+ // #2828/#3642: distinguish a FLAT unmilestoned roadmap (no milestone
2672
+ // sectioning at all — only Phase headings) from a MILESTONED one
2673
+ // (milestone/version headings exist). Computed before the
2674
+ // boundedness gate so the explicit-null branch can use it (#5038).
2675
+ // #3642: the flat test uses the >=1 sibling (hasAnyMilestoneSection),
2676
+ // not the >=2 predicate. >=2 under-answers the question this branch
2677
+ // asks: with EXACTLY ONE milestone section and an asserted milestone
2678
+ // absent from the ROADMAP, >=2 read "flat" and the whole-document
2679
+ // count — which IS that single section's phases — was written as the
2680
+ // asserted milestone's total, silently clobbering the stored value.
2681
+ // The >=2 threshold governs SIBLING conflation; asserted-vs-section
2682
+ // needs only one section to go wrong. Zero sections (genuinely flat)
2683
+ // keeps the whole-document count, per #2828.
2684
+ const roadmapHasAnyMilestoneSection = roadmapRaw !== null
2685
+ && hasAnyMilestoneSection(roadmapRaw);
2621
2686
  // #3216 fix (#1761 regression): use `assertedMilestoneVersion` —
2622
2687
  // the version STATE.md actually asserts — not the scope-gated
2623
2688
  // `milestone`. `milestone` is null on any non-COMPLETE identity
@@ -2635,27 +2700,19 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
2635
2700
  // defect, design row 17).
2636
2701
  milestoneBounded = isMilestoneBounded(roadmapRaw, String(assertedMilestoneVersion).trim(), phaseConvention);
2637
2702
  }
2638
- // #2828: distinguish a FLAT unmilestoned roadmap (no milestone sectioning
2639
- // at all — only Phase headings) from a MILESTONED-but-unbounded one
2640
- // (milestone/version headings exist but the asserted one isn't among them).
2641
- // On a flat roadmap the whole-doc count is correct (no sibling milestones to
2642
- // conflate); on a sectioned-but-unbounded one it conflates siblings (#1761),
2643
- // so fall back to phaseDirs.length.
2703
+ else if (milestoneExplicitlyNull && roadmapRaw !== null && roadmapHasAnyMilestoneSection) {
2704
+ // #5038: explicit null on a sectioned ROADMAP: no section owns
2705
+ // the counts, so treat as unbound for the withhold gate (the
2706
+ // warning below is skipped, null is deliberate).
2707
+ milestoneBounded = false;
2708
+ }
2709
+ // #2828: on a flat roadmap the whole-doc count is correct (no
2710
+ // sibling milestones to conflate); on a sectioned-but-unbounded one
2711
+ // it conflates siblings (#1761), so fall back to phaseDirs.length.
2644
2712
  // #3184: routed through the single owner (roadmap-parser.cjs) —
2645
2713
  // deliberately weaker than isMilestoneBoundedInRoadmap above (no
2646
2714
  // version-token requirement); see hasMilestoneSectioning's own
2647
2715
  // doc comment for why that distinction is load-bearing.
2648
- // #3642: the flat test uses the >=1 sibling (hasAnyMilestoneSection),
2649
- // not the >=2 predicate. >=2 under-answers the question this branch
2650
- // asks: with EXACTLY ONE milestone section and an asserted milestone
2651
- // absent from the ROADMAP, >=2 read "flat" and the whole-document
2652
- // count — which IS that single section's phases — was written as the
2653
- // asserted milestone's total, silently clobbering the stored value.
2654
- // The >=2 threshold governs SIBLING conflation; asserted-vs-section
2655
- // needs only one section to go wrong. Zero sections (genuinely flat)
2656
- // keeps the whole-document count, per #2828.
2657
- const roadmapHasAnyMilestoneSection = roadmapRaw !== null
2658
- && hasAnyMilestoneSection(roadmapRaw);
2659
2716
  const safeToUseRoadmapCount = milestoneBounded
2660
2717
  || (roadmapPhaseCount > 0 && !roadmapHasAnyMilestoneSection);
2661
2718
  // #3354: the milestoned-but-unbounded sibling of the #2828/#3204
@@ -2673,7 +2730,8 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
2673
2730
  // The degenerate un-sectioned zero-heading case keeps the
2674
2731
  // phaseDirs.length fallback — with nothing declared anywhere else,
2675
2732
  // the disk count is the only source and remains correct.
2676
- const milestonedButUnbounded = !milestoneBounded && roadmapHasAnyMilestoneSection;
2733
+ // #5038: explicit null is withheld silently (explicitNullWithheld).
2734
+ const milestonedButUnbounded = !milestoneBounded && roadmapHasAnyMilestoneSection && !milestoneExplicitlyNull;
2677
2735
  if (milestonedButUnbounded) {
2678
2736
  process.stderr.write(`gsd: warning — milestone '${String(assertedMilestoneVersion ?? '').trim()}' is asserted in STATE.md but matches no ROADMAP heading, and the ROADMAP carries milestone section(s) — one (#3642) or several (#3354) — none matching it; the whole-document count would attribute a foreign section's phases to this milestone and the on-disk phase-directory count would understate the declared total, so the progress counters (total_phases, completed_phases, total_plans, completed_plans) are left at their stored values. (#3354/#3642/#4094)\n`);
2679
2737
  }
@@ -2688,9 +2746,11 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
2688
2746
  // value instead. Without an asserted milestone (fresh project,
2689
2747
  // pre-roadmap) the disk count is still the only source and stays
2690
2748
  // authoritative (the #3354 doctrine's degenerate case).
2749
+ // #5038: an explicit null is not an asserted milestone.
2691
2750
  const roadmapAbsentWithAssertedMilestone = roadmapRaw === null &&
2692
2751
  typeof storedMilestone === 'string' &&
2693
- storedMilestone.trim() !== '';
2752
+ storedMilestone.trim() !== '' &&
2753
+ !milestoneExplicitlyNull;
2694
2754
  if (roadmapAbsentWithAssertedMilestone) {
2695
2755
  process.stderr.write(`gsd: warning — milestone '${storedMilestone.trim()}' is asserted in STATE.md but ROADMAP.md is absent or unreadable, so the phase-heading total cannot be derived; the on-disk phase-directory count would understate the declared total, so the progress counters (total_phases, completed_phases, total_plans, completed_plans) are left at their stored values. (#3573) (#4094)\n`);
2696
2756
  }
@@ -2703,7 +2763,9 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
2703
2763
  // Pre-#4094 only totalPhases was nulled here, so every resyncing
2704
2764
  // write silently clobbered the three stored siblings with the
2705
2765
  // under-scoped disk numbers.
2706
- const diskCountsWithheld = milestonedButUnbounded || roadmapAbsentWithAssertedMilestone;
2766
+ // #5038: silent sibling of milestonedButUnbounded.
2767
+ const explicitNullWithheld = milestoneExplicitlyNull && !milestoneBounded && roadmapHasAnyMilestoneSection;
2768
+ const diskCountsWithheld = milestonedButUnbounded || roadmapAbsentWithAssertedMilestone || explicitNullWithheld;
2707
2769
  // #4129: floor the completed-phases numerator at the ROADMAP's own
2708
2770
  // milestone Complete-row count. The disk numerator counts ONLY
2709
2771
  // phase dirs whose *-VERIFICATION.md routes `passed` (isPhaseComplete,
@@ -2796,9 +2858,13 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
2796
2858
  * crash `state show`; on failure this simply keeps whatever
2797
2859
  * frontmatter-derived totals/completedPhases/etc. were already set
2798
2860
  * above, a graceful degrade rather than a corrupted write (nothing is
2799
- * persisted from this block). */
2861
+ * persisted from this block). #5118: an out-of-set verification
2862
+ * report status is not a read failure to degrade over — rethrown. */
2863
+ }
2864
+ catch (err) {
2865
+ if (err instanceof VerificationStatusError)
2866
+ throw err;
2800
2867
  }
2801
- catch { /* intentionally empty */ }
2802
2868
  }
2803
2869
  // Derive percent from disk counts when available (ground truth).
2804
2870
  // Uses min(plan_fraction, phase_fraction) via computeProgressPercent so that
@@ -3215,7 +3281,7 @@ function syncStateFrontmatter(content, cwd, authoritativeFm, sanctionedPermanent
3215
3281
  // milestoned-but-unbounded withhold can preserve it across the write
3216
3282
  // (the derived progress sub-block replaces the stored one wholesale below,
3217
3283
  // so an omitted key would otherwise DELETE the stored value).
3218
- const derivedFm = buildStateFrontmatter(body, cwd, storedMilestone, readStoredTotalPhases(existingFm), readStoredCompletedPhases(existingFm), readStoredTotalPlans(existingFm), readStoredCompletedPlans(existingFm));
3284
+ const derivedFm = buildStateFrontmatter(body, cwd, storedMilestone, readStoredTotalPhases(existingFm), readStoredCompletedPhases(existingFm), readStoredTotalPlans(existingFm), readStoredCompletedPlans(existingFm), isMilestoneExplicitlyNull(existingFm));
3219
3285
  // Preserve existing frontmatter status when body-derived status is 'unknown'.
3220
3286
  // This prevents a missing Status: field in the body from overwriting a
3221
3287
  // previously valid status (e.g., 'executing' → 'unknown').
@@ -4567,7 +4633,7 @@ function cmdStateJson(cwd, raw) {
4567
4633
  // reports the phase-directory count while the persisted file preserves the
4568
4634
  // stored total, exactly the write/read divergence #3354 closed for its shape.
4569
4635
  const storedMilestoneJson = typeof existingFm['milestone'] === 'string' ? existingFm['milestone'] : null;
4570
- const built = buildStateFrontmatter(body, cwd, storedMilestoneJson, readStoredTotalPhases(existingFm), readStoredCompletedPhases(existingFm), readStoredTotalPlans(existingFm), readStoredCompletedPlans(existingFm));
4636
+ const built = buildStateFrontmatter(body, cwd, storedMilestoneJson, readStoredTotalPhases(existingFm), readStoredCompletedPhases(existingFm), readStoredTotalPlans(existingFm), readStoredCompletedPlans(existingFm), isMilestoneExplicitlyNull(existingFm));
4571
4637
  // ADR-3408 §8.5 / D3: route stopped_at / paused_at / status / current_phase /
4572
4638
  // current_phase_name / current_plan through the SAME `preserve-when-unchanged`
4573
4639
  // executor the write path uses (`applyPreserveWhenUnchanged`), instead of a
@@ -5563,7 +5629,13 @@ function cmdStateSync(cwd, options, raw) {
5563
5629
  // #612: `syncConvention` threaded — the write-side half of
5564
5630
  // buildStateFrontmatter's thread above, so `state sync` and `state json`
5565
5631
  // keep agreeing on completed_phases under the bracket convention.
5566
- if (isPhaseComplete(dirPath, { convention: syncConvention }).value.complete)
5632
+ // #5118 (no write before the error): `state sync` writes STATE.md only
5633
+ // after this scan, so an out-of-set report status fails it here, having
5634
+ // written nothing.
5635
+ const completion = isPhaseComplete(dirPath, { convention: syncConvention });
5636
+ if (completion.value.statusError)
5637
+ throw completion.value.statusError;
5638
+ if (completion.value.complete)
5567
5639
  diskCompletedPhases++;
5568
5640
  // Track the highest phase with incomplete plans (or any plans)
5569
5641
  const phaseMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
@@ -5610,8 +5682,10 @@ function cmdStateSync(cwd, options, raw) {
5610
5682
  // set — leave Progress untouched (percent=null) rather than silently writing
5611
5683
  // fallback-derived wrong values. Projects without a milestone version (the common
5612
5684
  // sync-test shape) are unaffected: the gate only fires when a version is asserted.
5613
- const fmVersion = extractFrontmatter(content, statePath).milestone;
5614
- const versionStr = typeof fmVersion === 'string' && fmVersion.trim() ? fmVersion.trim() : null;
5685
+ const syncFm = extractFrontmatter(content, statePath);
5686
+ const fmVersion = syncFm.milestone;
5687
+ const milestoneExplicitlyNull = isMilestoneExplicitlyNull(syncFm);
5688
+ const versionStr = typeof fmVersion === 'string' && fmVersion.trim() && !milestoneExplicitlyNull ? fmVersion.trim() : null;
5615
5689
  let milestoneBounded = true;
5616
5690
  if (versionStr !== null && syncRoadmapRaw !== null) {
5617
5691
  // #3184: routed through the single owner (roadmap-parser.cjs) instead of
@@ -5620,6 +5694,10 @@ function cmdStateSync(cwd, options, raw) {
5620
5694
  // extension on top inside isMilestoneBounded.
5621
5695
  milestoneBounded = isMilestoneBounded(syncRoadmapRaw, versionStr, syncConvention);
5622
5696
  }
5697
+ else if (milestoneExplicitlyNull && syncRoadmapRaw !== null && hasAnyMilestoneSection(syncRoadmapRaw)) {
5698
+ // #5038: mirror buildStateFrontmatter's silent explicit-null withhold.
5699
+ milestoneBounded = false;
5700
+ }
5623
5701
  let percent = null;
5624
5702
  if (!milestoneBounded) {
5625
5703
  changes.push(`Progress: skipped — milestone ${versionStr} cannot be bounded to a versioned ROADMAP phase set (#1761)`);
@@ -6169,6 +6247,35 @@ function cmdStateCompletePhase(cwd, raw, overridePhase) {
6169
6247
  if (wrote)
6170
6248
  publishStateContract(cwd);
6171
6249
  }
6250
+ /**
6251
+ * #5118 (no write before the error): throws `VerificationStatusError` when a
6252
+ * verification report in the set every STATE.md write rebuilds its frontmatter
6253
+ * from carries a `status` outside the closed set — and returns having done
6254
+ * nothing else. It runs `buildStateFrontmatter` itself (the SAME disk scan,
6255
+ * milestone scoping and dedupe every write performs), so a caller that writes
6256
+ * OTHER files before STATE.md (`phase complete`, `phase remove`, `quick
6257
+ * archive`, `validate health --repair`) refuses for exactly the phases the
6258
+ * STATE.md write would refuse for — the set cannot drift from a second copy.
6259
+ */
6260
+ function assertVerificationReportsReadable(bodyContent, cwd) {
6261
+ buildStateFrontmatter(bodyContent, cwd);
6262
+ }
6263
+ /**
6264
+ * The phase directory NAMES (under `.planning/phases/`) a STATE.md frontmatter
6265
+ * rebuild scans — milestone-scoped, retired phases dropped, one per phase key
6266
+ * (`scanStatePhaseDirs`). For a caller that must validate the reports a rebuild
6267
+ * WILL read after it has removed a directory of its own (`phase remove`): it
6268
+ * takes this set and drops the directory it is about to delete (#5118).
6269
+ */
6270
+ function statePhaseDirsToScan(cwd) {
6271
+ const phasesDir = planningPaths(cwd).phases;
6272
+ if (!node_fs_1.default.existsSync(phasesDir))
6273
+ return [];
6274
+ // quiet: this is a pre-write validation pass, always followed by the real
6275
+ // rebuild scan (buildStateFrontmatter), which owns the duplicate-phase-key
6276
+ // stderr warning — so the command prints it once, not twice.
6277
+ return scanStatePhaseDirs(cwd, phasesDir, resolvePhaseIdConvention(cwd), undefined, { quiet: true }).phaseDirs;
6278
+ }
6172
6279
  module.exports = {
6173
6280
  stateExtractField: state_document_cjs_1.stateExtractField,
6174
6281
  stateReplaceField: state_document_cjs_1.stateReplaceField,
@@ -6178,6 +6285,8 @@ module.exports = {
6178
6285
  writeStateMd,
6179
6286
  readModifyWriteStateMd,
6180
6287
  syncStateFrontmatter,
6288
+ assertVerificationReportsReadable,
6289
+ statePhaseDirsToScan,
6181
6290
  // #3374: the shared post-sync preservation pass (snapshots + table-driven
6182
6291
  // applyStatePreservation + #2736 re-assert).
6183
6292
  applyPostSyncPreservation,
@@ -35,6 +35,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
35
35
  const node_os_1 = __importDefault(require("node:os"));
36
36
  const node_path_1 = __importDefault(require("node:path"));
37
37
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
38
+ const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
38
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
39
40
  const installProfiles = require("./install-profiles.cjs");
40
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -351,7 +352,7 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry,
351
352
  // then projects the defaulted value to the boolean `_computePathPrefix`'s
352
353
  // existing `isGlobal: boolean` API requires.
353
354
  const _isGlobal = (0, install_scope_cjs_1.isGlobalScope)(layout.scope ?? 'global');
354
- const _isOpencode = layout.runtime === 'opencode';
355
+ const _isOpencode = (0, runtime_name_policy_cjs_1.hostBehaviorsFor)(layout.runtime).opencodePathPrefix === true;
355
356
  const _isWindowsHost = (opts?.platform ?? process.platform) === 'win32';
356
357
  // #4377: style is an install-time fact, not a process environment setting.
357
358
  // A later gsd-tools surface apply runs in another process, so it must reuse