@opengsd/gsd-core 1.15.0 → 1.17.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 (505) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -2
  3. package/.opencode/plugins/gsd-core.js +336 -6
  4. package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
  5. package/GEMINI.md +1 -1
  6. package/agents/gsd-code-fixer.compact.md +1 -1
  7. package/agents/gsd-code-fixer.md +1 -1
  8. package/agents/gsd-code-reviewer.compact.md +5 -3
  9. package/agents/gsd-code-reviewer.md +8 -6
  10. package/agents/gsd-debug-session-manager.compact.md +7 -1
  11. package/agents/gsd-debug-session-manager.md +7 -1
  12. package/agents/gsd-debugger.md +2 -2
  13. package/agents/gsd-eval-auditor.compact.md +1 -1
  14. package/agents/gsd-eval-auditor.md +1 -1
  15. package/agents/gsd-executor.md +4 -4
  16. package/agents/gsd-intel-updater.compact.md +1 -1
  17. package/agents/gsd-intel-updater.md +1 -1
  18. package/agents/gsd-mempalace-curator.md +2 -2
  19. package/agents/gsd-phase-researcher.md +1 -1
  20. package/agents/gsd-plan-checker.md +7 -2
  21. package/agents/gsd-planner.md +3 -3
  22. package/agents/gsd-project-researcher.compact.md +1 -1
  23. package/agents/gsd-project-researcher.md +1 -1
  24. package/agents/gsd-research-synthesizer.compact.md +1 -1
  25. package/agents/gsd-research-synthesizer.md +1 -1
  26. package/agents/gsd-ui-auditor.compact.md +21 -30
  27. package/agents/gsd-ui-auditor.md +42 -51
  28. package/agents/gsd-ui-researcher.compact.md +1 -1
  29. package/agents/gsd-ui-researcher.md +1 -1
  30. package/agents/gsd-verifier.md +31 -9
  31. package/bin/gsd-mcp-server.js +3 -1
  32. package/bin/install.js +487 -290
  33. package/commands/gsd/add-tests.md +6 -1
  34. package/commands/gsd/ai-integration-phase.md +6 -1
  35. package/commands/gsd/audit-fix.md +5 -0
  36. package/commands/gsd/audit-milestone.md +6 -1
  37. package/commands/gsd/autonomous.md +5 -0
  38. package/commands/gsd/capture.md +8 -4
  39. package/commands/gsd/code-review.md +7 -2
  40. package/commands/gsd/complete-milestone.md +4 -0
  41. package/commands/gsd/config.md +7 -3
  42. package/commands/gsd/debug.md +11 -7
  43. package/commands/gsd/discuss-phase.md +7 -3
  44. package/commands/gsd/docs-update.md +12 -7
  45. package/commands/gsd/eval-review.md +6 -1
  46. package/commands/gsd/execute-phase.md +12 -7
  47. package/commands/gsd/extract-learnings.md +5 -0
  48. package/commands/gsd/fast.md +4 -0
  49. package/commands/gsd/forensics.md +5 -1
  50. package/commands/gsd/graphify.md +10 -6
  51. package/commands/gsd/health.md +5 -0
  52. package/commands/gsd/help.md +7 -2
  53. package/commands/gsd/import.md +7 -3
  54. package/commands/gsd/inbox.md +5 -0
  55. package/commands/gsd/ingest-docs.md +5 -1
  56. package/commands/gsd/manager.md +6 -1
  57. package/commands/gsd/map-codebase.md +7 -3
  58. package/commands/gsd/mempalace-capture.md +5 -1
  59. package/commands/gsd/mempalace-recall.md +5 -1
  60. package/commands/gsd/milestone-summary.md +5 -1
  61. package/commands/gsd/mvp-phase.md +8 -3
  62. package/commands/gsd/new-milestone.md +6 -1
  63. package/commands/gsd/new-project.md +5 -0
  64. package/commands/gsd/next.md +6 -1
  65. package/commands/gsd/ns-context.md +4 -0
  66. package/commands/gsd/ns-ideate.md +4 -0
  67. package/commands/gsd/ns-manage.md +4 -0
  68. package/commands/gsd/ns-project.md +4 -0
  69. package/commands/gsd/ns-review.md +4 -0
  70. package/commands/gsd/ns-workflow.md +4 -0
  71. package/commands/gsd/onboard.md +6 -1
  72. package/commands/gsd/pause-work.md +5 -1
  73. package/commands/gsd/phase.md +8 -4
  74. package/commands/gsd/plan-phase.md +6 -1
  75. package/commands/gsd/plan-review-convergence.md +5 -1
  76. package/commands/gsd/pr-branch.md +4 -0
  77. package/commands/gsd/profile-user.md +5 -1
  78. package/commands/gsd/progress.md +6 -1
  79. package/commands/gsd/quick-batch.md +20 -8
  80. package/commands/gsd/quick.md +12 -7
  81. package/commands/gsd/review.md +5 -1
  82. package/commands/gsd/secure-phase.md +6 -1
  83. package/commands/gsd/ship.md +5 -0
  84. package/commands/gsd/sketch.md +7 -2
  85. package/commands/gsd/spec-phase.md +5 -1
  86. package/commands/gsd/spike.md +8 -3
  87. package/commands/gsd/surface.md +5 -1
  88. package/commands/gsd/thread.md +4 -0
  89. package/commands/gsd/ui-phase.md +6 -1
  90. package/commands/gsd/ui-review.md +6 -1
  91. package/commands/gsd/ultraplan-phase.md +5 -1
  92. package/commands/gsd/undo.md +5 -1
  93. package/commands/gsd/update.md +6 -2
  94. package/commands/gsd/validate-phase.md +6 -1
  95. package/commands/gsd/verify-work.md +6 -1
  96. package/commands/gsd/workspace.md +7 -3
  97. package/gsd-core/bin/gsd-tools.cjs +159 -58
  98. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  99. package/gsd-core/bin/lib/adr-parser.cjs +37 -5
  100. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  101. package/gsd-core/bin/lib/audit.cjs +78 -44
  102. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  103. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  104. package/gsd-core/bin/lib/capability-registry.cjs +249 -117
  105. package/gsd-core/bin/lib/capability-validator.cjs +18 -1
  106. package/gsd-core/bin/lib/capability-writer.cjs +2 -1
  107. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  108. package/gsd-core/bin/lib/check-command-router.cjs +164 -1712
  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 +11 -8
  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 +201 -178
  115. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  116. package/gsd-core/bin/lib/config.cjs +43 -12
  117. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  118. package/gsd-core/bin/lib/coverage.cjs +4 -8
  119. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  120. package/gsd-core/bin/lib/drift.cjs +177 -42
  121. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  122. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  123. package/gsd-core/bin/lib/frontmatter.cjs +413 -234
  124. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  125. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  126. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  127. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  128. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  129. package/gsd-core/bin/lib/gate-context-drift.cjs +142 -0
  130. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  131. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  132. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  133. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  134. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  135. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  136. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  137. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  138. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  139. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  140. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  141. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  142. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  143. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  144. package/gsd-core/bin/lib/gate-ui-safety.cjs +81 -0
  145. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  146. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  147. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  148. package/gsd-core/bin/lib/graphify.cjs +3 -2
  149. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  150. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  151. package/gsd-core/bin/lib/init.cjs +194 -108
  152. package/gsd-core/bin/lib/install-engine.cjs +30 -35
  153. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  154. package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
  155. package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
  156. package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
  157. package/gsd-core/bin/lib/io.cjs +122 -3
  158. package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
  159. package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
  160. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  161. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  162. package/gsd-core/bin/lib/mcp-server.cjs +5 -1
  163. package/gsd-core/bin/lib/milestone.cjs +19 -1
  164. package/gsd-core/bin/lib/model-resolver.cjs +18 -18
  165. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  166. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  167. package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
  168. package/gsd-core/bin/lib/opencode-version.cjs +486 -0
  169. package/gsd-core/bin/lib/pattern.cjs +10 -0
  170. package/gsd-core/bin/lib/phase-command-router.cjs +11 -4
  171. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  172. package/gsd-core/bin/lib/phase-id.cjs +1 -1
  173. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  174. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  175. package/gsd-core/bin/lib/phase.cjs +301 -80
  176. package/gsd-core/bin/lib/plan-document.cjs +93 -19
  177. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  178. package/gsd-core/bin/lib/planning-document.cjs +273 -40
  179. package/gsd-core/bin/lib/planning-inspect.cjs +42 -8
  180. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  181. package/gsd-core/bin/lib/planning-workspace.cjs +76 -53
  182. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  183. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  184. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  185. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  186. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +13 -9
  187. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  188. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  189. package/gsd-core/bin/lib/roadmap-command-router.cjs +13 -15
  190. package/gsd-core/bin/lib/roadmap-parser.cjs +82 -15
  191. package/gsd-core/bin/lib/roadmap-upgrade.cjs +127 -65
  192. package/gsd-core/bin/lib/roadmap.cjs +169 -72
  193. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +186 -161
  194. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -1
  195. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  196. package/gsd-core/bin/lib/runtime-homes.cjs +13 -10
  197. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +365 -594
  198. package/gsd-core/bin/lib/runtime-name-policy.cjs +142 -27
  199. package/gsd-core/bin/lib/runtime-slash.cjs +60 -30
  200. package/gsd-core/bin/lib/shell-command-projection.cjs +37 -2
  201. package/gsd-core/bin/lib/smart-entry.cjs +111 -56
  202. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  203. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  204. package/gsd-core/bin/lib/state-document.cjs +100 -22
  205. package/gsd-core/bin/lib/state-transition.cjs +67 -9
  206. package/gsd-core/bin/lib/state.cjs +668 -171
  207. package/gsd-core/bin/lib/surface.cjs +2 -1
  208. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -152
  209. package/gsd-core/bin/lib/uat-predicate.cjs +313 -35
  210. package/gsd-core/bin/lib/uat.cjs +424 -7
  211. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +94 -58
  212. package/gsd-core/bin/lib/update-context.cjs +18 -1
  213. package/gsd-core/bin/lib/vendor/README.md +31 -9
  214. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  215. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  216. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  217. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  218. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  219. package/gsd-core/bin/lib/verification.cjs +1265 -324
  220. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  221. package/gsd-core/bin/lib/verify.cjs +286 -567
  222. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  223. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  224. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  225. package/gsd-core/bin/lib/workstream.cjs +11 -2
  226. package/gsd-core/bin/shared/config-defaults.manifest.json +5 -0
  227. package/gsd-core/bin/shared/config-schema.manifest.json +4 -0
  228. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  229. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  230. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  231. package/gsd-core/references/execute-phase-between-wave-reset.md +3 -0
  232. package/gsd-core/references/execute-phase-response-language.md +1 -1
  233. package/gsd-core/references/gsd-run-resolver.md +1 -1
  234. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  235. package/gsd-core/references/offer-next.md +1 -1
  236. package/gsd-core/references/planning-config.md +1 -1
  237. package/gsd-core/references/spidr-splitting.md +1 -1
  238. package/gsd-core/references/tdd.md +10 -4
  239. package/gsd-core/references/verifier-phase-gates.md +5 -2
  240. package/gsd-core/references/verify-mvp-mode.md +2 -2
  241. package/gsd-core/references/workstream-flag.md +33 -3
  242. package/gsd-core/templates/README.md +1 -1
  243. package/gsd-core/templates/UAT.md +17 -1
  244. package/gsd-core/templates/config.json +2 -11
  245. package/gsd-core/templates/verification-report.md +1 -1
  246. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  247. package/gsd-core/workflows/add-backlog.md +1 -1
  248. package/gsd-core/workflows/add-phase.md +8 -7
  249. package/gsd-core/workflows/add-tests.md +3 -2
  250. package/gsd-core/workflows/add-todo.md +4 -3
  251. package/gsd-core/workflows/ai-integration-phase.md +3 -2
  252. package/gsd-core/workflows/audit-fix.md +1 -1
  253. package/gsd-core/workflows/audit-milestone.md +4 -3
  254. package/gsd-core/workflows/audit-uat.md +1 -1
  255. package/gsd-core/workflows/autonomous.md +28 -18
  256. package/gsd-core/workflows/check-todos.md +6 -5
  257. package/gsd-core/workflows/cleanup.md +1 -1
  258. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  259. package/gsd-core/workflows/code-review-fix.md +5 -4
  260. package/gsd-core/workflows/code-review.md +100 -91
  261. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  262. package/gsd-core/workflows/complete-milestone.md +12 -7
  263. package/gsd-core/workflows/debug.md +30 -5
  264. package/gsd-core/workflows/diagnose-issues.md +3 -2
  265. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  266. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  267. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  268. package/gsd-core/workflows/discuss-phase.md +4 -3
  269. package/gsd-core/workflows/do.md +1 -1
  270. package/gsd-core/workflows/docs-update.md +4 -3
  271. package/gsd-core/workflows/edit-phase.md +4 -3
  272. package/gsd-core/workflows/eval-review.md +5 -3
  273. package/gsd-core/workflows/execute-phase/detail/elaboration.md +1 -1
  274. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +4 -2
  275. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  276. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  277. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +2 -2
  278. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  279. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  280. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  281. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  282. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  283. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  284. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  285. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  286. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  287. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  288. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  289. package/gsd-core/workflows/execute-phase.md +81 -171
  290. package/gsd-core/workflows/execute-plan.md +13 -19
  291. package/gsd-core/workflows/explore.md +2 -2
  292. package/gsd-core/workflows/extract-learnings.md +3 -2
  293. package/gsd-core/workflows/fast.md +1 -1
  294. package/gsd-core/workflows/forensics.md +1 -1
  295. package/gsd-core/workflows/graduation.md +1 -1
  296. package/gsd-core/workflows/health.md +2 -1
  297. package/gsd-core/workflows/import.md +3 -2
  298. package/gsd-core/workflows/inbox.md +1 -1
  299. package/gsd-core/workflows/ingest-docs.md +1 -1
  300. package/gsd-core/workflows/insert-phase.md +4 -3
  301. package/gsd-core/workflows/list-seeds.md +1 -1
  302. package/gsd-core/workflows/list-workspaces.md +1 -1
  303. package/gsd-core/workflows/manager.md +5 -3
  304. package/gsd-core/workflows/map-codebase.md +4 -3
  305. package/gsd-core/workflows/milestone-summary.md +3 -2
  306. package/gsd-core/workflows/mvp-phase.md +14 -14
  307. package/gsd-core/workflows/new-milestone.md +10 -10
  308. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  309. package/gsd-core/workflows/new-project.md +1 -1
  310. package/gsd-core/workflows/new-workspace.md +1 -1
  311. package/gsd-core/workflows/next.md +1 -1
  312. package/gsd-core/workflows/pause-work.md +2 -2
  313. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  314. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  315. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  316. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  317. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +1 -1
  318. package/gsd-core/workflows/plan-phase.md +27 -18
  319. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  320. package/gsd-core/workflows/plant-seed.md +1 -1
  321. package/gsd-core/workflows/pr-branch.md +1 -1
  322. package/gsd-core/workflows/profile-user.md +1 -1
  323. package/gsd-core/workflows/progress.md +19 -49
  324. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -2
  325. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  326. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  327. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  328. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  329. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  330. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  331. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  332. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  333. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  334. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  335. package/gsd-core/workflows/quick-batch.md +15 -10
  336. package/gsd-core/workflows/quick.md +42 -31
  337. package/gsd-core/workflows/remove-phase.md +3 -2
  338. package/gsd-core/workflows/remove-workspace.md +1 -1
  339. package/gsd-core/workflows/resume-project.md +17 -12
  340. package/gsd-core/workflows/review.md +3 -2
  341. package/gsd-core/workflows/scan.md +3 -2
  342. package/gsd-core/workflows/secure-phase.md +11 -12
  343. package/gsd-core/workflows/settings-advanced.md +1 -1
  344. package/gsd-core/workflows/settings-integrations.md +1 -1
  345. package/gsd-core/workflows/settings.md +1 -1
  346. package/gsd-core/workflows/ship.md +12 -12
  347. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  348. package/gsd-core/workflows/sketch.md +1 -1
  349. package/gsd-core/workflows/smart-entry.md +1 -1
  350. package/gsd-core/workflows/spec-phase.md +1 -1
  351. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  352. package/gsd-core/workflows/spike.md +1 -1
  353. package/gsd-core/workflows/stats.md +1 -1
  354. package/gsd-core/workflows/sync-skills.md +1 -1
  355. package/gsd-core/workflows/thread.md +2 -2
  356. package/gsd-core/workflows/transition.md +13 -23
  357. package/gsd-core/workflows/ui-phase.md +5 -4
  358. package/gsd-core/workflows/ui-review.md +4 -3
  359. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  360. package/gsd-core/workflows/undo.md +1 -1
  361. package/gsd-core/workflows/validate-phase.md +10 -12
  362. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  363. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  364. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  365. package/gsd-core/workflows/verify-work.md +68 -59
  366. package/hooks/dist/gsd-agent-isolation-guard.js +8 -0
  367. package/hooks/dist/gsd-check-update-worker.js +8 -0
  368. package/hooks/dist/gsd-check-update.js +34 -20
  369. package/hooks/dist/gsd-context-monitor.js +45 -13
  370. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  371. package/hooks/dist/gsd-secret-read-guard.js +263 -16
  372. package/hooks/dist/gsd-statusline.js +336 -29
  373. package/hooks/dist/gsd-update-banner.js +8 -0
  374. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  375. package/hooks/dist/gsd-workflow-guard.js +5 -4
  376. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  377. package/hooks/dist/lib/cli-exit.js +12 -0
  378. package/hooks/dist/lib/git-probe.js +17 -1
  379. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  380. package/hooks/gsd-agent-isolation-guard.js +8 -0
  381. package/hooks/gsd-check-update-worker.js +8 -0
  382. package/hooks/gsd-check-update.js +34 -20
  383. package/hooks/gsd-context-monitor.js +45 -13
  384. package/hooks/gsd-cursor-subagent-start.js +8 -0
  385. package/hooks/gsd-secret-read-guard.js +263 -16
  386. package/hooks/gsd-statusline.js +336 -29
  387. package/hooks/gsd-update-banner.js +8 -0
  388. package/hooks/gsd-windsurf-pre-write.js +11 -2
  389. package/hooks/gsd-workflow-guard.js +5 -4
  390. package/hooks/gsd-worktree-path-guard.js +6 -2
  391. package/hooks/hooks.json +5 -5
  392. package/hooks/lib/cli-exit.js +12 -0
  393. package/hooks/lib/git-probe.js +17 -1
  394. package/hooks/lib/isolation-sentinel.js +2 -2
  395. package/package.json +21 -4
  396. package/scripts/changeset/parse.cjs +52 -4
  397. package/scripts/ci-timeout-report.cjs +770 -4
  398. package/scripts/command-contract-helpers.cjs +12 -8
  399. package/scripts/docs-guard-registry.cjs +6 -0
  400. package/scripts/gen-capability-registry.cjs +7 -1
  401. package/scripts/gen-features.cjs +13 -8
  402. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  403. package/scripts/gen-loop-host-contract.cjs +14 -2
  404. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  405. package/scripts/gen-plugin-skills.cjs +87 -1
  406. package/scripts/gen-research-agents.cjs +24 -31
  407. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  408. package/scripts/gen-test-timings.cjs +32 -7
  409. package/scripts/lib/cli-exit.cjs +12 -0
  410. package/scripts/lib/macos-conformance-tier.generated.cjs +24 -2
  411. package/scripts/lib/ndjson-reporter.cjs +28 -3
  412. package/scripts/lib/platform-conformance-tier.generated.cjs +44 -5
  413. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  414. package/scripts/lib/vendor-bundle.cjs +59 -0
  415. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  416. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  417. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  418. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  419. package/scripts/lint-descriptions.cjs +7 -3
  420. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +10 -0
  421. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  422. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  423. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  424. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  425. package/scripts/lint-phase-id-drift.cjs +29 -0
  426. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  427. package/scripts/lint-response-language-coverage.cjs +3 -0
  428. package/scripts/lint-skill-deps.cjs +7 -3
  429. package/scripts/lint-test-file-count.allowlist.json +11 -1
  430. package/scripts/lint-test-file-count.cjs +34 -1
  431. package/scripts/lint-vendored-deps.cjs +41 -5
  432. package/scripts/lint-workflow-shellcheck-baseline.json +10 -10
  433. package/scripts/mutation-matrix.cjs +53 -6
  434. package/scripts/release-tarball-smoke.cjs +14 -7
  435. package/scripts/require-issue-link-policy.cjs +6 -2
  436. package/scripts/sync-runtime-launcher.cjs +184 -2
  437. package/scripts/verify-npm-publish.cjs +76 -20
  438. package/skills/gsd-add-tests/SKILL.md +6 -1
  439. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  440. package/skills/gsd-audit-fix/SKILL.md +5 -0
  441. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  442. package/skills/gsd-autonomous/SKILL.md +5 -0
  443. package/skills/gsd-capture/SKILL.md +8 -4
  444. package/skills/gsd-code-review/SKILL.md +7 -2
  445. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  446. package/skills/gsd-config/SKILL.md +8 -4
  447. package/skills/gsd-debug/SKILL.md +11 -7
  448. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  449. package/skills/gsd-docs-update/SKILL.md +12 -7
  450. package/skills/gsd-eval-review/SKILL.md +6 -1
  451. package/skills/gsd-execute-phase/SKILL.md +12 -7
  452. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  453. package/skills/gsd-fast/SKILL.md +4 -0
  454. package/skills/gsd-forensics/SKILL.md +5 -1
  455. package/skills/gsd-graphify/SKILL.md +10 -6
  456. package/skills/gsd-health/SKILL.md +5 -0
  457. package/skills/gsd-help/SKILL.md +7 -2
  458. package/skills/gsd-import/SKILL.md +7 -3
  459. package/skills/gsd-inbox/SKILL.md +5 -0
  460. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  461. package/skills/gsd-manager/SKILL.md +6 -1
  462. package/skills/gsd-map-codebase/SKILL.md +7 -3
  463. package/skills/gsd-mempalace-capture/SKILL.md +5 -1
  464. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  465. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  466. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  467. package/skills/gsd-new-milestone/SKILL.md +6 -1
  468. package/skills/gsd-new-project/SKILL.md +5 -0
  469. package/skills/gsd-next/SKILL.md +6 -1
  470. package/skills/gsd-ns-context/SKILL.md +4 -0
  471. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  472. package/skills/gsd-ns-manage/SKILL.md +4 -0
  473. package/skills/gsd-ns-project/SKILL.md +4 -0
  474. package/skills/gsd-ns-review/SKILL.md +4 -0
  475. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  476. package/skills/gsd-onboard/SKILL.md +6 -1
  477. package/skills/gsd-pause-work/SKILL.md +5 -1
  478. package/skills/gsd-phase/SKILL.md +8 -4
  479. package/skills/gsd-plan-phase/SKILL.md +6 -1
  480. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  481. package/skills/gsd-pr-branch/SKILL.md +4 -0
  482. package/skills/gsd-profile-user/SKILL.md +5 -1
  483. package/skills/gsd-progress/SKILL.md +6 -1
  484. package/skills/gsd-quick/SKILL.md +16 -10
  485. package/skills/gsd-quick-batch/SKILL.md +20 -8
  486. package/skills/gsd-review/SKILL.md +5 -1
  487. package/skills/gsd-review-backlog/SKILL.md +3 -2
  488. package/skills/gsd-secure-phase/SKILL.md +6 -1
  489. package/skills/gsd-ship/SKILL.md +5 -0
  490. package/skills/gsd-sketch/SKILL.md +7 -2
  491. package/skills/gsd-spec-phase/SKILL.md +5 -1
  492. package/skills/gsd-spike/SKILL.md +8 -3
  493. package/skills/gsd-surface/SKILL.md +5 -1
  494. package/skills/gsd-thread/SKILL.md +4 -0
  495. package/skills/gsd-ui-phase/SKILL.md +6 -1
  496. package/skills/gsd-ui-review/SKILL.md +6 -1
  497. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  498. package/skills/gsd-undo/SKILL.md +5 -1
  499. package/skills/gsd-update/SKILL.md +6 -2
  500. package/skills/gsd-validate-phase/SKILL.md +6 -1
  501. package/skills/gsd-verify-work/SKILL.md +6 -1
  502. package/skills/gsd-workspace/SKILL.md +7 -3
  503. package/skills/gsd-workstreams/SKILL.md +6 -6
  504. package/vscode/package.json +1 -1
  505. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +0 -24
