@opengsd/gsd-core 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (551) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +85 -5
  4. package/README.ja-JP.md +3 -3
  5. package/README.ko-KR.md +3 -3
  6. package/README.pt-BR.md +3 -3
  7. package/README.zh-CN.md +3 -3
  8. package/agents/gsd-code-fixer.compact.md +7 -6
  9. package/agents/gsd-code-fixer.md +9 -8
  10. package/agents/gsd-code-reviewer.compact.md +5 -3
  11. package/agents/gsd-code-reviewer.md +8 -6
  12. package/agents/gsd-debug-session-manager.compact.md +17 -2
  13. package/agents/gsd-debug-session-manager.md +17 -2
  14. package/agents/gsd-debugger.md +3 -3
  15. package/agents/gsd-eval-auditor.compact.md +1 -1
  16. package/agents/gsd-eval-auditor.md +1 -1
  17. package/agents/gsd-executor.md +17 -12
  18. package/agents/gsd-intel-updater.compact.md +1 -1
  19. package/agents/gsd-intel-updater.md +1 -1
  20. package/agents/gsd-mempalace-curator.md +2 -2
  21. package/agents/gsd-phase-researcher.md +19 -11
  22. package/agents/gsd-plan-checker.md +15 -9
  23. package/agents/gsd-planner.md +15 -11
  24. package/agents/gsd-project-researcher.compact.md +1 -1
  25. package/agents/gsd-project-researcher.md +1 -1
  26. package/agents/gsd-research-synthesizer.compact.md +1 -1
  27. package/agents/gsd-research-synthesizer.md +1 -1
  28. package/agents/gsd-ui-auditor.compact.md +21 -30
  29. package/agents/gsd-ui-auditor.md +166 -37
  30. package/agents/gsd-ui-researcher.compact.md +1 -1
  31. package/agents/gsd-ui-researcher.md +1 -1
  32. package/agents/gsd-verifier.md +37 -14
  33. package/bin/install.js +764 -287
  34. package/commands/gsd/add-tests.md +6 -1
  35. package/commands/gsd/ai-integration-phase.md +6 -1
  36. package/commands/gsd/audit-fix.md +5 -0
  37. package/commands/gsd/audit-milestone.md +6 -1
  38. package/commands/gsd/autonomous.md +7 -2
  39. package/commands/gsd/capture.md +9 -5
  40. package/commands/gsd/code-review.md +7 -2
  41. package/commands/gsd/complete-milestone.md +4 -0
  42. package/commands/gsd/config.md +7 -3
  43. package/commands/gsd/debug.md +11 -7
  44. package/commands/gsd/discuss-phase.md +7 -3
  45. package/commands/gsd/docs-update.md +12 -7
  46. package/commands/gsd/eval-review.md +6 -1
  47. package/commands/gsd/execute-phase.md +12 -7
  48. package/commands/gsd/extract-learnings.md +5 -0
  49. package/commands/gsd/fast.md +4 -0
  50. package/commands/gsd/forensics.md +5 -1
  51. package/commands/gsd/graphify.md +10 -6
  52. package/commands/gsd/health.md +5 -0
  53. package/commands/gsd/help.md +7 -2
  54. package/commands/gsd/import.md +7 -3
  55. package/commands/gsd/inbox.md +5 -0
  56. package/commands/gsd/ingest-docs.md +5 -1
  57. package/commands/gsd/manager.md +6 -1
  58. package/commands/gsd/map-codebase.md +7 -3
  59. package/commands/gsd/mempalace-capture.md +12 -4
  60. package/commands/gsd/mempalace-recall.md +5 -1
  61. package/commands/gsd/milestone-summary.md +5 -1
  62. package/commands/gsd/mvp-phase.md +8 -3
  63. package/commands/gsd/new-milestone.md +6 -1
  64. package/commands/gsd/new-project.md +5 -0
  65. package/commands/gsd/next.md +6 -1
  66. package/commands/gsd/ns-context.md +4 -0
  67. package/commands/gsd/ns-ideate.md +4 -0
  68. package/commands/gsd/ns-manage.md +4 -0
  69. package/commands/gsd/ns-project.md +4 -0
  70. package/commands/gsd/ns-review.md +4 -0
  71. package/commands/gsd/ns-workflow.md +4 -0
  72. package/commands/gsd/onboard.md +6 -1
  73. package/commands/gsd/pause-work.md +5 -1
  74. package/commands/gsd/phase.md +8 -4
  75. package/commands/gsd/plan-phase.md +6 -1
  76. package/commands/gsd/plan-review-convergence.md +11 -7
  77. package/commands/gsd/pr-branch.md +4 -0
  78. package/commands/gsd/profile-user.md +5 -1
  79. package/commands/gsd/progress.md +7 -2
  80. package/commands/gsd/quick-batch.md +21 -9
  81. package/commands/gsd/quick.md +12 -7
  82. package/commands/gsd/review.md +7 -4
  83. package/commands/gsd/secure-phase.md +6 -1
  84. package/commands/gsd/ship.md +5 -0
  85. package/commands/gsd/sketch.md +7 -2
  86. package/commands/gsd/spec-phase.md +5 -1
  87. package/commands/gsd/spike.md +8 -3
  88. package/commands/gsd/surface.md +5 -1
  89. package/commands/gsd/thread.md +4 -0
  90. package/commands/gsd/ui-phase.md +6 -1
  91. package/commands/gsd/ui-review.md +6 -1
  92. package/commands/gsd/ultraplan-phase.md +5 -1
  93. package/commands/gsd/undo.md +5 -1
  94. package/commands/gsd/update.md +6 -2
  95. package/commands/gsd/validate-phase.md +6 -1
  96. package/commands/gsd/verify-work.md +6 -1
  97. package/commands/gsd/workspace.md +7 -3
  98. package/gsd-core/bin/gsd-tools.cjs +477 -78
  99. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  100. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  101. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  102. package/gsd-core/bin/lib/audit.cjs +144 -42
  103. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  104. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  105. package/gsd-core/bin/lib/capability-registry.cjs +197 -222
  106. package/gsd-core/bin/lib/capability-validator.cjs +16 -1
  107. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  108. package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
  109. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  110. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  111. package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
  112. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  113. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  114. package/gsd-core/bin/lib/commands.cjs +343 -207
  115. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  116. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  117. package/gsd-core/bin/lib/config.cjs +76 -19
  118. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  119. package/gsd-core/bin/lib/coverage.cjs +4 -8
  120. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  121. package/gsd-core/bin/lib/decisions.cjs +30 -14
  122. package/gsd-core/bin/lib/drift.cjs +177 -42
  123. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  124. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  125. package/gsd-core/bin/lib/frontmatter.cjs +426 -234
  126. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  127. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  128. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  129. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  130. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  131. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  132. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  133. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  134. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  135. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  136. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  137. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  138. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  139. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  140. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  141. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  142. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  143. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  144. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  145. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  146. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  147. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  148. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  149. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  150. package/gsd-core/bin/lib/graphify.cjs +10 -2
  151. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  152. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  153. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  154. package/gsd-core/bin/lib/init.cjs +364 -132
  155. package/gsd-core/bin/lib/install-engine.cjs +30 -33
  156. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  157. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  158. package/gsd-core/bin/lib/io.cjs +122 -3
  159. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  160. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  161. package/gsd-core/bin/lib/milestone.cjs +37 -6
  162. package/gsd-core/bin/lib/model-resolver.cjs +171 -62
  163. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  164. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  165. package/gsd-core/bin/lib/pattern.cjs +10 -0
  166. package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
  167. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  168. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  169. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  170. package/gsd-core/bin/lib/phase-id.cjs +110 -8
  171. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  172. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  173. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  174. package/gsd-core/bin/lib/phase.cjs +489 -88
  175. package/gsd-core/bin/lib/plan-document.cjs +142 -20
  176. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  177. package/gsd-core/bin/lib/planning-document.cjs +692 -0
  178. package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
  179. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  180. package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
  181. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  182. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  183. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  184. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  185. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  186. package/gsd-core/bin/lib/project-root.cjs +41 -2
  187. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  188. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  189. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  190. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  191. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  192. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  193. package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
  194. package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
  195. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
  196. package/gsd-core/bin/lib/roadmap.cjs +405 -88
  197. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
  198. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
  199. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  200. package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
  201. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
  202. package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
  203. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  204. package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
  205. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  206. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  207. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  208. package/gsd-core/bin/lib/state-document.cjs +100 -22
  209. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  210. package/gsd-core/bin/lib/state.cjs +256 -105
  211. package/gsd-core/bin/lib/surface.cjs +19 -2
  212. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
  213. package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
  214. package/gsd-core/bin/lib/uat.cjs +432 -7
  215. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  216. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
  217. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  218. package/gsd-core/bin/lib/vendor/README.md +31 -9
  219. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  220. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  221. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  222. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  223. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  224. package/gsd-core/bin/lib/verification.cjs +1378 -274
  225. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  226. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  227. package/gsd-core/bin/lib/verify.cjs +357 -531
  228. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  229. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  230. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  231. package/gsd-core/bin/lib/workstream.cjs +11 -2
  232. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  233. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  234. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
  235. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  236. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  237. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  238. package/gsd-core/references/checkpoints.md +5 -3
  239. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  240. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  241. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  242. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  243. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  244. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  245. package/gsd-core/references/edge-probe.md +195 -21
  246. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  247. package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
  248. package/gsd-core/references/execute-phase-response-language.md +1 -1
  249. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  250. package/gsd-core/references/gsd-run-resolver.md +1 -1
  251. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  252. package/gsd-core/references/model-profiles.md +1 -1
  253. package/gsd-core/references/offer-next.md +1 -1
  254. package/gsd-core/references/phase-argument-parsing.md +9 -7
  255. package/gsd-core/references/phase-id-convention.md +28 -0
  256. package/gsd-core/references/planner-gap-closure.md +2 -0
  257. package/gsd-core/references/planner-load-graph-context.md +24 -13
  258. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  259. package/gsd-core/references/planning-config.md +12 -3
  260. package/gsd-core/references/spidr-splitting.md +1 -1
  261. package/gsd-core/references/tdd.md +37 -8
  262. package/gsd-core/references/ui-consideration-probe.md +10 -5
  263. package/gsd-core/references/verifier-phase-gates.md +5 -2
  264. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  265. package/gsd-core/references/verify-mvp-mode.md +2 -2
  266. package/gsd-core/references/workstream-flag.md +33 -3
  267. package/gsd-core/references/worktree-path-safety.md +321 -0
  268. package/gsd-core/templates/README.md +1 -1
  269. package/gsd-core/templates/UAT.md +17 -1
  270. package/gsd-core/templates/config.json +2 -11
  271. package/gsd-core/templates/verification-report.md +1 -1
  272. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  273. package/gsd-core/workflows/add-backlog.md +1 -1
  274. package/gsd-core/workflows/add-phase.md +8 -7
  275. package/gsd-core/workflows/add-tests.md +4 -3
  276. package/gsd-core/workflows/add-todo.md +6 -5
  277. package/gsd-core/workflows/ai-integration-phase.md +13 -4
  278. package/gsd-core/workflows/audit-fix.md +1 -1
  279. package/gsd-core/workflows/audit-milestone.md +4 -3
  280. package/gsd-core/workflows/audit-uat.md +1 -1
  281. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  282. package/gsd-core/workflows/autonomous.md +43 -23
  283. package/gsd-core/workflows/check-todos.md +7 -6
  284. package/gsd-core/workflows/cleanup.md +2 -2
  285. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  286. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  287. package/gsd-core/workflows/code-review-fix.md +112 -25
  288. package/gsd-core/workflows/code-review.md +146 -135
  289. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  290. package/gsd-core/workflows/complete-milestone.md +13 -8
  291. package/gsd-core/workflows/debug.md +32 -7
  292. package/gsd-core/workflows/diagnose-issues.md +3 -2
  293. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  294. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  295. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  296. package/gsd-core/workflows/discuss-phase.md +4 -3
  297. package/gsd-core/workflows/do.md +2 -2
  298. package/gsd-core/workflows/docs-update.md +6 -5
  299. package/gsd-core/workflows/edit-phase.md +4 -3
  300. package/gsd-core/workflows/eval-review.md +14 -5
  301. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  302. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
  303. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  304. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  305. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  306. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  307. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  308. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  309. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  310. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  311. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  312. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  313. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  314. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  315. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  316. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  317. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  318. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  319. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  320. package/gsd-core/workflows/execute-phase.md +96 -178
  321. package/gsd-core/workflows/execute-plan.md +18 -23
  322. package/gsd-core/workflows/explore.md +4 -4
  323. package/gsd-core/workflows/extract-learnings.md +4 -2
  324. package/gsd-core/workflows/fast.md +1 -1
  325. package/gsd-core/workflows/forensics.md +1 -1
  326. package/gsd-core/workflows/graduation.md +1 -1
  327. package/gsd-core/workflows/health.md +3 -2
  328. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  329. package/gsd-core/workflows/help/modes/full.md +5 -5
  330. package/gsd-core/workflows/help/modes/topic.md +15 -5
  331. package/gsd-core/workflows/import.md +4 -3
  332. package/gsd-core/workflows/inbox.md +2 -2
  333. package/gsd-core/workflows/ingest-docs.md +3 -3
  334. package/gsd-core/workflows/insert-phase.md +4 -3
  335. package/gsd-core/workflows/list-seeds.md +1 -1
  336. package/gsd-core/workflows/list-workspaces.md +1 -1
  337. package/gsd-core/workflows/manager.md +6 -4
  338. package/gsd-core/workflows/map-codebase.md +5 -4
  339. package/gsd-core/workflows/milestone-summary.md +3 -2
  340. package/gsd-core/workflows/mvp-phase.md +14 -14
  341. package/gsd-core/workflows/new-milestone.md +11 -11
  342. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  343. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  344. package/gsd-core/workflows/new-project.md +7 -7
  345. package/gsd-core/workflows/new-workspace.md +2 -2
  346. package/gsd-core/workflows/next.md +1 -1
  347. package/gsd-core/workflows/note.md +1 -1
  348. package/gsd-core/workflows/onboard.md +1 -1
  349. package/gsd-core/workflows/pause-work.md +2 -2
  350. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  351. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  352. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  353. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  354. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  355. package/gsd-core/workflows/plan-phase.md +50 -24
  356. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  357. package/gsd-core/workflows/plant-seed.md +62 -20
  358. package/gsd-core/workflows/pr-branch.md +113 -13
  359. package/gsd-core/workflows/profile-user.md +2 -2
  360. package/gsd-core/workflows/progress.md +19 -49
  361. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
  362. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  363. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  364. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  365. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  366. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  367. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  368. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  369. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  370. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  371. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  372. package/gsd-core/workflows/quick-batch.md +15 -10
  373. package/gsd-core/workflows/quick.md +62 -39
  374. package/gsd-core/workflows/reapply-patches.md +9 -3
  375. package/gsd-core/workflows/remove-phase.md +3 -2
  376. package/gsd-core/workflows/remove-workspace.md +2 -2
  377. package/gsd-core/workflows/resume-project.md +3 -2
  378. package/gsd-core/workflows/review.md +33 -17
  379. package/gsd-core/workflows/scan.md +3 -2
  380. package/gsd-core/workflows/secure-phase.md +13 -13
  381. package/gsd-core/workflows/settings-advanced.md +30 -10
  382. package/gsd-core/workflows/settings-integrations.md +2 -3
  383. package/gsd-core/workflows/settings.md +4 -4
  384. package/gsd-core/workflows/ship.md +14 -13
  385. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  386. package/gsd-core/workflows/sketch.md +1 -1
  387. package/gsd-core/workflows/smart-entry.md +2 -2
  388. package/gsd-core/workflows/spec-phase.md +15 -5
  389. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  390. package/gsd-core/workflows/spike.md +1 -1
  391. package/gsd-core/workflows/stats.md +1 -1
  392. package/gsd-core/workflows/sync-skills.md +5 -5
  393. package/gsd-core/workflows/thread.md +2 -2
  394. package/gsd-core/workflows/transition.md +13 -23
  395. package/gsd-core/workflows/ui-phase.md +48 -11
  396. package/gsd-core/workflows/ui-review.md +21 -6
  397. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  398. package/gsd-core/workflows/undo.md +339 -20
  399. package/gsd-core/workflows/update.md +7 -7
  400. package/gsd-core/workflows/validate-phase.md +12 -13
  401. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  402. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  403. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  404. package/gsd-core/workflows/verify-work.md +129 -55
  405. package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
  406. package/hooks/dist/gsd-check-update-worker.js +8 -0
  407. package/hooks/dist/gsd-check-update.js +8 -0
  408. package/hooks/dist/gsd-context-monitor.js +31 -8
  409. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  410. package/hooks/dist/gsd-secret-read-guard.js +161 -4
  411. package/hooks/dist/gsd-statusline.js +85 -20
  412. package/hooks/dist/gsd-update-banner.js +8 -0
  413. package/hooks/dist/gsd-validate-commit.sh +63 -4
  414. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  415. package/hooks/dist/gsd-workflow-guard.js +5 -4
  416. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  417. package/hooks/dist/lib/cli-exit.js +12 -0
  418. package/hooks/dist/lib/git-probe.js +17 -1
  419. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  420. package/hooks/gsd-agent-isolation-guard.js +32 -0
  421. package/hooks/gsd-check-update-worker.js +8 -0
  422. package/hooks/gsd-check-update.js +8 -0
  423. package/hooks/gsd-context-monitor.js +31 -8
  424. package/hooks/gsd-cursor-subagent-start.js +8 -0
  425. package/hooks/gsd-secret-read-guard.js +161 -4
  426. package/hooks/gsd-statusline.js +85 -20
  427. package/hooks/gsd-update-banner.js +8 -0
  428. package/hooks/gsd-validate-commit.sh +63 -4
  429. package/hooks/gsd-windsurf-pre-write.js +11 -2
  430. package/hooks/gsd-workflow-guard.js +5 -4
  431. package/hooks/gsd-worktree-path-guard.js +6 -2
  432. package/hooks/hooks.json +5 -5
  433. package/hooks/lib/cli-exit.js +12 -0
  434. package/hooks/lib/git-probe.js +17 -1
  435. package/hooks/lib/isolation-sentinel.js +2 -2
  436. package/package.json +22 -4
  437. package/scripts/build-hooks.js +15 -6
  438. package/scripts/changeset/parse.cjs +52 -4
  439. package/scripts/check-contract-drift.cjs +127 -11
  440. package/scripts/ci-timeout-report.cjs +770 -4
  441. package/scripts/command-contract-helpers.cjs +15 -8
  442. package/scripts/docs-guard-registry.cjs +34 -0
  443. package/scripts/gen-features.cjs +13 -8
  444. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  445. package/scripts/gen-loop-host-contract.cjs +79 -1
  446. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  447. package/scripts/gen-plugin-skills.cjs +87 -1
  448. package/scripts/gen-research-agents.cjs +24 -31
  449. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  450. package/scripts/gen-test-timings.cjs +32 -7
  451. package/scripts/lib/cli-exit.cjs +12 -0
  452. package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
  453. package/scripts/lib/ndjson-reporter.cjs +31 -5
  454. package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
  455. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  456. package/scripts/lib/vendor-bundle.cjs +59 -0
  457. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  458. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  459. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  460. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  461. package/scripts/lint-descriptions.cjs +7 -3
  462. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
  463. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  464. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  465. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  466. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  467. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  468. package/scripts/lint-phase-id-drift.cjs +319 -5
  469. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  470. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  471. package/scripts/lint-response-language-coverage.cjs +3 -0
  472. package/scripts/lint-retired-runtime-name.cjs +619 -0
  473. package/scripts/lint-skill-deps.cjs +7 -3
  474. package/scripts/lint-state-write-path-drift.cjs +93 -0
  475. package/scripts/lint-test-file-count.allowlist.json +38 -9
  476. package/scripts/lint-test-file-count.cjs +34 -1
  477. package/scripts/lint-vendored-deps.cjs +41 -5
  478. package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
  479. package/scripts/mutation-matrix.cjs +50 -5
  480. package/scripts/prompt-injection-scan.sh +4 -0
  481. package/scripts/release-tarball-smoke.cjs +194 -1
  482. package/scripts/require-issue-link-policy.cjs +6 -2
  483. package/scripts/sync-runtime-launcher.cjs +184 -2
  484. package/scripts/verify-npm-publish.cjs +76 -20
  485. package/skills/gsd-add-tests/SKILL.md +6 -1
  486. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  487. package/skills/gsd-audit-fix/SKILL.md +5 -0
  488. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  489. package/skills/gsd-autonomous/SKILL.md +7 -2
  490. package/skills/gsd-capture/SKILL.md +9 -5
  491. package/skills/gsd-code-review/SKILL.md +7 -2
  492. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  493. package/skills/gsd-config/SKILL.md +8 -4
  494. package/skills/gsd-debug/SKILL.md +11 -7
  495. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  496. package/skills/gsd-docs-update/SKILL.md +12 -7
  497. package/skills/gsd-eval-review/SKILL.md +6 -1
  498. package/skills/gsd-execute-phase/SKILL.md +12 -7
  499. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  500. package/skills/gsd-fast/SKILL.md +4 -0
  501. package/skills/gsd-forensics/SKILL.md +5 -1
  502. package/skills/gsd-graphify/SKILL.md +10 -6
  503. package/skills/gsd-health/SKILL.md +5 -0
  504. package/skills/gsd-help/SKILL.md +7 -2
  505. package/skills/gsd-import/SKILL.md +7 -3
  506. package/skills/gsd-inbox/SKILL.md +5 -0
  507. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  508. package/skills/gsd-manager/SKILL.md +6 -1
  509. package/skills/gsd-map-codebase/SKILL.md +7 -3
  510. package/skills/gsd-mempalace-capture/SKILL.md +12 -4
  511. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  512. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  513. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  514. package/skills/gsd-new-milestone/SKILL.md +6 -1
  515. package/skills/gsd-new-project/SKILL.md +5 -0
  516. package/skills/gsd-next/SKILL.md +6 -1
  517. package/skills/gsd-ns-context/SKILL.md +4 -0
  518. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  519. package/skills/gsd-ns-manage/SKILL.md +4 -0
  520. package/skills/gsd-ns-project/SKILL.md +4 -0
  521. package/skills/gsd-ns-review/SKILL.md +4 -0
  522. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  523. package/skills/gsd-onboard/SKILL.md +6 -1
  524. package/skills/gsd-pause-work/SKILL.md +5 -1
  525. package/skills/gsd-phase/SKILL.md +8 -4
  526. package/skills/gsd-plan-phase/SKILL.md +6 -1
  527. package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
  528. package/skills/gsd-pr-branch/SKILL.md +4 -0
  529. package/skills/gsd-profile-user/SKILL.md +5 -1
  530. package/skills/gsd-progress/SKILL.md +7 -2
  531. package/skills/gsd-quick/SKILL.md +16 -10
  532. package/skills/gsd-quick-batch/SKILL.md +21 -9
  533. package/skills/gsd-review/SKILL.md +7 -4
  534. package/skills/gsd-review-backlog/SKILL.md +3 -2
  535. package/skills/gsd-secure-phase/SKILL.md +6 -1
  536. package/skills/gsd-ship/SKILL.md +5 -0
  537. package/skills/gsd-sketch/SKILL.md +7 -2
  538. package/skills/gsd-spec-phase/SKILL.md +5 -1
  539. package/skills/gsd-spike/SKILL.md +8 -3
  540. package/skills/gsd-surface/SKILL.md +5 -1
  541. package/skills/gsd-thread/SKILL.md +4 -0
  542. package/skills/gsd-ui-phase/SKILL.md +6 -1
  543. package/skills/gsd-ui-review/SKILL.md +6 -1
  544. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  545. package/skills/gsd-undo/SKILL.md +5 -1
  546. package/skills/gsd-update/SKILL.md +6 -2
  547. package/skills/gsd-validate-phase/SKILL.md +6 -1
  548. package/skills/gsd-verify-work/SKILL.md +6 -1
  549. package/skills/gsd-workspace/SKILL.md +7 -3
  550. package/skills/gsd-workstreams/SKILL.md +6 -6
  551. package/vscode/package.json +1 -1