@@ -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");
@@ -62,7 +63,7 @@ const planningWorkspace = require("./planning-workspace.cjs");
62
63
  const { planningDir, planningPaths, todosDir, resolvePhaseIdConvention } = planningWorkspace;
63
64
  // eslint-disable-next-line @typescript-eslint/no-require-imports
64
65
  const frontmatter = require("./frontmatter.cjs");
65
- const { extractFrontmatter, agentScalarNeedsDoubleQuoting, escapeDoubleQuotedScalar } = frontmatter;
66
+ const { extractFrontmatter, agentScalarNeedsDoubleQuoting, escapeDoubleQuotedScalar, FRONTMATTER_UNPARSEABLE } = frontmatter;
66
67
  // eslint-disable-next-line @typescript-eslint/no-require-imports
67
68
  const modelProfiles = require("./model-profiles.cjs");
68
69
  const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
@@ -72,9 +73,40 @@ const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
72
73
  // eslint-disable-next-line @typescript-eslint/no-require-imports
73
74
  const planScanMod = require("./plan-scan.cjs");
74
75
  const { scanPhasePlans } = planScanMod;
75
- // 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
76
82
  const verificationMod = require("./verification.cjs");
77
- const { resolveVerificationFile } = verificationMod;
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
+ }
78
110
  /**
79
111
  * Project one canonical bracket directory onto its display identity and slug.
80
112
  *
@@ -101,94 +133,6 @@ function recoverBracketPhaseName(dir, phaseToken) {
101
133
  const afterToken = dir.slice(tokenOffset + tokenBoundary.length).replace(/^-/, '');
102
134
  return afterToken ? afterToken.replace(/-/g, ' ') : '';
103
135
  }
104
- // ─── Phase Status ─────────────────────────────────────────────────────────────
105
- /**
106
- * Phase-status precedence ladder — furthest-along wins (#2408).
107
- *
108
- * `cmdStats` builds `phasesByNumber` by scanning on-disk phase directories.
109
- * When two directories normalize to the same phase key (e.g. `05-real/` and
110
- * `05-real-stray/`), the status field must be folded by precedence rather
111
- * than overwritten last-write-wins — otherwise `/gsd-stats` reports whatever
112
- * directory `fs.readdirSync` happened to yield last, which is non-deterministic
113
- * across platforms and can silently call a `Complete` phase `Not Started`.
114
- */
115
- const PHASE_STATUS_PRECEDENCE = [
116
- 'Complete',
117
- 'Needs Review',
118
- 'Executed',
119
- 'In Progress',
120
- 'Planned',
121
- 'Not Started',
122
- 'Pending',
123
- ];
124
- const PHASE_STATUS_RANK = new Map(PHASE_STATUS_PRECEDENCE.map((s, i) => [s, i]));
125
- /**
126
- * Fold two phase statuses by precedence — returns whichever is further along
127
- * the {@link PHASE_STATUS_PRECEDENCE} ladder. Unrecognized statuses fall behind
128
- * every recognized one (so a recognized status always wins over an unknown one;
129
- * two unrecognized statuses favor `a` for determinism).
130
- */
131
- function foldPhaseStatus(a, b) {
132
- const ra = PHASE_STATUS_RANK.get(a);
133
- const rb = PHASE_STATUS_RANK.get(b);
134
- if (ra === undefined && rb === undefined)
135
- return a;
136
- if (ra === undefined)
137
- return b;
138
- if (rb === undefined)
139
- return a;
140
- // Lower rank = higher precedence (Complete=0 wins over Not Started=5).
141
- return ra <= rb ? a : b;
142
- }
143
- /**
144
- * Determine phase status by checking plan/summary counts AND verification state.
145
- * Introduces "Executed" for phases with all summaries but no passing verification.
146
- */
147
- function determinePhaseStatus(plans, summaries, phaseDir, defaultPending) {
148
- if (plans === 0)
149
- return defaultPending;
150
- if (summaries < plans && summaries > 0)
151
- return 'In Progress';
152
- if (summaries < plans)
153
- return 'Planned';
154
- // summaries >= plans — check verification
155
- try {
156
- const files = node_fs_1.default.readdirSync(phaseDir);
157
- // #3473 F2: routed through the shared resolver (readdir order is
158
- // filesystem-dependent, so the prior hand-rolled `.find()` could pick
159
- // either file when a phase held both a canonical report and an ad-hoc
160
- // `-CORRECTION-VERIFICATION.md` worksheet — see #3357).
161
- // #3492: pin selection to THIS phase's own token so a stray cross-phase
162
- // or sentinel-numbered canonically-shaped file cannot outrank this
163
- // phase's own (possibly non-canonical) report.
164
- const phaseDirName = node_path_1.default.basename(phaseDir);
165
- const phaseToken = extractPhaseToken(phaseDirName);
166
- const verificationFile = resolveVerificationFile(files, { allowBare: true, phaseToken, phaseDirName });
167
- if (verificationFile) {
168
- const verificationFilePath = node_path_1.default.join(phaseDir, verificationFile);
169
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(verificationFilePath) || '';
170
- // #1159 (Defect A): read ONLY the frontmatter `status` key to avoid false
171
- // matches from historical body metadata such as `previous_status: gaps_found`.
172
- // Full-text regexes like /status:\s*gaps_found/ match the substring inside
173
- // `previous_status: gaps_found`, producing incorrect phase status labels.
174
- const fm = extractFrontmatter(content, verificationFilePath);
175
- // Normalise to lower-case to preserve the prior case-insensitive behaviour
176
- // while reading only the frontmatter `status` key (not the full body text).
177
- const fmStatus = typeof fm['status'] === 'string' ? fm['status'].trim().toLowerCase() : '';
178
- if (fmStatus === 'passed')
179
- return 'Complete';
180
- if (fmStatus === 'human_needed')
181
- return 'Needs Review';
182
- if (fmStatus === 'gaps_found')
183
- return 'Executed';
184
- // Verification exists but unrecognized status — treat as executed
185
- return 'Executed';
186
- }
187
- }
188
- catch { /* directory read failed — fall through */ }
189
- // No verification file — executed but not verified
190
- return 'Executed';
191
- }
192
136
  function cmdGenerateSlug(text, raw) {
193
137
  if (!text) {
194
138
  error('text required for slug generation');
@@ -602,19 +546,19 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
602
546
  let effortEffective = effort;
603
547
  if (runtime === 'claude') {
604
548
  try {
605
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
606
- const { getGlobalConfigDir } = require('./runtime-homes.cjs');
607
- const agentsDirEff = node_path_1.default.join(getGlobalConfigDir(runtime), 'agents');
549
+ const agentsDirEff = claudeEffortAgentsDir();
608
550
  // agentType is an unvalidated CLI positional: keep the read inside the
609
551
  // agents dir so `../../x` cannot point it elsewhere (defense in depth —
610
552
  // the reflected surface is only a frontmatter effort line). Untrusted
611
553
  // input feeding a real read → realpath family (ADR-4650 decision 6).
612
554
  const agentPath = (0, security_cjs_1.assertWithinRoot)(`${agentType}.md`, agentsDirEff, 'agent file');
613
555
  const agentContent = node_fs_1.default.readFileSync(agentPath, 'utf8');
614
- // eslint-disable-next-line local/no-unbounded-quantifier -- same lazy `*?` bounded by the `^---$/m` closing anchor as the sibling frontmatter regexes in this file
615
- const fmMatchEff = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(agentContent);
616
- if (fmMatchEff) {
617
- 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);
618
562
  if (effortLine) {
619
563
  effortEffective = effortLine[1];
620
564
  effortEffectiveSource = 'frontmatter';
@@ -701,33 +645,42 @@ function effortSurfaceForHost(cwd, host) {
701
645
  return 'none';
702
646
  }
703
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
+ }
704
663
  /**
705
664
  * #488 — Replace or inject the `<key>:` value in YAML frontmatter.
706
665
  * Unlike injectEffortFrontmatter (install.js), this overwrites an existing value.
707
666
  * #3706: key-parameterised so the same line-editor serves both claude's
708
- * `effort:` and OpenCode's `variant:`. #3706: all offsets (eol, openLen,
709
- * closingStart) are derived from the MATCHED BLOCK, not the start of the
710
- * 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.
711
670
  */
712
671
  function setFrontmatterKeyLine(content, key, value) {
713
- const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
714
- const match = fmRe.exec(content);
715
- if (!match)
672
+ const span = agentFrontmatterSpan(content, EFFORT_SYNC_FENCE);
673
+ if (!span)
716
674
  return content;
717
- const fmBody = match[1];
675
+ const fmBody = span.body;
718
676
  // Both writers of these frontmatter keys — this sync path and the
719
677
  // install-side `frontmatterScalar` in runtime-artifact-conversion.cts —
720
678
  // now share one escaping rule: quote via `agentScalarNeedsDoubleQuoting` +
721
679
  // `escapeDoubleQuotedScalar` (both from frontmatter.cts) rather than each
722
680
  // interpolating `value` raw/differently.
723
681
  const renderedValue = agentScalarNeedsDoubleQuoting(value) ? `"${escapeDoubleQuotedScalar(value)}"` : value;
724
- // EOL comes from the MATCHED BLOCK, not the start of the file. With a
725
- // preamble the two can disagree, and on a CRLF document that misaligns every
726
- // offset below by one byte and mangles the opening fence.
727
- const eol = /^---\r\n/.test(match[0]) ? '\r\n' : '\n';
728
- const openLen = 3 + eol.length;
729
- const bodyStart = match.index + openLen;
730
- 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;
731
684
  // #3706: key is now generic (not just the literal 'effort'/'variant'
732
685
  // callers happen to pass today) — escape it before interpolating into the
733
686
  // RegExp so a future caller can't have its key metacharacters reinterpreted.
@@ -763,19 +716,17 @@ function setFrontmatterKeyLine(content, key, value) {
763
716
  * codex-agent-toml strip discipline: targeted line removal, EOL-aware, every
764
717
  * other byte (comments, sibling keys, the body) untouched.
765
718
  * #3706: key-parameterised so the same line-editor serves both claude's
766
- * `effort:` and OpenCode's `variant:`. #3706: openLen is derived from the
767
- * MATCHED BLOCK, not the start of the file — a preamble on a CRLF document
768
- * would otherwise misalign every offset below.
719
+ * `effort:` and OpenCode's `variant:`. #3706: every offset comes from the
720
+ * block `agentFrontmatterSpan` found.
769
721
  */
770
722
  function removeFrontmatterKeyLine(content, key) {
771
723
  // Scoped to the FIRST frontmatter block (not a whole-file /m match): a
772
724
  // preamble or body line starting with `<key>:` (a fenced config example,
773
725
  // a thematic-break flanked fragment) must never be the line removed.
774
- const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
775
- const match = fmRe.exec(content);
776
- if (!match)
726
+ const span = agentFrontmatterSpan(content, EFFORT_SYNC_FENCE);
727
+ if (!span)
777
728
  return content;
778
- const fmBody = match[1];
729
+ const fmBody = span.body;
779
730
  // #3706: same generic-key escape as setFrontmatterKeyLine above.
780
731
  const lineRe = new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}:[ \\t]*.*\\r?\\n?`, 'm');
781
732
  if (!lineRe.test(fmBody))
@@ -790,12 +741,7 @@ function removeFrontmatterKeyLine(content, key) {
790
741
  // is removed in one pass.
791
742
  const stripAllRe = new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}:[ \\t]*.*\\r?\\n?`, 'gm');
792
743
  const strippedFm = fmBody.replace(stripAllRe, '');
793
- // Same rule as setFrontmatterKeyLine: the EOL must come from the matched
794
- // block, not the start of the file, or a preambled CRLF document misaligns.
795
- const eol = /^---\r\n/.test(match[0]) ? '\r\n' : '\n';
796
- const openLen = 3 + eol.length;
797
- const closingStart = match.index + openLen + fmBody.length;
798
- return content.slice(0, match.index + openLen) + strippedFm + content.slice(closingStart);
744
+ return content.slice(0, span.bodyStart) + strippedFm + content.slice(span.closingStart);
799
745
  }
800
746
  /** #488 — Replace or inject the `effort:` value in YAML frontmatter. */
801
747
  function setEffortFrontmatter(content, effortValue) {
@@ -840,8 +786,6 @@ function cmdEffortSync(cwd, raw, opts) {
840
786
  output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, reason: `runtime '${runtime}' does not use effort: frontmatter` }, raw, '');
841
787
  return;
842
788
  }
843
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
844
- const { getGlobalConfigDir } = require('./runtime-homes.cjs');
845
789
  // Use install-time resolvers: they merge ~/.gsd/defaults.json with project config,
846
790
  // matching the exact logic used when agents were originally installed. #2071: these
847
791
  // live in the shipped sibling install-effort-resolver.cjs (extracted from the
@@ -849,7 +793,7 @@ function cmdEffortSync(cwd, raw, opts) {
849
793
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
850
794
  const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('./install-effort-resolver.cjs');
851
795
  const effortCfg = readGsdEffectiveEffortConfig(cwd);
852
- const agentsDir = node_path_1.default.join(opts.configDir || getGlobalConfigDir(runtime), 'agents');
796
+ const agentsDir = claudeEffortAgentsDir(opts.configDir);
853
797
  if (!node_fs_1.default.existsSync(agentsDir)) {
854
798
  output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, agents_dir: agentsDir, reason: 'agents directory not found' }, raw, '');
855
799
  return;
@@ -867,6 +811,10 @@ function cmdEffortSync(cwd, raw, opts) {
867
811
  }).sort(); // #3706: sorted like the codex and
868
812
  // opencode branches — readdir order is platform-dependent, so leaving it unsorted makes the
869
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
+ }
870
818
  const changes = [];
871
819
  let synced = 0;
872
820
  let skipped = 0;
@@ -911,7 +859,7 @@ function cmdEffortSync(cwd, raw, opts) {
911
859
  // drift and the sync re-added a hand-stripped key on every apply. A
912
860
  // present key under inherit is stripped, reported as {from, to: null}.
913
861
  if (universalEffort === 'inherit') {
914
- const fmMatchInherit = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
862
+ const fmMatchInherit = agentFrontmatterSpan(content, EFFORT_SYNC_FENCE);
915
863
  if (!fmMatchInherit) {
916
864
  skipped++;
917
865
  continue;
@@ -922,12 +870,12 @@ function cmdEffortSync(cwd, raw, opts) {
922
870
  // from a null value alone is wrong here — it would leave an
923
871
  // unresolvable `effort: null` key on disk forever. Test presence with
924
872
  // its own regex, and only compare values once presence is known.
925
- const effortPresentInherit = /^effort:/m.test(fmMatchInherit[1]);
873
+ const effortPresentInherit = /^effort:/m.test(fmMatchInherit.body);
926
874
  if (!effortPresentInherit) {
927
875
  skipped++;
928
876
  continue;
929
877
  }
930
- const effortMatchInherit = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatchInherit[1]);
878
+ const effortMatchInherit = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatchInherit.body);
931
879
  // `effortPresentInherit` is guaranteed true here (checked above), so a
932
880
  // failed value match means the key is present with an EMPTY value —
933
881
  // report `''`, not `null`, so "present-but-empty" is never conflated
@@ -986,7 +934,7 @@ function cmdEffortSync(cwd, raw, opts) {
986
934
  // codex's 'ultra' rejection can produce a null value).
987
935
  const rendered = (0, model_catalog_cjs_1.renderEffortForRuntime)(runtime, universalEffort);
988
936
  const newEffortValue = rendered.value;
989
- const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
937
+ const fmMatch = agentFrontmatterSpan(content, EFFORT_SYNC_FENCE);
990
938
  if (!fmMatch) {
991
939
  skipped++;
992
940
  continue;
@@ -999,8 +947,8 @@ function cmdEffortSync(cwd, raw, opts) {
999
947
  // never null on this path (guarded above), so an absent key already
1000
948
  // yields `currentEffort === null !== newEffortValue` without consulting
1001
949
  // presence separately.
1002
- const effortPresent = /^effort:/m.test(fmMatch[1]);
1003
- 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);
1004
952
  // `null` (key absent) and `''` (key present, value empty) are distinct
1005
953
  // states `effortPresent` deliberately disambiguates — collapsing both to
1006
954
  // `null` here would make the reported `from` lie about which case fired.
@@ -1273,7 +1221,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
1273
1221
  // exactly what install writes.
1274
1222
  const universal = effortCfg ? resolveInstallTimeEffort(effortCfg, agentName) : null;
1275
1223
  const target = universal ? clampEffortForHost('opencode', universal) : null;
1276
- const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
1224
+ const fmMatch = agentFrontmatterSpan(content, EFFORT_SYNC_FENCE);
1277
1225
  if (!fmMatch) {
1278
1226
  skipped++;
1279
1227
  continue;
@@ -1285,25 +1233,45 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
1285
1233
  // null — it would leave an unresolvable `variant: null` key on disk
1286
1234
  // forever. Test presence with its own regex, and only compare values
1287
1235
  // once presence is known.
1288
- const variantPresent = /^variant:/m.test(fmMatch[1]);
1289
- const variantMatch = /^variant:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch[1]);
1236
+ // Native OpenCode 2.x form (ADR-5331 D5): the effort rides on the model reference as
1237
+ // `provider/model#variant`, with no separate `variant:` key. When the agent's `model:` carries a
1238
+ // `#` suffix that suffix IS the effort, so the sync edits the model instead of adding a second,
1239
+ // conflicting `variant:` line.
1240
+ const modelLineMatch = /^model:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
1241
+ const modelValue = modelLineMatch ? modelLineMatch[1].replace(/\r$/, '').replace(/^(["'])(.*)\1$/, '$2') : null;
1242
+ const hashAt = modelValue === null ? -1 : modelValue.lastIndexOf('#');
1243
+ let modelFormContent = null;
1244
+ let modelFormFrom = null;
1245
+ if (modelValue !== null && hashAt > 0) {
1246
+ const base = modelValue.slice(0, hashAt);
1247
+ modelFormFrom = modelValue.slice(hashAt + 1);
1248
+ if (target !== null && modelFormFrom === target) {
1249
+ skipped++;
1250
+ continue;
1251
+ }
1252
+ modelFormContent = setFrontmatterKeyLine(content, 'model', target === null ? base : `${base}#${target}`);
1253
+ }
1254
+ const variantPresent = /^variant:/m.test(fmMatch.body);
1255
+ const variantMatch = /^variant:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
1290
1256
  // `null` (key absent) and `''` (key present, value empty) are distinct
1291
1257
  // states this code deliberately tracks via `variantPresent` above — a
1292
1258
  // reported `from` that collapses both to `null` would make "no key" and
1293
1259
  // "empty key" indistinguishable in the sync output, even though only one
1294
1260
  // of them actually has a `variant:` line to remove.
1295
1261
  const currentVariant = variantPresent ? (variantMatch ? variantMatch[1] : '') : null;
1296
- if (target === null) {
1297
- if (!variantPresent) {
1262
+ if (modelFormContent === null) {
1263
+ if (target === null) {
1264
+ if (!variantPresent) {
1265
+ skipped++;
1266
+ continue;
1267
+ }
1268
+ }
1269
+ else if (variantPresent && currentVariant === target) {
1298
1270
  skipped++;
1299
1271
  continue;
1300
1272
  }
1301
1273
  }
1302
- else if (variantPresent && currentVariant === target) {
1303
- skipped++;
1304
- continue;
1305
- }
1306
- changes.push({ agent: agentName, from: currentVariant, to: target });
1274
+ changes.push({ agent: agentName, from: modelFormContent === null ? currentVariant : modelFormFrom, to: target });
1307
1275
  synced++;
1308
1276
  if (!dryRun) {
1309
1277
  // Atomic publish AND mode preservation, same discipline as
@@ -1331,7 +1299,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
1331
1299
  }
1332
1300
  catch { /* non-fatal: fall back to writing without an explicit mode */ }
1333
1301
  try {
1334
- node_fs_1.default.writeFileSync(tmpPath, target === null ? removeFrontmatterKeyLine(content, 'variant') : setFrontmatterKeyLine(content, 'variant', target), originalMode !== undefined ? { mode: originalMode } : undefined);
1302
+ node_fs_1.default.writeFileSync(tmpPath, modelFormContent ?? (target === null ? removeFrontmatterKeyLine(content, 'variant') : setFrontmatterKeyLine(content, 'variant', target)), originalMode !== undefined ? { mode: originalMode } : undefined);
1335
1303
  // Not redundant with the `mode` option above: `mode` only applies
1336
1304
  // when the file is actually created (O_CREAT). A leftover tmp file
1337
1305
  // from an earlier crashed run would be reused (truncated) at its OLD
@@ -2883,13 +2851,38 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
2883
2851
  if (!summaryPath) {
2884
2852
  error('summary-path required for summary-extract');
2885
2853
  }
2886
- const fullPath = node_path_1.default.join(cwd, summaryPath);
2854
+ // #5013: requireSafePath (not path.join) so an absolute summaryPath resolves to
2855
+ // that literal path instead of being concatenated onto cwd — path.join has no
2856
+ // special case for an absolute second argument the way path.resolve does. Uses
2857
+ // requireSafePath (the same PathAcceptance.AbsoluteInsideRoot pattern
2858
+ // cmdAuditAcknowledge's write-side counterpart already uses in audit.cts)
2859
+ // rather than a bare path.resolve: a plain path.resolve would also have made
2860
+ // an absolute summaryPath anywhere on the filesystem resolve and have its
2861
+ // content echoed back (this command reads and returns file content, unlike
2862
+ // cmdVerifyPathExists's exists-only check) — an orthogonal-review finding on
2863
+ // this same fix. requireSafePath keeps the acceptance criterion (an absolute
2864
+ // path within the project resolves correctly) while refusing one that
2865
+ // escapes cwd, and rejects relative `../` escapes the same way (pre-existing
2866
+ // in this function, closed as a direct consequence, not separately in scope).
2867
+ const fullPath = (0, security_cjs_1.requireSafePath)(summaryPath, cwd, 'summary-extract target', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
2887
2868
  if (!node_fs_1.default.existsSync(fullPath)) {
2888
2869
  output({ error: 'File not found', path: summaryPath }, raw, undefined);
2889
2870
  return;
2890
2871
  }
2891
2872
  const content = node_fs_1.default.readFileSync(fullPath, 'utf-8');
2892
2873
  const fm = extractFrontmatter(content, fullPath);
2874
+ // #5013: an unparseable frontmatter block (malformed YAML in the fence) is
2875
+ // NOT the same as an absent/empty one — extractFrontmatter marks it with
2876
+ // FRONTMATTER_UNPARSEABLE (mirrors state.cts's isUnparseableFrontmatter /
2877
+ // state-transition.cts's identical helper). Left unchecked, every field
2878
+ // below silently falls back to its empty default and this command exits 0,
2879
+ // so a milestone audit reading requirements_completed through it would
2880
+ // report "zero requirements completed" for a plan that completed several,
2881
+ // with nothing on stderr. Refuse instead, naming the file, the same way
2882
+ // cmdAuditAcknowledge (audit.cts) refuses on the write-side counterpart.
2883
+ if (fm[FRONTMATTER_UNPARSEABLE] === true) {
2884
+ 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 });
2885
+ }
2893
2886
  // Parse key-decisions into structured format
2894
2887
  const parseDecisions = (decisionsList) => {
2895
2888
  if (!decisionsList || !Array.isArray(decisionsList))
@@ -3060,6 +3053,8 @@ function cmdProgressRender(cwd, format, raw) {
3060
3053
  let totalPlans = 0;
3061
3054
  let totalSummaries = 0;
3062
3055
  let phaseScope = null;
3056
+ // #5118: carried out of the scan below and failed on after it.
3057
+ let statusError;
3063
3058
  try {
3064
3059
  // #3185 (ADR-3180 Decision 1): the single owner applies the milestone
3065
3060
  // window AND the sentinel filter and returns dirs already sorted by
@@ -3096,14 +3091,16 @@ function cmdProgressRender(cwd, format, raw) {
3096
3091
  phaseNum = dm ? dm[1] : dir;
3097
3092
  phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
3098
3093
  }
3099
- // #3183: canonical plan/summary counts (root+nested, superseded-excluded,
3100
- // canonical pairing) from the single owner.
3101
- const phaseScan = scanPhasePlans(node_path_1.default.join(phasesDir, dir));
3102
- const plans = phaseScan.planCount;
3103
- const summaries = phaseScan.summaryCount;
3094
+ // #3183/#5060: canonical plan/summary counts and lifecycle status
3095
+ // (root+nested, superseded-excluded, canonical pairing) from the
3096
+ // single owner (Phase Status Module, ADR-5057).
3097
+ const ps = (0, phase_status_cjs_1.phaseStatus)(node_path_1.default.join(phasesDir, dir), { convention: phaseIdConvention });
3098
+ statusError = firstStatusError(statusError, ps.value.statusError);
3099
+ const plans = ps.value.planCount;
3100
+ const summaries = ps.value.summaryCount;
3104
3101
  totalPlans += plans;
3105
3102
  totalSummaries += summaries;
3106
- const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Pending');
3103
+ const status = (0, phase_status_cjs_1.toDisplayLabel)(ps.value.status, { pendingWord: 'Pending' });
3107
3104
  phases.push({
3108
3105
  number: phaseNum,
3109
3106
  ...(displayId ? { display_id: displayId } : {}),
@@ -3115,6 +3112,9 @@ function cmdProgressRender(cwd, format, raw) {
3115
3112
  }
3116
3113
  }
3117
3114
  catch { /* intentionally empty */ }
3115
+ // #5118: a read-only aggregate over a refused report prints nothing.
3116
+ if (statusError)
3117
+ failOnVerificationStatusError(statusError);
3118
3118
  // #3217 (ADR-3180 §7.6 rule 4): `phaseScope` was already computed above
3119
3119
  // (Phase 3, #3222) but never consulted before rendering — a percentage was
3120
3120
  // rendered from counts the scope said were not answers (TRUNCATED /
@@ -3273,29 +3273,35 @@ function cmdTodoMatchPhase(cwd, phase, raw) {
3273
3273
  // #4096: upsert completion keys INSIDE the leading frontmatter block. Never a
3274
3274
  // bare prefix line above the opening `---` (that displaces the fence to line 2
3275
3275
  // and breaks every fence-locating reader). A file with no well-formed block
3276
- // (absent, or an unterminated opening fence) gains a complete block.
3276
+ // (absent, or an unterminated opening fence) gains a complete block. The block is
3277
+ // the one the one fence owner (`locateFrontmatterFence`) finds, so this writer and
3278
+ // every todo reader agree on where it is.
3277
3279
  function upsertTodoCompletionFields(content, today) {
3278
- const lines = content.split('\n');
3279
3280
  const fields = [`completed: ${today}`, 'status: completed'];
3280
- const hasOpeningFence = lines[0] !== undefined && lines[0].trim() === '---';
3281
- const closeIdx = hasOpeningFence ? lines.findIndex((l, i) => i > 0 && l.trim() === '---') : -1;
3282
- if (!hasOpeningFence || closeIdx === -1) {
3281
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content);
3282
+ if (!fence?.closed) {
3283
3283
  // No parseable frontmatter: wrap the whole content in a complete block
3284
- // rather than prefixing bare keys (#4096 fix 2).
3285
- return `---\n${fields.join('\n')}\n---\n\n${content}`;
3286
- }
3287
- const block = lines.slice(1, closeIdx);
3284
+ // rather than prefixing bare keys (#4096 fix 2). A leading BOM (#2977) stays
3285
+ // the document's first character, ahead of the new fence.
3286
+ const bom = content.charCodeAt(0) === 0xFEFF ? content.slice(0, 1) : '';
3287
+ return `${bom}---\n${fields.join('\n')}\n---\n\n${content.slice(bom.length)}`;
3288
+ }
3289
+ // The block's lines, split on a bare '\n' so a CRLF line keeps its '\r' — and a
3290
+ // written field line carries one too, so a CRLF block never gains LF-only lines.
3291
+ const inner = content.slice(fence.openEnd, fence.closingStart);
3292
+ const block = inner === '' ? [] : inner.slice(0, -1).split('\n');
3293
+ const cr = fence.eol === '\r\n' ? '\r' : '';
3288
3294
  for (const field of fields) {
3289
3295
  const key = `${field.slice(0, field.indexOf(':'))}:`;
3290
3296
  const idx = block.findIndex(l => l.startsWith(key));
3291
3297
  if (idx === -1) {
3292
- block.push(field);
3298
+ block.push(field + cr);
3293
3299
  }
3294
3300
  else {
3295
- block[idx] = field;
3301
+ block[idx] = field + cr;
3296
3302
  }
3297
3303
  }
3298
- return [...lines.slice(0, 1), ...block, ...lines.slice(closeIdx)].join('\n');
3304
+ return `${content.slice(0, fence.openEnd)}${block.join('\n')}\n${content.slice(fence.closingStart)}`;
3299
3305
  }
3300
3306
  function cmdTodoComplete(cwd, filename, options, raw) {
3301
3307
  if (!filename) {
@@ -3313,7 +3319,7 @@ function cmdTodoComplete(cwd, filename, options, raw) {
3313
3319
  // to a location inside todosRoot (or inside pending/) and would pass
3314
3320
  // containment, yet none of them is a bare filename. Reject on basename
3315
3321
  // shape FIRST, before any path is even joined — same predicate shape as
3316
- // findPhaseArtifact in check-command-router.cts. Checking both `/` and
3322
+ // findPhaseArtifact in gate-predicate.cts. Checking both `/` and
3317
3323
  // `\` explicitly (not just path.basename) matters on POSIX, where a
3318
3324
  // literal backslash is just an ordinary filename character to
3319
3325
  // path.basename but not to path.win32.basename or to the user's intent.
@@ -3448,6 +3454,9 @@ function cmdStats(cwd, format, raw) {
3448
3454
  : null;
3449
3455
  const milestoneVersion = milestoneDisplay ?? milestone?.version ?? null;
3450
3456
  // Phase & plan stats (reuse progress pattern)
3457
+ // #5060: `phaseStatus` here is the internal PHASE_STATUS ladder value, never
3458
+ // a display label — the output projection below is the only place a label
3459
+ // is produced (toDisplayLabel).
3451
3460
  const phasesByNumber = new Map();
3452
3461
  let totalPlans = 0;
3453
3462
  let totalSummaries = 0;
@@ -3505,11 +3514,14 @@ function cmdStats(cwd, format, raw) {
3505
3514
  name: phaseName.replace(/\(INSERTED\)/i, '').trim(),
3506
3515
  plans: 0,
3507
3516
  summaries: 0,
3508
- status: 'Not Started',
3517
+ phaseStatus: phase_status_cjs_1.PHASE_STATUS.NOT_STARTED,
3509
3518
  });
3510
3519
  }
3511
3520
  }
3512
3521
  catch { /* intentionally empty */ }
3522
+ // #5118: the first phase whose verification report `status` is outside the
3523
+ // closed set — carried out of the scan below and failed on after it.
3524
+ let statusError;
3513
3525
  try {
3514
3526
  // #3185 (ADR-3180 Decision 1): route through the single owner. This
3515
3527
  // previously applied the milestone window but NOT a directory-level
@@ -3546,14 +3558,15 @@ function cmdStats(cwd, format, raw) {
3546
3558
  const afterToken = dir.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
3547
3559
  phaseName = afterToken ? afterToken.replace(/-/g, ' ') : '';
3548
3560
  }
3549
- // #3183: canonical plan/summary counts (root+nested, superseded-excluded,
3550
- // canonical pairing) from the single owner.
3551
- const phaseScan = scanPhasePlans(node_path_1.default.join(phasesDir, dir));
3552
- const plans = phaseScan.planCount;
3553
- const summaries = phaseScan.summaryCount;
3561
+ // #3183/#5060: canonical plan/summary counts and lifecycle status
3562
+ // (root+nested, superseded-excluded, canonical pairing) from the
3563
+ // single owner (Phase Status Module, ADR-5057).
3564
+ const ps = (0, phase_status_cjs_1.phaseStatus)(node_path_1.default.join(phasesDir, dir), { convention: phaseIdConvention });
3565
+ statusError = firstStatusError(statusError, ps.value.statusError);
3566
+ const plans = ps.value.planCount;
3567
+ const summaries = ps.value.summaryCount;
3554
3568
  totalPlans += plans;
3555
3569
  totalSummaries += summaries;
3556
- const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Not Started');
3557
3570
  const normalizedNum = normalizePhaseName(phaseNum);
3558
3571
  const existing = phasesByNumber.get(normalizedNum);
3559
3572
  phasesByNumber.set(normalizedNum, {
@@ -3569,13 +3582,26 @@ function cmdStats(cwd, format, raw) {
3569
3582
  // platforms, so a naive overwrite can report a Complete phase as Not
3570
3583
  // Started (or vice versa) depending on read order. The fold picks the
3571
3584
  // furthest-along status, matching what an operator expects.
3572
- status: existing ? foldPhaseStatus(existing.status, status) : status,
3585
+ phaseStatus: existing ? (0, phase_status_cjs_1.foldPhaseStatuses)(existing.phaseStatus, ps.value.status) : ps.value.status,
3573
3586
  });
3574
3587
  }
3575
3588
  }
3576
3589
  catch { /* intentionally empty */ }
3577
- const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
3578
- const completedPhases = phases.filter(p => p.status === 'Complete').length;
3590
+ // #5118: a read-only aggregate over a refused report prints nothing.
3591
+ if (statusError)
3592
+ failOnVerificationStatusError(statusError);
3593
+ const internalPhases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
3594
+ const completedPhases = internalPhases.filter(p => p.phaseStatus === phase_status_cjs_1.PHASE_STATUS.COMPLETE).length;
3595
+ // #5060: project the internal PHASE_STATUS ladder to the display label at
3596
+ // the output boundary only — the internal `phaseStatus` field is never emitted.
3597
+ const phases = internalPhases.map(p => ({
3598
+ number: p.number,
3599
+ ...(p.display_id ? { display_id: p.display_id } : {}),
3600
+ name: p.name,
3601
+ plans: p.plans,
3602
+ summaries: p.summaries,
3603
+ status: (0, phase_status_cjs_1.toDisplayLabel)(p.phaseStatus, { pendingWord: 'Not Started' }),
3604
+ }));
3579
3605
  // #3217 (ADR-3180 §7.6 rule 4): both percentages here are derived from the
3580
3606
  // same `phaseScope`-carrying directory enumeration above (Phase 3, #3222) —
3581
3607
  // withhold both when that scope is not COMPLETE, same rationale as
@@ -3901,9 +3927,6 @@ function cmdCommitDocsGuardDisable(cwd, raw) {
3901
3927
  module.exports = {
3902
3928
  effortSurfaceForHost,
3903
3929
  groupFilesBySubrepo,
3904
- determinePhaseStatus,
3905
- foldPhaseStatus,
3906
- PHASE_STATUS_PRECEDENCE,
3907
3930
  cmdGenerateSlug,
3908
3931
  cmdCurrentTimestamp,
3909
3932
  cmdListTodos,