@@ -14,6 +14,7 @@ const node_path_1 = __importDefault(require("node:path"));
14
14
  const text_lines_cjs_1 = require("./text-lines.cjs");
15
15
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
16
16
  const pattern_cjs_1 = require("./pattern.cjs");
17
+ const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
17
18
  const security_cjs_1 = require("./security.cjs");
18
19
  // eslint-disable-next-line @typescript-eslint/no-require-imports
19
20
  const ioMod = require("./io.cjs");
@@ -30,7 +31,10 @@ const coreUtilsMod = require("./core-utils.cjs");
30
31
  const { toPosixPath, generateSlugInternal, extractOneLinerFromBody } = coreUtilsMod;
31
32
  // eslint-disable-next-line @typescript-eslint/no-require-imports
32
33
  const phaseIdMod = require("./phase-id.cjs");
33
- const { normalizePhaseName, comparePhaseNum, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE, isSentinelPhaseId, renderPhaseBranchName } = phaseIdMod;
34
+ const { normalizePhaseName, comparePhaseNum, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE, isSentinelPhaseId, renderPhaseBranchName, parsePhaseId, renderPhaseId, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, } = phaseIdMod;
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
36
+ const phaseIdDisplayMod = require("./phase-id-display.cjs");
37
+ const { renderBracketMilestoneDisplay } = phaseIdDisplayMod;
34
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports
35
39
  const phaseLocatorMod = require("./phase-locator.cjs");
36
40
  const { getArchivedPhaseDirs, findPhaseInternal, listMilestonePhaseDirs } = phaseLocatorMod;
@@ -56,10 +60,10 @@ const codex_agent_toml_cjs_1 = require("./codex-agent-toml.cjs");
56
60
  const hostIntegrationMod = require("./host-integration.cjs");
57
61
  // eslint-disable-next-line @typescript-eslint/no-require-imports
58
62
  const planningWorkspace = require("./planning-workspace.cjs");
59
- const { planningDir, planningPaths, todosDir } = planningWorkspace;
63
+ const { planningDir, planningPaths, todosDir, resolvePhaseIdConvention } = planningWorkspace;
60
64
  // eslint-disable-next-line @typescript-eslint/no-require-imports
61
65
  const frontmatter = require("./frontmatter.cjs");
62
- const { extractFrontmatter, agentScalarNeedsDoubleQuoting, escapeDoubleQuotedScalar } = frontmatter;
66
+ const { extractFrontmatter, agentScalarNeedsDoubleQuoting, escapeDoubleQuotedScalar, FRONTMATTER_UNPARSEABLE } = frontmatter;
63
67
  // eslint-disable-next-line @typescript-eslint/no-require-imports
64
68
  const modelProfiles = require("./model-profiles.cjs");
65
69
  const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
@@ -69,96 +73,65 @@ const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
69
73
  // eslint-disable-next-line @typescript-eslint/no-require-imports
70
74
  const planScanMod = require("./plan-scan.cjs");
71
75
  const { scanPhasePlans } = planScanMod;
72
- // eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
76
+ // #5060: Phase Status Module — the single owner of "what state is phase P
77
+ // in?" (ADR-5057 §1/§2). Replaces this file's own precedence ladder and
78
+ // determinePhaseStatus derivation.
79
+ const phase_status_cjs_1 = require("./phase-status.cjs");
80
+ // #5118: an aggregate over a refused verification report fails through the owner.
81
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
73
82
  const verificationMod = require("./verification.cjs");
74
- const { resolveVerificationFile } = verificationMod;
75
- // ─── Phase Status ─────────────────────────────────────────────────────────────
83
+ const { failOnVerificationStatusError, firstStatusError } = verificationMod;
84
+ /** The effort-sync line editors' fence reading: a block behind a preamble is still edited (#3706). */
85
+ const EFFORT_SYNC_FENCE = Object.freeze({ allowPreamble: true });
86
+ /** Prefer the Claude agents beside the running local install over the global home. */
87
+ function claudeEffortAgentsDir(explicitDir) {
88
+ if (explicitDir)
89
+ return node_path_1.default.join(explicitDir, 'agents');
90
+ // Compiled commands.cjs lives at <configDir>/gsd-core/bin/lib. In a source
91
+ // checkout the same path leads to the repository root, where no install
92
+ // manifest exists; keep the existing global fallback in that case.
93
+ const configDir = node_path_1.default.resolve(__dirname, '..', '..', '..');
94
+ try {
95
+ const manifestPath = node_path_1.default.join(configDir, 'gsd-file-manifest.json');
96
+ if (node_fs_1.default.lstatSync(manifestPath).isFile()) {
97
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
98
+ const migrations = require('./installer-migrations.cjs');
99
+ const manifest = migrations.readInstallManifest(configDir);
100
+ if (manifest.runtime === 'claude' && manifest.scope === 'local') {
101
+ return node_path_1.default.join(configDir, 'agents');
102
+ }
103
+ }
104
+ }
105
+ catch { /* an absent or unreadable manifest retains the global fallback */ }
106
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
107
+ const { getGlobalConfigDir } = require('./runtime-homes.cjs');
108
+ return node_path_1.default.join(getGlobalConfigDir('claude'), 'agents');
109
+ }
76
110
  /**
77
- * Phase-status precedence ladder — furthest-along wins (#2408).
111
+ * Project one canonical bracket directory onto its display identity and slug.
78
112
  *
79
- * `cmdStats` builds `phasesByNumber` by scanning on-disk phase directories.
80
- * When two directories normalize to the same phase key (e.g. `05-real/` and
81
- * `05-real-stray/`), the status field must be folded by precedence rather
82
- * than overwritten last-write-wins — otherwise `/gsd-stats` reports whatever
83
- * directory `fs.readdirSync` happened to yield last, which is non-deterministic
84
- * across platforms and can silently call a `Complete` phase `Not Started`.
85
- */
86
- const PHASE_STATUS_PRECEDENCE = [
87
- 'Complete',
88
- 'Needs Review',
89
- 'Executed',
90
- 'In Progress',
91
- 'Planned',
92
- 'Not Started',
93
- 'Pending',
94
- ];
95
- const PHASE_STATUS_RANK = new Map(PHASE_STATUS_PRECEDENCE.map((s, i) => [s, i]));
96
- /**
97
- * Fold two phase statuses by precedence — returns whichever is further along
98
- * the {@link PHASE_STATUS_PRECEDENCE} ladder. Unrecognized statuses fall behind
99
- * every recognized one (so a recognized status always wins over an unknown one;
100
- * two unrecognized statuses favor `a` for determinism).
113
+ * The identity is deliberately obtained only through parsePhaseId/renderPhaseId;
114
+ * this helper owns no second bracket grammar. Callers invoke it only after the
115
+ * project's resolved convention is exactly `bracket`.
101
116
  */
102
- function foldPhaseStatus(a, b) {
103
- const ra = PHASE_STATUS_RANK.get(a);
104
- const rb = PHASE_STATUS_RANK.get(b);
105
- if (ra === undefined && rb === undefined)
106
- return a;
107
- if (ra === undefined)
108
- return b;
109
- if (rb === undefined)
110
- return a;
111
- // Lower rank = higher precedence (Complete=0 wins over Not Started=5).
112
- return ra <= rb ? a : b;
117
+ function bracketPhaseDirProjection(dir) {
118
+ const id = parsePhaseId(dir);
119
+ const number = id.subphase ? `${id.phase}.${id.subphase}` : id.phase;
120
+ const identityPrefix = `${id.project}.${id.milestone}-${number}`;
121
+ const slug = dir.slice(identityPrefix.length).replace(/^-/, '');
122
+ return {
123
+ number,
124
+ display_id: renderPhaseId(id),
125
+ name: slug ? slug.replace(/-/g, ' ') : '',
126
+ };
113
127
  }
114
- /**
115
- * Determine phase status by checking plan/summary counts AND verification state.
116
- * Introduces "Executed" for phases with all summaries but no passing verification.
117
- */
118
- function determinePhaseStatus(plans, summaries, phaseDir, defaultPending) {
119
- if (plans === 0)
120
- return defaultPending;
121
- if (summaries < plans && summaries > 0)
122
- return 'In Progress';
123
- if (summaries < plans)
124
- return 'Planned';
125
- // summaries >= plans — check verification
126
- try {
127
- const files = node_fs_1.default.readdirSync(phaseDir);
128
- // #3473 F2: routed through the shared resolver (readdir order is
129
- // filesystem-dependent, so the prior hand-rolled `.find()` could pick
130
- // either file when a phase held both a canonical report and an ad-hoc
131
- // `-CORRECTION-VERIFICATION.md` worksheet — see #3357).
132
- // #3492: pin selection to THIS phase's own token so a stray cross-phase
133
- // or sentinel-numbered canonically-shaped file cannot outrank this
134
- // phase's own (possibly non-canonical) report.
135
- const phaseDirName = node_path_1.default.basename(phaseDir);
136
- const phaseToken = extractPhaseToken(phaseDirName);
137
- const verificationFile = resolveVerificationFile(files, { allowBare: true, phaseToken, phaseDirName });
138
- if (verificationFile) {
139
- const verificationFilePath = node_path_1.default.join(phaseDir, verificationFile);
140
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(verificationFilePath) || '';
141
- // #1159 (Defect A): read ONLY the frontmatter `status` key to avoid false
142
- // matches from historical body metadata such as `previous_status: gaps_found`.
143
- // Full-text regexes like /status:\s*gaps_found/ match the substring inside
144
- // `previous_status: gaps_found`, producing incorrect phase status labels.
145
- const fm = extractFrontmatter(content, verificationFilePath);
146
- // Normalise to lower-case to preserve the prior case-insensitive behaviour
147
- // while reading only the frontmatter `status` key (not the full body text).
148
- const fmStatus = typeof fm['status'] === 'string' ? fm['status'].trim().toLowerCase() : '';
149
- if (fmStatus === 'passed')
150
- return 'Complete';
151
- if (fmStatus === 'human_needed')
152
- return 'Needs Review';
153
- if (fmStatus === 'gaps_found')
154
- return 'Executed';
155
- // Verification exists but unrecognized status — treat as executed
156
- return 'Executed';
157
- }
158
- }
159
- catch { /* directory read failed — fall through */ }
160
- // No verification file — executed but not verified
161
- return 'Executed';
128
+ function recoverBracketPhaseName(dir, phaseToken) {
129
+ const tokenBoundary = `-${phaseToken}`;
130
+ const tokenOffset = dir.indexOf(tokenBoundary);
131
+ if (tokenOffset === -1)
132
+ return '';
133
+ const afterToken = dir.slice(tokenOffset + tokenBoundary.length).replace(/^-/, '');
134
+ return afterToken ? afterToken.replace(/-/g, ' ') : '';
162
135
  }
163
136
  function cmdGenerateSlug(text, raw) {
164
137
  if (!text) {
@@ -237,26 +210,43 @@ function cmdListTodos(cwd, area, raw) {
237
210
  * displayed field is passed through sanitizeForDisplay and each file path is
238
211
  * validated with requireSafePath before reading. Read-only — never mutates.
239
212
  */
213
+ /**
214
+ * Seed id grammars. `SEED-YYMMDD-xxx` (date + 3 base36 chars, the shape
215
+ * `.planning/quick/` uses) is what plant-seed has minted since #4378 removed
216
+ * the shared `wc -l` counter; `SEED-NNN` is the legacy counter form, which
217
+ * keeps parsing forever — existing seeds must never lose their identity.
218
+ *
219
+ * Known (theoretical, documented-not-fixed per #4378 review): a frontmatter-less
220
+ * legacy file whose counter is exactly 6 digits and whose slug opens with
221
+ * exactly 3 base36 chars parses as new-format. Requires a counter >= 100000 AND
222
+ * a missing frontmatter id; with frontmatter the legacy id always wins.
223
+ */
224
+ const CANONICAL_SEED_ID_RE = /^SEED-(?:\d{6}-[a-z0-9]{3}|\d+)$/i;
225
+ const SEED_ID_PREFIX_RE = /^(SEED-(?:\d{6}-[a-z0-9]{3}|\d+))/i;
226
+ const SEED_SLUG_RE = /^SEED-(?:\d{6}-[a-z0-9]{3}|\d+)-(.+)$/i;
240
227
  /**
241
228
  * Derive the canonical `{ seed_id, slug }` from a seed filename stem and the
242
229
  * frontmatter `id:` value. Pure (no I/O) so it can be property-tested directly.
243
230
  *
244
- * seed_id: frontmatter `id:` when it matches `SEED-NNN`, else the numeric prefix
245
- * of the filename (`SEED-NNN-…`), else the whole stem. slug: the descriptive
246
- * remainder after `SEED-NNN-`, else the stem with a leading `SEED-` stripped.
247
- * `rawFmId` is `unknown` because frontmatter values are not guaranteed strings.
231
+ * seed_id: frontmatter `id:` when it matches a seed id grammar (`SEED-YYMMDD-xxx`
232
+ * or legacy `SEED-NNN`), else the id prefix of the filename (`SEED-…-<slug>`),
233
+ * else the whole stem. The prefix fallback must keep the FULL new-format id —
234
+ * truncating at the date gives every same-day seed the same id (#4378).
235
+ * slug: the descriptive remainder after the id, else the stem with a leading
236
+ * `SEED-` stripped. `rawFmId` is `unknown` because frontmatter values are not
237
+ * guaranteed strings.
248
238
  */
249
239
  function deriveSeedIdentity(stem, rawFmId) {
250
240
  const fmId = typeof rawFmId === 'string' ? rawFmId.trim() : '';
251
241
  let seedId;
252
- if (/^SEED-\d+$/i.test(fmId)) {
242
+ if (CANONICAL_SEED_ID_RE.test(fmId)) {
253
243
  seedId = fmId;
254
244
  }
255
245
  else {
256
- const numMatch = stem.match(/^(SEED-\d+)/i);
257
- seedId = numMatch ? numMatch[1] : stem;
246
+ const prefixMatch = stem.match(SEED_ID_PREFIX_RE);
247
+ seedId = prefixMatch ? prefixMatch[1] : stem;
258
248
  }
259
- const slugMatch = stem.match(/^SEED-\d+-(.+)$/i);
249
+ const slugMatch = stem.match(SEED_SLUG_RE);
260
250
  const slug = slugMatch ? slugMatch[1] : stem.replace(/^SEED-/i, '');
261
251
  return { seed_id: seedId, slug };
262
252
  }
@@ -303,9 +293,11 @@ function cmdListSeeds(cwd, statusFilter, raw) {
303
293
  // sanitizeForDisplay is for output, not comparison.
304
294
  if (wantStatus && status !== wantStatus)
305
295
  continue;
306
- // Canonical seed id is `SEED-NNN` (frontmatter `id:`, e.g. SEED-001). Fall
307
- // back to the numeric prefix of the filename, then to the whole stem. The
308
- // descriptive remainder of the filename (`SEED-NNN-<slug>.md`) is the slug.
296
+ // Canonical seed ids are `SEED-YYMMDD-xxx` (frontmatter `id:`, what
297
+ // plant-seed has minted since #4378) or legacy `SEED-NNN`; deriveSeedIdentity
298
+ // owns that grammar. Fall back to the id prefix of the filename, then to the
299
+ // whole stem. The descriptive remainder of the filename (`SEED-…-<slug>.md`)
300
+ // is the slug.
309
301
  const stem = node_path_1.default.basename(entry.name, '.md');
310
302
  const { seed_id: seedId, slug } = deriveSeedIdentity(stem, fm.id);
311
303
  let title = (0, security_cjs_1.sanitizeForDisplay)(fmStr(fm.title).slice(0, 100));
@@ -503,13 +495,10 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
503
495
  opts = opts || {};
504
496
  const config = loadConfig(cwd);
505
497
  const profile = config['model_profile'] || 'balanced';
506
- // #2068: resolve the model per-attempt so dynamic_routing escalates the MODEL
507
- // (heavy tier) alongside effort. Gated on an explicit --attempt exactly like the
508
- // effort resolution below, so the two fields stay symmetric: with no --attempt
509
- // the model comes from the classic profile path (unchanged for everyone,
510
- // including dynamic_routing-enabled users who don't pass --attempt), and only an
511
- // explicit attempt routes through the tier ladder. resolveModelForTier itself
512
- // still falls back to resolveModelInternal when dynamic_routing is off.
498
+ // #2068: resolve the model per-attempt so dynamic_routing ESCALATES the MODEL
499
+ // (heavy tier) alongside effort. The FIRST-spawn tier now comes from
500
+ // resolveModelInternal's own dynamic_routing step (#4505), so the absent-attempt
501
+ // branch below reaches it too — this gate is only about escalation.
513
502
  let model = (opts.attempt !== undefined && opts.attempt !== null)
514
503
  ? resolveModelForTier(cwd, agentType, opts.attempt)
515
504
  : resolveModelInternal(cwd, agentType);
@@ -557,19 +546,19 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
557
546
  let effortEffective = effort;
558
547
  if (runtime === 'claude') {
559
548
  try {
560
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
561
- const { getGlobalConfigDir } = require('./runtime-homes.cjs');
562
- const agentsDirEff = node_path_1.default.join(getGlobalConfigDir(runtime), 'agents');
549
+ const agentsDirEff = claudeEffortAgentsDir();
563
550
  // agentType is an unvalidated CLI positional: keep the read inside the
564
551
  // agents dir so `../../x` cannot point it elsewhere (defense in depth —
565
552
  // the reflected surface is only a frontmatter effort line). Untrusted
566
553
  // input feeding a real read → realpath family (ADR-4650 decision 6).
567
554
  const agentPath = (0, security_cjs_1.assertWithinRoot)(`${agentType}.md`, agentsDirEff, 'agent file');
568
555
  const agentContent = node_fs_1.default.readFileSync(agentPath, 'utf8');
569
- // eslint-disable-next-line local/no-unbounded-quantifier -- same lazy `*?` bounded by the `^---$/m` closing anchor as the sibling frontmatter regexes in this file
570
- const fmMatchEff = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(agentContent);
571
- if (fmMatchEff) {
572
- const effortLine = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatchEff[1]);
556
+ // Byte-0 read: the runtime only honors a frontmatter block that opens at
557
+ // byte 0, so a block behind a preamble is not frontmatter to the loader
558
+ // and this reports what the installed agent will actually run at (#5105).
559
+ const fmSpanEff = agentFrontmatterSpan(agentContent);
560
+ if (fmSpanEff) {
561
+ const effortLine = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmSpanEff.body);
573
562
  if (effortLine) {
574
563
  effortEffective = effortLine[1];
575
564
  effortEffectiveSource = 'frontmatter';
@@ -656,33 +645,42 @@ function effortSurfaceForHost(cwd, host) {
656
645
  return 'none';
657
646
  }
658
647
  }
648
+ /**
649
+ * The leading frontmatter block of an installed agent file, as the one fence owner
650
+ * (`locateFrontmatterFence`) finds it: `body` is the text between the two fences (the last
651
+ * content line's line ending included), `bodyStart`/`closingStart` bound it, and `eol` is the
652
+ * opening fence's line ending. Null when the file has no closed block. By default a block that
653
+ * does not open at byte 0 is not one, exactly as the runtime that loads the agent reads it;
654
+ * the effort-sync line editors pass `EFFORT_SYNC_FENCE` to edit a block behind a preamble too
655
+ * (#3706 pinned that).
656
+ */
657
+ function agentFrontmatterSpan(content, options) {
658
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content, options);
659
+ if (!fence?.closed)
660
+ return null;
661
+ return { body: content.slice(fence.openEnd, fence.closingStart), bodyStart: fence.openEnd, closingStart: fence.closingStart, eol: fence.eol };
662
+ }
659
663
  /**
660
664
  * #488 — Replace or inject the `<key>:` value in YAML frontmatter.
661
665
  * Unlike injectEffortFrontmatter (install.js), this overwrites an existing value.
662
666
  * #3706: key-parameterised so the same line-editor serves both claude's
663
- * `effort:` and OpenCode's `variant:`. #3706: all offsets (eol, openLen,
664
- * closingStart) are derived from the MATCHED BLOCK, not the start of the
665
- * file, and the existing-key replace is scoped to the frontmatter span only.
667
+ * `effort:` and OpenCode's `variant:`. #3706: all offsets (eol, bodyStart,
668
+ * closingStart) come from the block `agentFrontmatterSpan` found, and the
669
+ * existing-key replace is scoped to the frontmatter span only.
666
670
  */
667
671
  function setFrontmatterKeyLine(content, key, value) {
668
- const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
669
- const match = fmRe.exec(content);
670
- if (!match)
672
+ const span = agentFrontmatterSpan(content, EFFORT_SYNC_FENCE);
673
+ if (!span)
671
674
  return content;
672
- const fmBody = match[1];
675
+ const fmBody = span.body;
673
676
  // Both writers of these frontmatter keys — this sync path and the
674
677
  // install-side `frontmatterScalar` in runtime-artifact-conversion.cts —
675
678
  // now share one escaping rule: quote via `agentScalarNeedsDoubleQuoting` +
676
679
  // `escapeDoubleQuotedScalar` (both from frontmatter.cts) rather than each
677
680
  // interpolating `value` raw/differently.
678
681
  const renderedValue = agentScalarNeedsDoubleQuoting(value) ? `"${escapeDoubleQuotedScalar(value)}"` : value;
679
- // EOL comes from the MATCHED BLOCK, not the start of the file. With a
680
- // preamble the two can disagree, and on a CRLF document that misaligns every
681
- // offset below by one byte and mangles the opening fence.
682
- const eol = /^---\r\n/.test(match[0]) ? '\r\n' : '\n';
683
- const openLen = 3 + eol.length;
684
- const bodyStart = match.index + openLen;
685
- const closingStart = bodyStart + fmBody.length;
682
+ // Every offset and the EOL come from the block the fence owner found (#3706).
683
+ const { eol, bodyStart, closingStart } = span;
686
684
  // #3706: key is now generic (not just the literal 'effort'/'variant'
687
685
  // callers happen to pass today) — escape it before interpolating into the
688
686
  // RegExp so a future caller can't have its key metacharacters reinterpreted.
@@ -718,19 +716,17 @@ function setFrontmatterKeyLine(content, key, value) {
718
716
  * codex-agent-toml strip discipline: targeted line removal, EOL-aware, every
719
717
  * other byte (comments, sibling keys, the body) untouched.
720
718
  * #3706: key-parameterised so the same line-editor serves both claude's
721
- * `effort:` and OpenCode's `variant:`. #3706: openLen is derived from the
722
- * MATCHED BLOCK, not the start of the file — a preamble on a CRLF document
723
- * would otherwise misalign every offset below.
719
+ * `effort:` and OpenCode's `variant:`. #3706: every offset comes from the
720
+ * block `agentFrontmatterSpan` found.
724
721
  */
725
722
  function removeFrontmatterKeyLine(content, key) {
726
723
  // Scoped to the FIRST frontmatter block (not a whole-file /m match): a
727
724
  // preamble or body line starting with `<key>:` (a fenced config example,
728
725
  // a thematic-break flanked fragment) must never be the line removed.
729
- const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
730
- const match = fmRe.exec(content);
731
- if (!match)
726
+ const span = agentFrontmatterSpan(content, EFFORT_SYNC_FENCE);
727
+ if (!span)
732
728
  return content;
733
- const fmBody = match[1];
729
+ const fmBody = span.body;
734
730
  // #3706: same generic-key escape as setFrontmatterKeyLine above.
735
731
  const lineRe = new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}:[ \\t]*.*\\r?\\n?`, 'm');
736
732
  if (!lineRe.test(fmBody))
@@ -745,12 +741,7 @@ function removeFrontmatterKeyLine(content, key) {
745
741
  // is removed in one pass.
746
742
  const stripAllRe = new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}:[ \\t]*.*\\r?\\n?`, 'gm');
747
743
  const strippedFm = fmBody.replace(stripAllRe, '');
748
- // Same rule as setFrontmatterKeyLine: the EOL must come from the matched
749
- // block, not the start of the file, or a preambled CRLF document misaligns.
750
- const eol = /^---\r\n/.test(match[0]) ? '\r\n' : '\n';
751
- const openLen = 3 + eol.length;
752
- const closingStart = match.index + openLen + fmBody.length;
753
- return content.slice(0, match.index + openLen) + strippedFm + content.slice(closingStart);
744
+ return content.slice(0, span.bodyStart) + strippedFm + content.slice(span.closingStart);
754
745
  }
755
746
  /** #488 — Replace or inject the `effort:` value in YAML frontmatter. */
756
747
  function setEffortFrontmatter(content, effortValue) {
@@ -795,8 +786,6 @@ function cmdEffortSync(cwd, raw, opts) {
795
786
  output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, reason: `runtime '${runtime}' does not use effort: frontmatter` }, raw, '');
796
787
  return;
797
788
  }
798
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
799
- const { getGlobalConfigDir } = require('./runtime-homes.cjs');
800
789
  // Use install-time resolvers: they merge ~/.gsd/defaults.json with project config,
801
790
  // matching the exact logic used when agents were originally installed. #2071: these
802
791
  // live in the shipped sibling install-effort-resolver.cjs (extracted from the
@@ -804,7 +793,7 @@ function cmdEffortSync(cwd, raw, opts) {
804
793
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
805
794
  const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('./install-effort-resolver.cjs');
806
795
  const effortCfg = readGsdEffectiveEffortConfig(cwd);
807
- const agentsDir = node_path_1.default.join(opts.configDir || getGlobalConfigDir(runtime), 'agents');
796
+ const agentsDir = claudeEffortAgentsDir(opts.configDir);
808
797
  if (!node_fs_1.default.existsSync(agentsDir)) {
809
798
  output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, agents_dir: agentsDir, reason: 'agents directory not found' }, raw, '');
810
799
  return;
@@ -822,6 +811,10 @@ function cmdEffortSync(cwd, raw, opts) {
822
811
  }).sort(); // #3706: sorted like the codex and
823
812
  // opencode branches — readdir order is platform-dependent, so leaving it unsorted makes the
824
813
  // reported `changes` ordering differ across machines for identical inputs.
814
+ if (files.length === 0) {
815
+ output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, agents_dir: agentsDir, reason: 'no GSD agent files found' }, raw, '');
816
+ return;
817
+ }
825
818
  const changes = [];
826
819
  let synced = 0;
827
820
  let skipped = 0;
@@ -866,7 +859,7 @@ function cmdEffortSync(cwd, raw, opts) {
866
859
  // drift and the sync re-added a hand-stripped key on every apply. A
867
860
  // present key under inherit is stripped, reported as {from, to: null}.
868
861
  if (universalEffort === 'inherit') {
869
- const fmMatchInherit = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
862
+ const fmMatchInherit = agentFrontmatterSpan(content, EFFORT_SYNC_FENCE);
870
863
  if (!fmMatchInherit) {
871
864
  skipped++;
872
865
  continue;
@@ -877,12 +870,12 @@ function cmdEffortSync(cwd, raw, opts) {
877
870
  // from a null value alone is wrong here — it would leave an
878
871
  // unresolvable `effort: null` key on disk forever. Test presence with
879
872
  // its own regex, and only compare values once presence is known.
880
- const effortPresentInherit = /^effort:/m.test(fmMatchInherit[1]);
873
+ const effortPresentInherit = /^effort:/m.test(fmMatchInherit.body);
881
874
  if (!effortPresentInherit) {
882
875
  skipped++;
883
876
  continue;
884
877
  }
885
- const effortMatchInherit = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatchInherit[1]);
878
+ const effortMatchInherit = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatchInherit.body);
886
879
  // `effortPresentInherit` is guaranteed true here (checked above), so a
887
880
  // failed value match means the key is present with an EMPTY value —
888
881
  // report `''`, not `null`, so "present-but-empty" is never conflated
@@ -941,7 +934,7 @@ function cmdEffortSync(cwd, raw, opts) {
941
934
  // codex's 'ultra' rejection can produce a null value).
942
935
  const rendered = (0, model_catalog_cjs_1.renderEffortForRuntime)(runtime, universalEffort);
943
936
  const newEffortValue = rendered.value;
944
- const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
937
+ const fmMatch = agentFrontmatterSpan(content, EFFORT_SYNC_FENCE);
945
938
  if (!fmMatch) {
946
939
  skipped++;
947
940
  continue;
@@ -954,8 +947,8 @@ function cmdEffortSync(cwd, raw, opts) {
954
947
  // never null on this path (guarded above), so an absent key already
955
948
  // yields `currentEffort === null !== newEffortValue` without consulting
956
949
  // presence separately.
957
- const effortPresent = /^effort:/m.test(fmMatch[1]);
958
- const effortMatch = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch[1]);
950
+ const effortPresent = /^effort:/m.test(fmMatch.body);
951
+ const effortMatch = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
959
952
  // `null` (key absent) and `''` (key present, value empty) are distinct
960
953
  // states `effortPresent` deliberately disambiguates — collapsing both to
961
954
  // `null` here would make the reported `from` lie about which case fired.
@@ -1228,7 +1221,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
1228
1221
  // exactly what install writes.
1229
1222
  const universal = effortCfg ? resolveInstallTimeEffort(effortCfg, agentName) : null;
1230
1223
  const target = universal ? clampEffortForHost('opencode', universal) : null;
1231
- const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
1224
+ const fmMatch = agentFrontmatterSpan(content, EFFORT_SYNC_FENCE);
1232
1225
  if (!fmMatch) {
1233
1226
  skipped++;
1234
1227
  continue;
@@ -1240,8 +1233,8 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
1240
1233
  // null — it would leave an unresolvable `variant: null` key on disk
1241
1234
  // forever. Test presence with its own regex, and only compare values
1242
1235
  // once presence is known.
1243
- const variantPresent = /^variant:/m.test(fmMatch[1]);
1244
- const variantMatch = /^variant:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch[1]);
1236
+ const variantPresent = /^variant:/m.test(fmMatch.body);
1237
+ const variantMatch = /^variant:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
1245
1238
  // `null` (key absent) and `''` (key present, value empty) are distinct
1246
1239
  // states this code deliberately tracks via `variantPresent` above — a
1247
1240
  // reported `from` that collapses both to `null` would make "no key" and
@@ -2838,13 +2831,38 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
2838
2831
  if (!summaryPath) {
2839
2832
  error('summary-path required for summary-extract');
2840
2833
  }
2841
- const fullPath = node_path_1.default.join(cwd, summaryPath);
2834
+ // #5013: requireSafePath (not path.join) so an absolute summaryPath resolves to
2835
+ // that literal path instead of being concatenated onto cwd — path.join has no
2836
+ // special case for an absolute second argument the way path.resolve does. Uses
2837
+ // requireSafePath (the same PathAcceptance.AbsoluteInsideRoot pattern
2838
+ // cmdAuditAcknowledge's write-side counterpart already uses in audit.cts)
2839
+ // rather than a bare path.resolve: a plain path.resolve would also have made
2840
+ // an absolute summaryPath anywhere on the filesystem resolve and have its
2841
+ // content echoed back (this command reads and returns file content, unlike
2842
+ // cmdVerifyPathExists's exists-only check) — an orthogonal-review finding on
2843
+ // this same fix. requireSafePath keeps the acceptance criterion (an absolute
2844
+ // path within the project resolves correctly) while refusing one that
2845
+ // escapes cwd, and rejects relative `../` escapes the same way (pre-existing
2846
+ // in this function, closed as a direct consequence, not separately in scope).
2847
+ const fullPath = (0, security_cjs_1.requireSafePath)(summaryPath, cwd, 'summary-extract target', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
2842
2848
  if (!node_fs_1.default.existsSync(fullPath)) {
2843
2849
  output({ error: 'File not found', path: summaryPath }, raw, undefined);
2844
2850
  return;
2845
2851
  }
2846
2852
  const content = node_fs_1.default.readFileSync(fullPath, 'utf-8');
2847
2853
  const fm = extractFrontmatter(content, fullPath);
2854
+ // #5013: an unparseable frontmatter block (malformed YAML in the fence) is
2855
+ // NOT the same as an absent/empty one — extractFrontmatter marks it with
2856
+ // FRONTMATTER_UNPARSEABLE (mirrors state.cts's isUnparseableFrontmatter /
2857
+ // state-transition.cts's identical helper). Left unchecked, every field
2858
+ // below silently falls back to its empty default and this command exits 0,
2859
+ // so a milestone audit reading requirements_completed through it would
2860
+ // report "zero requirements completed" for a plan that completed several,
2861
+ // with nothing on stderr. Refuse instead, naming the file, the same way
2862
+ // cmdAuditAcknowledge (audit.cts) refuses on the write-side counterpart.
2863
+ if (fm[FRONTMATTER_UNPARSEABLE] === true) {
2864
+ error(`summary-extract: the frontmatter of "${summaryPath}" is not parseable YAML — fix the syntax error in the file (e.g. an unquoted ": " inside a block-sequence item) before re-running; every field would otherwise silently read as empty`, ERROR_REASON.SUMMARY_EXTRACT_UNPARSEABLE, { path: summaryPath });
2865
+ }
2848
2866
  // Parse key-decisions into structured format
2849
2867
  const parseDecisions = (decisionsList) => {
2850
2868
  if (!decisionsList || !Array.isArray(decisionsList))
@@ -3006,34 +3024,77 @@ async function cmdWebsearch(query, options, raw) {
3006
3024
  function cmdProgressRender(cwd, format, raw) {
3007
3025
  const phasesDir = planningPaths(cwd).phases;
3008
3026
  const milestone = getMilestoneInfo(cwd).value;
3027
+ const phaseIdConvention = resolvePhaseIdConvention(cwd);
3028
+ const milestoneDisplay = phaseIdConvention === 'bracket'
3029
+ ? renderBracketMilestoneDisplay(milestone?.version, loadConfig(cwd).project_code)
3030
+ : null;
3031
+ const milestoneVersion = milestoneDisplay ?? milestone?.version ?? null;
3009
3032
  const phases = [];
3010
3033
  let totalPlans = 0;
3011
3034
  let totalSummaries = 0;
3012
3035
  let phaseScope = null;
3036
+ // #5118: carried out of the scan below and failed on after it.
3037
+ let statusError;
3013
3038
  try {
3014
3039
  // #3185 (ADR-3180 Decision 1): the single owner applies the milestone
3015
3040
  // window AND the sentinel filter and returns dirs already sorted by
3016
3041
  // comparePhaseNum. This command previously read the phases directory
3017
3042
  // directly with neither, which is why `query progress` listed 999.*
3018
3043
  // backlog directories as current-milestone phases (#3167).
3019
- const { value: dirs, scope } = listMilestonePhaseDirs(phasesDir, { cwd });
3044
+ const { value: dirs, scope } = listMilestonePhaseDirs(phasesDir, {
3045
+ cwd,
3046
+ phaseIdConvention,
3047
+ });
3020
3048
  phaseScope = scope;
3021
3049
  for (const dir of dirs) {
3022
- const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
3023
- const phaseNum = dm ? dm[1] : dir;
3024
- const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
3025
- // #3183: canonical plan/summary counts (root+nested, superseded-excluded,
3026
- // canonical pairing) from the single owner.
3027
- const phaseScan = scanPhasePlans(node_path_1.default.join(phasesDir, dir));
3028
- const plans = phaseScan.planCount;
3029
- const summaries = phaseScan.summaryCount;
3050
+ let phaseNum;
3051
+ let phaseName;
3052
+ let displayId;
3053
+ if (phaseIdConvention === 'bracket') {
3054
+ try {
3055
+ const projection = bracketPhaseDirProjection(dir);
3056
+ phaseNum = projection.number;
3057
+ phaseName = projection.name;
3058
+ displayId = projection.display_id;
3059
+ }
3060
+ catch {
3061
+ // A malformed/tolerated directory must not suppress every later row.
3062
+ // It cannot receive a canonical display_id because parsePhaseId
3063
+ // rejected it, but the convention-aware token keeps it observable.
3064
+ const phaseToken = extractPhaseToken(dir, phaseIdConvention);
3065
+ phaseNum = phaseToken || dir;
3066
+ phaseName = recoverBracketPhaseName(dir, phaseToken);
3067
+ }
3068
+ }
3069
+ else {
3070
+ const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
3071
+ phaseNum = dm ? dm[1] : dir;
3072
+ phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
3073
+ }
3074
+ // #3183/#5060: canonical plan/summary counts and lifecycle status
3075
+ // (root+nested, superseded-excluded, canonical pairing) from the
3076
+ // single owner (Phase Status Module, ADR-5057).
3077
+ const ps = (0, phase_status_cjs_1.phaseStatus)(node_path_1.default.join(phasesDir, dir), { convention: phaseIdConvention });
3078
+ statusError = firstStatusError(statusError, ps.value.statusError);
3079
+ const plans = ps.value.planCount;
3080
+ const summaries = ps.value.summaryCount;
3030
3081
  totalPlans += plans;
3031
3082
  totalSummaries += summaries;
3032
- const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Pending');
3033
- phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
3083
+ const status = (0, phase_status_cjs_1.toDisplayLabel)(ps.value.status, { pendingWord: 'Pending' });
3084
+ phases.push({
3085
+ number: phaseNum,
3086
+ ...(displayId ? { display_id: displayId } : {}),
3087
+ name: phaseName,
3088
+ plans,
3089
+ summaries,
3090
+ status,
3091
+ });
3034
3092
  }
3035
3093
  }
3036
3094
  catch { /* intentionally empty */ }
3095
+ // #5118: a read-only aggregate over a refused report prints nothing.
3096
+ if (statusError)
3097
+ failOnVerificationStatusError(statusError);
3037
3098
  // #3217 (ADR-3180 §7.6 rule 4): `phaseScope` was already computed above
3038
3099
  // (Phase 3, #3222) but never consulted before rendering — a percentage was
3039
3100
  // rendered from counts the scope said were not answers (TRUNCATED /
@@ -3048,12 +3109,12 @@ function cmdProgressRender(cwd, format, raw) {
3048
3109
  // Render markdown table
3049
3110
  const bar = (0, phase_lifecycle_cjs_1.renderProgressBar)(percent, 10);
3050
3111
  const percentSuffix = percent === null ? '' : ` (${percent}%)`;
3051
- let out = `# ${milestone?.version ?? ''} ${milestone?.name ?? ''}\n\n`;
3112
+ let out = `# ${milestoneVersion ?? ''} ${milestone?.name ?? ''}\n\n`;
3052
3113
  out += `**Progress:** [${bar}] ${totalSummaries}/${totalPlans} plans${percentSuffix}\n\n`;
3053
3114
  out += `| Phase | Name | Plans | Status |\n`;
3054
3115
  out += `|-------|------|-------|--------|\n`;
3055
3116
  for (const p of phases) {
3056
- out += `| ${p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`;
3117
+ out += `| ${p.display_id ?? p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`;
3057
3118
  }
3058
3119
  output({ rendered: out }, raw, out);
3059
3120
  }
@@ -3066,7 +3127,7 @@ function cmdProgressRender(cwd, format, raw) {
3066
3127
  else {
3067
3128
  // JSON format
3068
3129
  output({
3069
- milestone_version: milestone?.version ?? null,
3130
+ milestone_version: milestoneVersion,
3070
3131
  milestone_name: milestone?.name ?? null,
3071
3132
  phases,
3072
3133
  total_plans: totalPlans,
@@ -3192,29 +3253,35 @@ function cmdTodoMatchPhase(cwd, phase, raw) {
3192
3253
  // #4096: upsert completion keys INSIDE the leading frontmatter block. Never a
3193
3254
  // bare prefix line above the opening `---` (that displaces the fence to line 2
3194
3255
  // and breaks every fence-locating reader). A file with no well-formed block
3195
- // (absent, or an unterminated opening fence) gains a complete block.
3256
+ // (absent, or an unterminated opening fence) gains a complete block. The block is
3257
+ // the one the one fence owner (`locateFrontmatterFence`) finds, so this writer and
3258
+ // every todo reader agree on where it is.
3196
3259
  function upsertTodoCompletionFields(content, today) {
3197
- const lines = content.split('\n');
3198
3260
  const fields = [`completed: ${today}`, 'status: completed'];
3199
- const hasOpeningFence = lines[0] !== undefined && lines[0].trim() === '---';
3200
- const closeIdx = hasOpeningFence ? lines.findIndex((l, i) => i > 0 && l.trim() === '---') : -1;
3201
- if (!hasOpeningFence || closeIdx === -1) {
3261
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content);
3262
+ if (!fence?.closed) {
3202
3263
  // No parseable frontmatter: wrap the whole content in a complete block
3203
- // rather than prefixing bare keys (#4096 fix 2).
3204
- return `---\n${fields.join('\n')}\n---\n\n${content}`;
3205
- }
3206
- const block = lines.slice(1, closeIdx);
3264
+ // rather than prefixing bare keys (#4096 fix 2). A leading BOM (#2977) stays
3265
+ // the document's first character, ahead of the new fence.
3266
+ const bom = content.charCodeAt(0) === 0xFEFF ? content.slice(0, 1) : '';
3267
+ return `${bom}---\n${fields.join('\n')}\n---\n\n${content.slice(bom.length)}`;
3268
+ }
3269
+ // The block's lines, split on a bare '\n' so a CRLF line keeps its '\r' — and a
3270
+ // written field line carries one too, so a CRLF block never gains LF-only lines.
3271
+ const inner = content.slice(fence.openEnd, fence.closingStart);
3272
+ const block = inner === '' ? [] : inner.slice(0, -1).split('\n');
3273
+ const cr = fence.eol === '\r\n' ? '\r' : '';
3207
3274
  for (const field of fields) {
3208
3275
  const key = `${field.slice(0, field.indexOf(':'))}:`;
3209
3276
  const idx = block.findIndex(l => l.startsWith(key));
3210
3277
  if (idx === -1) {
3211
- block.push(field);
3278
+ block.push(field + cr);
3212
3279
  }
3213
3280
  else {
3214
- block[idx] = field;
3281
+ block[idx] = field + cr;
3215
3282
  }
3216
3283
  }
3217
- return [...lines.slice(0, 1), ...block, ...lines.slice(closeIdx)].join('\n');
3284
+ return `${content.slice(0, fence.openEnd)}${block.join('\n')}\n${content.slice(fence.closingStart)}`;
3218
3285
  }
3219
3286
  function cmdTodoComplete(cwd, filename, options, raw) {
3220
3287
  if (!filename) {
@@ -3232,7 +3299,7 @@ function cmdTodoComplete(cwd, filename, options, raw) {
3232
3299
  // to a location inside todosRoot (or inside pending/) and would pass
3233
3300
  // containment, yet none of them is a bare filename. Reject on basename
3234
3301
  // shape FIRST, before any path is even joined — same predicate shape as
3235
- // findPhaseArtifact in check-command-router.cts. Checking both `/` and
3302
+ // findPhaseArtifact in gate-predicate.cts. Checking both `/` and
3236
3303
  // `\` explicitly (not just path.basename) matters on POSIX, where a
3237
3304
  // literal backslash is just an ordinary filename character to
3238
3305
  // path.basename but not to path.win32.basename or to the user's intent.
@@ -3361,7 +3428,15 @@ function cmdStats(cwd, format, raw) {
3361
3428
  const reqPath = planningPaths(cwd).requirements;
3362
3429
  const statePath = planningPaths(cwd).state;
3363
3430
  const milestone = getMilestoneInfo(cwd).value;
3431
+ const phaseIdConvention = resolvePhaseIdConvention(cwd);
3432
+ const milestoneDisplay = phaseIdConvention === 'bracket'
3433
+ ? renderBracketMilestoneDisplay(milestone?.version, loadConfig(cwd).project_code)
3434
+ : null;
3435
+ const milestoneVersion = milestoneDisplay ?? milestone?.version ?? null;
3364
3436
  // Phase & plan stats (reuse progress pattern)
3437
+ // #5060: `phaseStatus` here is the internal PHASE_STATUS ladder value, never
3438
+ // a display label — the output projection below is the only place a label
3439
+ // is produced (toDisplayLabel).
3365
3440
  const phasesByNumber = new Map();
3366
3441
  let totalPlans = 0;
3367
3442
  let totalSummaries = 0;
@@ -3380,54 +3455,105 @@ function cmdStats(cwd, format, raw) {
3380
3455
  // prose mentioning `### Phase N:` inside an inline code span produced a phantom
3381
3456
  // Not-Started row and made phases_total disagree with roadmap analyze.
3382
3457
  // phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
3383
- const headingPattern = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+([A-Za-z]?\d+[A-Z]?(?:[.-]\d+)*)(?:\s*\([^)\n]{0,200}\))?\s*:\s*([^\n]+)/gi;
3458
+ const capturesBracketId = phaseIdConvention === 'bracket';
3459
+ const headingPrefix = phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.ANY_BRACKET, phaseIdConvention, capturesBracketId);
3460
+ // phase-id-owner: this preserves cmdStats's shipped dot-or-dash heading
3461
+ // token variant; PHASE_NUMBER_TOKEN_SOURCE is dot-only and would drop M-NN.
3462
+ const headingPattern = new RegExp(
3463
+ // phase-id-owner: cmdStats's shipped [.-] token variant; the canonical token is dot-only.
3464
+ `#{2,4}\\s*${headingPrefix}([A-Za-z]?\\d+[A-Z]?(?:[.-]\\d+)*)(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
3384
3465
  let match;
3385
3466
  while ((match = headingPattern.exec(roadmapContent)) !== null) {
3467
+ const bracketId = capturesBracketId ? match[1] : undefined;
3468
+ const phaseToken = capturesBracketId ? match[2] : match[1];
3469
+ const phaseName = capturesBracketId ? match[3] : match[2];
3386
3470
  // #3185: the heading seed carried no sentinel filter, so a
3387
3471
  // `### Phase 999.1:` backlog heading produced a stats row even with no
3388
3472
  // directory on disk. Uses the canonical predicate (phase-id.cts), not a
3389
3473
  // local literal — the rule had five copies and three regex variants
3390
3474
  // before this phase, disagreeing about Phase 0.
3391
- if (isSentinelPhaseId(match[1]))
3475
+ const sentinelId = bracketId ? `${bracketId}-${phaseToken}` : phaseToken;
3476
+ if (capturesBracketId
3477
+ ? isSentinelPhaseId(sentinelId, phaseIdConvention)
3478
+ : isSentinelPhaseId(sentinelId))
3392
3479
  continue;
3393
- const key = normalizePhaseName(match[1]);
3480
+ const key = normalizePhaseName(phaseToken);
3481
+ let displayId;
3482
+ if (bracketId) {
3483
+ try {
3484
+ displayId = renderPhaseId(parsePhaseId(`${bracketId}-${phaseToken}`));
3485
+ }
3486
+ catch {
3487
+ // Read tolerance can admit a non-canonical heading spelling; keep
3488
+ // the stats row but do not invent a canonical identity for it.
3489
+ }
3490
+ }
3394
3491
  phasesByNumber.set(key, {
3395
3492
  number: key,
3396
- name: match[2].replace(/\(INSERTED\)/i, '').trim(),
3493
+ ...(displayId ? { display_id: displayId } : {}),
3494
+ name: phaseName.replace(/\(INSERTED\)/i, '').trim(),
3397
3495
  plans: 0,
3398
3496
  summaries: 0,
3399
- status: 'Not Started',
3497
+ phaseStatus: phase_status_cjs_1.PHASE_STATUS.NOT_STARTED,
3400
3498
  });
3401
3499
  }
3402
3500
  }
3403
3501
  catch { /* intentionally empty */ }
3502
+ // #5118: the first phase whose verification report `status` is outside the
3503
+ // closed set — carried out of the scan below and failed on after it.
3504
+ let statusError;
3404
3505
  try {
3405
3506
  // #3185 (ADR-3180 Decision 1): route through the single owner. This
3406
3507
  // previously applied the milestone window but NOT a directory-level
3407
3508
  // sentinel filter — and getMilestonePhaseFilter degrades to a pass-all
3408
3509
  // predicate when its heading set is empty, at which point every directory
3409
3510
  // on disk passed, backlog included (#3167).
3410
- const { value: dirs, scope } = listMilestonePhaseDirs(phasesDir, { cwd });
3511
+ const { value: dirs, scope } = listMilestonePhaseDirs(phasesDir, {
3512
+ cwd,
3513
+ phaseIdConvention,
3514
+ });
3411
3515
  phaseScope = scope;
3412
3516
  for (const dir of dirs) {
3413
- // Use extractPhaseToken to correctly parse M-NN-style and code-prefixed dir names.
3414
- const phaseToken = extractPhaseToken(dir);
3415
- const phaseNum = phaseToken || dir;
3416
- // phaseName is everything after the token (strip leading '-')
3417
- const afterToken = dir.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
3418
- const phaseName = afterToken ? afterToken.replace(/-/g, ' ') : '';
3419
- // #3183: canonical plan/summary counts (root+nested, superseded-excluded,
3420
- // canonical pairing) from the single owner.
3421
- const phaseScan = scanPhasePlans(node_path_1.default.join(phasesDir, dir));
3422
- const plans = phaseScan.planCount;
3423
- const summaries = phaseScan.summaryCount;
3517
+ let phaseNum;
3518
+ let phaseName;
3519
+ let displayId;
3520
+ if (phaseIdConvention === 'bracket') {
3521
+ try {
3522
+ const projection = bracketPhaseDirProjection(dir);
3523
+ phaseNum = projection.number;
3524
+ phaseName = projection.name;
3525
+ displayId = projection.display_id;
3526
+ }
3527
+ catch {
3528
+ const phaseToken = extractPhaseToken(dir, phaseIdConvention);
3529
+ phaseNum = phaseToken || dir;
3530
+ phaseName = recoverBracketPhaseName(dir, phaseToken);
3531
+ }
3532
+ }
3533
+ else {
3534
+ // Use extractPhaseToken to correctly parse M-NN-style and code-prefixed dir names.
3535
+ const phaseToken = extractPhaseToken(dir);
3536
+ phaseNum = phaseToken || dir;
3537
+ // phaseName is everything after the token (strip leading '-')
3538
+ const afterToken = dir.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
3539
+ phaseName = afterToken ? afterToken.replace(/-/g, ' ') : '';
3540
+ }
3541
+ // #3183/#5060: canonical plan/summary counts and lifecycle status
3542
+ // (root+nested, superseded-excluded, canonical pairing) from the
3543
+ // single owner (Phase Status Module, ADR-5057).
3544
+ const ps = (0, phase_status_cjs_1.phaseStatus)(node_path_1.default.join(phasesDir, dir), { convention: phaseIdConvention });
3545
+ statusError = firstStatusError(statusError, ps.value.statusError);
3546
+ const plans = ps.value.planCount;
3547
+ const summaries = ps.value.summaryCount;
3424
3548
  totalPlans += plans;
3425
3549
  totalSummaries += summaries;
3426
- const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Not Started');
3427
3550
  const normalizedNum = normalizePhaseName(phaseNum);
3428
3551
  const existing = phasesByNumber.get(normalizedNum);
3429
3552
  phasesByNumber.set(normalizedNum, {
3430
3553
  number: normalizedNum,
3554
+ ...(existing?.display_id || displayId
3555
+ ? { display_id: existing?.display_id || displayId }
3556
+ : {}),
3431
3557
  name: existing?.name || phaseName,
3432
3558
  plans: (existing?.plans || 0) + plans,
3433
3559
  summaries: (existing?.summaries || 0) + summaries,
@@ -3436,13 +3562,26 @@ function cmdStats(cwd, format, raw) {
3436
3562
  // platforms, so a naive overwrite can report a Complete phase as Not
3437
3563
  // Started (or vice versa) depending on read order. The fold picks the
3438
3564
  // furthest-along status, matching what an operator expects.
3439
- status: existing ? foldPhaseStatus(existing.status, status) : status,
3565
+ phaseStatus: existing ? (0, phase_status_cjs_1.foldPhaseStatuses)(existing.phaseStatus, ps.value.status) : ps.value.status,
3440
3566
  });
3441
3567
  }
3442
3568
  }
3443
3569
  catch { /* intentionally empty */ }
3444
- const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
3445
- const completedPhases = phases.filter(p => p.status === 'Complete').length;
3570
+ // #5118: a read-only aggregate over a refused report prints nothing.
3571
+ if (statusError)
3572
+ failOnVerificationStatusError(statusError);
3573
+ const internalPhases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
3574
+ const completedPhases = internalPhases.filter(p => p.phaseStatus === phase_status_cjs_1.PHASE_STATUS.COMPLETE).length;
3575
+ // #5060: project the internal PHASE_STATUS ladder to the display label at
3576
+ // the output boundary only — the internal `phaseStatus` field is never emitted.
3577
+ const phases = internalPhases.map(p => ({
3578
+ number: p.number,
3579
+ ...(p.display_id ? { display_id: p.display_id } : {}),
3580
+ name: p.name,
3581
+ plans: p.plans,
3582
+ summaries: p.summaries,
3583
+ status: (0, phase_status_cjs_1.toDisplayLabel)(p.phaseStatus, { pendingWord: 'Not Started' }),
3584
+ }));
3446
3585
  // #3217 (ADR-3180 §7.6 rule 4): both percentages here are derived from the
3447
3586
  // same `phaseScope`-carrying directory enumeration above (Phase 3, #3222) —
3448
3587
  // withhold both when that scope is not COMPLETE, same rationale as
@@ -3486,7 +3625,7 @@ function cmdStats(cwd, format, raw) {
3486
3625
  }
3487
3626
  }
3488
3627
  const result = {
3489
- milestone_version: milestone?.version ?? null,
3628
+ milestone_version: milestoneVersion,
3490
3629
  milestone_name: milestone?.name ?? null,
3491
3630
  phases,
3492
3631
  phases_completed: completedPhases,
@@ -3506,7 +3645,7 @@ function cmdStats(cwd, format, raw) {
3506
3645
  };
3507
3646
  if (format === 'table') {
3508
3647
  const bar = (0, phase_lifecycle_cjs_1.renderProgressBar)(percent, 10);
3509
- let out = `# ${milestone?.version ?? ''} ${milestone?.name ?? ''} — Statistics\n\n`;
3648
+ let out = `# ${milestoneVersion ?? ''} ${milestone?.name ?? ''} — Statistics\n\n`;
3510
3649
  const percentSuffix = percent === null ? '' : ` (${percent}%)`;
3511
3650
  out += `**Progress:** [${bar}] ${completedPhases}/${phases.length} phases${percentSuffix}\n`;
3512
3651
  if (totalPlans > 0 && planPercent !== null) {
@@ -3520,7 +3659,7 @@ function cmdStats(cwd, format, raw) {
3520
3659
  out += `| Phase | Name | Plans | Completed | Status |\n`;
3521
3660
  out += `|-------|------|-------|-----------|--------|\n`;
3522
3661
  for (const p of phases) {
3523
- out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
3662
+ out += `| ${p.display_id ?? p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
3524
3663
  }
3525
3664
  if (gitCommits > 0) {
3526
3665
  out += `\n**Git:** ${gitCommits} commits`;
@@ -3768,9 +3907,6 @@ function cmdCommitDocsGuardDisable(cwd, raw) {
3768
3907
  module.exports = {
3769
3908
  effortSurfaceForHost,
3770
3909
  groupFilesBySubrepo,
3771
- determinePhaseStatus,
3772
- foldPhaseStatus,
3773
- PHASE_STATUS_PRECEDENCE,
3774
3910
  cmdGenerateSlug,
3775
3911
  cmdCurrentTimestamp,
3776
3912
  cmdListTodos,