@opengsd/gsd-core 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (487) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +85 -5
  4. package/agents/gsd-code-fixer.compact.md +1 -1
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-code-reviewer.compact.md +5 -3
  7. package/agents/gsd-code-reviewer.md +8 -6
  8. package/agents/gsd-debug-session-manager.compact.md +1 -1
  9. package/agents/gsd-debug-session-manager.md +1 -1
  10. package/agents/gsd-debugger.md +2 -2
  11. package/agents/gsd-eval-auditor.compact.md +1 -1
  12. package/agents/gsd-eval-auditor.md +1 -1
  13. package/agents/gsd-executor.md +4 -4
  14. package/agents/gsd-intel-updater.compact.md +1 -1
  15. package/agents/gsd-intel-updater.md +1 -1
  16. package/agents/gsd-mempalace-curator.md +2 -2
  17. package/agents/gsd-phase-researcher.md +1 -1
  18. package/agents/gsd-plan-checker.md +7 -2
  19. package/agents/gsd-planner.md +3 -3
  20. package/agents/gsd-project-researcher.compact.md +1 -1
  21. package/agents/gsd-project-researcher.md +1 -1
  22. package/agents/gsd-research-synthesizer.compact.md +1 -1
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-auditor.compact.md +21 -30
  25. package/agents/gsd-ui-auditor.md +42 -51
  26. package/agents/gsd-ui-researcher.compact.md +1 -1
  27. package/agents/gsd-ui-researcher.md +1 -1
  28. package/agents/gsd-verifier.md +31 -9
  29. package/bin/install.js +126 -196
  30. package/commands/gsd/add-tests.md +6 -1
  31. package/commands/gsd/ai-integration-phase.md +6 -1
  32. package/commands/gsd/audit-fix.md +5 -0
  33. package/commands/gsd/audit-milestone.md +6 -1
  34. package/commands/gsd/autonomous.md +5 -0
  35. package/commands/gsd/capture.md +8 -4
  36. package/commands/gsd/code-review.md +7 -2
  37. package/commands/gsd/complete-milestone.md +4 -0
  38. package/commands/gsd/config.md +7 -3
  39. package/commands/gsd/debug.md +11 -7
  40. package/commands/gsd/discuss-phase.md +7 -3
  41. package/commands/gsd/docs-update.md +12 -7
  42. package/commands/gsd/eval-review.md +6 -1
  43. package/commands/gsd/execute-phase.md +12 -7
  44. package/commands/gsd/extract-learnings.md +5 -0
  45. package/commands/gsd/fast.md +4 -0
  46. package/commands/gsd/forensics.md +5 -1
  47. package/commands/gsd/graphify.md +10 -6
  48. package/commands/gsd/health.md +5 -0
  49. package/commands/gsd/help.md +7 -2
  50. package/commands/gsd/import.md +7 -3
  51. package/commands/gsd/inbox.md +5 -0
  52. package/commands/gsd/ingest-docs.md +5 -1
  53. package/commands/gsd/manager.md +6 -1
  54. package/commands/gsd/map-codebase.md +7 -3
  55. package/commands/gsd/mempalace-capture.md +5 -1
  56. package/commands/gsd/mempalace-recall.md +5 -1
  57. package/commands/gsd/milestone-summary.md +5 -1
  58. package/commands/gsd/mvp-phase.md +8 -3
  59. package/commands/gsd/new-milestone.md +6 -1
  60. package/commands/gsd/new-project.md +5 -0
  61. package/commands/gsd/next.md +6 -1
  62. package/commands/gsd/ns-context.md +4 -0
  63. package/commands/gsd/ns-ideate.md +4 -0
  64. package/commands/gsd/ns-manage.md +4 -0
  65. package/commands/gsd/ns-project.md +4 -0
  66. package/commands/gsd/ns-review.md +4 -0
  67. package/commands/gsd/ns-workflow.md +4 -0
  68. package/commands/gsd/onboard.md +6 -1
  69. package/commands/gsd/pause-work.md +5 -1
  70. package/commands/gsd/phase.md +8 -4
  71. package/commands/gsd/plan-phase.md +6 -1
  72. package/commands/gsd/plan-review-convergence.md +5 -1
  73. package/commands/gsd/pr-branch.md +4 -0
  74. package/commands/gsd/profile-user.md +5 -1
  75. package/commands/gsd/progress.md +6 -1
  76. package/commands/gsd/quick-batch.md +20 -8
  77. package/commands/gsd/quick.md +12 -7
  78. package/commands/gsd/review.md +5 -1
  79. package/commands/gsd/secure-phase.md +6 -1
  80. package/commands/gsd/ship.md +5 -0
  81. package/commands/gsd/sketch.md +7 -2
  82. package/commands/gsd/spec-phase.md +5 -1
  83. package/commands/gsd/spike.md +8 -3
  84. package/commands/gsd/surface.md +5 -1
  85. package/commands/gsd/thread.md +4 -0
  86. package/commands/gsd/ui-phase.md +6 -1
  87. package/commands/gsd/ui-review.md +6 -1
  88. package/commands/gsd/ultraplan-phase.md +5 -1
  89. package/commands/gsd/undo.md +5 -1
  90. package/commands/gsd/update.md +6 -2
  91. package/commands/gsd/validate-phase.md +6 -1
  92. package/commands/gsd/verify-work.md +6 -1
  93. package/commands/gsd/workspace.md +7 -3
  94. package/gsd-core/bin/gsd-tools.cjs +142 -56
  95. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  96. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  97. package/gsd-core/bin/lib/audit.cjs +78 -44
  98. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  99. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  100. package/gsd-core/bin/lib/capability-registry.cjs +183 -103
  101. package/gsd-core/bin/lib/capability-validator.cjs +16 -0
  102. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  103. package/gsd-core/bin/lib/check-command-router.cjs +164 -1712
  104. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  105. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  106. package/gsd-core/bin/lib/codex-agent-toml.cjs +11 -8
  107. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  108. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  109. package/gsd-core/bin/lib/commands.cjs +173 -170
  110. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  111. package/gsd-core/bin/lib/config.cjs +43 -12
  112. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  113. package/gsd-core/bin/lib/coverage.cjs +4 -8
  114. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  115. package/gsd-core/bin/lib/drift.cjs +177 -42
  116. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  117. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  118. package/gsd-core/bin/lib/frontmatter.cjs +413 -234
  119. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  120. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  121. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  122. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  123. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  124. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  125. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  126. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  127. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  128. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  129. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  130. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  131. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  132. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  133. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  134. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  135. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  136. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  137. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  138. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  139. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  140. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  141. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  142. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  143. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  144. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  145. package/gsd-core/bin/lib/init.cjs +162 -96
  146. package/gsd-core/bin/lib/install-engine.cjs +19 -35
  147. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  148. package/gsd-core/bin/lib/io.cjs +122 -3
  149. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  150. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  151. package/gsd-core/bin/lib/milestone.cjs +19 -1
  152. package/gsd-core/bin/lib/model-resolver.cjs +18 -18
  153. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  154. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  155. package/gsd-core/bin/lib/pattern.cjs +10 -0
  156. package/gsd-core/bin/lib/phase-command-router.cjs +11 -4
  157. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  158. package/gsd-core/bin/lib/phase-id.cjs +1 -1
  159. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  160. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  161. package/gsd-core/bin/lib/phase.cjs +280 -80
  162. package/gsd-core/bin/lib/plan-document.cjs +93 -19
  163. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  164. package/gsd-core/bin/lib/planning-document.cjs +273 -40
  165. package/gsd-core/bin/lib/planning-inspect.cjs +42 -8
  166. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  167. package/gsd-core/bin/lib/planning-workspace.cjs +76 -53
  168. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  169. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  170. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  171. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  172. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  173. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  174. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  175. package/gsd-core/bin/lib/roadmap-command-router.cjs +13 -15
  176. package/gsd-core/bin/lib/roadmap-parser.cjs +82 -15
  177. package/gsd-core/bin/lib/roadmap-upgrade.cjs +127 -65
  178. package/gsd-core/bin/lib/roadmap.cjs +169 -72
  179. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +91 -157
  180. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -1
  181. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  182. package/gsd-core/bin/lib/runtime-homes.cjs +13 -10
  183. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +307 -591
  184. package/gsd-core/bin/lib/runtime-name-policy.cjs +142 -27
  185. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  186. package/gsd-core/bin/lib/shell-command-projection.cjs +37 -2
  187. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  188. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  189. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  190. package/gsd-core/bin/lib/state-document.cjs +100 -22
  191. package/gsd-core/bin/lib/state.cjs +214 -105
  192. package/gsd-core/bin/lib/surface.cjs +2 -1
  193. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -152
  194. package/gsd-core/bin/lib/uat-predicate.cjs +313 -35
  195. package/gsd-core/bin/lib/uat.cjs +424 -7
  196. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +94 -58
  197. package/gsd-core/bin/lib/vendor/README.md +31 -9
  198. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  199. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  200. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  201. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  202. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  203. package/gsd-core/bin/lib/verification.cjs +1165 -314
  204. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  205. package/gsd-core/bin/lib/verify.cjs +286 -567
  206. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  207. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  208. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  209. package/gsd-core/bin/lib/workstream.cjs +11 -2
  210. package/gsd-core/bin/shared/config-defaults.manifest.json +5 -0
  211. package/gsd-core/bin/shared/config-schema.manifest.json +4 -0
  212. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  213. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  214. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  215. package/gsd-core/references/execute-phase-between-wave-reset.md +3 -0
  216. package/gsd-core/references/execute-phase-response-language.md +1 -1
  217. package/gsd-core/references/gsd-run-resolver.md +1 -1
  218. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  219. package/gsd-core/references/offer-next.md +1 -1
  220. package/gsd-core/references/planning-config.md +1 -1
  221. package/gsd-core/references/spidr-splitting.md +1 -1
  222. package/gsd-core/references/tdd.md +10 -4
  223. package/gsd-core/references/verifier-phase-gates.md +5 -2
  224. package/gsd-core/references/verify-mvp-mode.md +2 -2
  225. package/gsd-core/references/workstream-flag.md +33 -3
  226. package/gsd-core/templates/README.md +1 -1
  227. package/gsd-core/templates/UAT.md +17 -1
  228. package/gsd-core/templates/config.json +2 -11
  229. package/gsd-core/templates/verification-report.md +1 -1
  230. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  231. package/gsd-core/workflows/add-backlog.md +1 -1
  232. package/gsd-core/workflows/add-phase.md +8 -7
  233. package/gsd-core/workflows/add-tests.md +3 -2
  234. package/gsd-core/workflows/add-todo.md +4 -3
  235. package/gsd-core/workflows/ai-integration-phase.md +3 -2
  236. package/gsd-core/workflows/audit-fix.md +1 -1
  237. package/gsd-core/workflows/audit-milestone.md +4 -3
  238. package/gsd-core/workflows/audit-uat.md +1 -1
  239. package/gsd-core/workflows/autonomous.md +28 -18
  240. package/gsd-core/workflows/check-todos.md +6 -5
  241. package/gsd-core/workflows/cleanup.md +1 -1
  242. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  243. package/gsd-core/workflows/code-review-fix.md +5 -4
  244. package/gsd-core/workflows/code-review.md +84 -90
  245. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  246. package/gsd-core/workflows/complete-milestone.md +12 -7
  247. package/gsd-core/workflows/debug.md +30 -5
  248. package/gsd-core/workflows/diagnose-issues.md +3 -2
  249. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  250. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  252. package/gsd-core/workflows/discuss-phase.md +4 -3
  253. package/gsd-core/workflows/do.md +1 -1
  254. package/gsd-core/workflows/docs-update.md +4 -3
  255. package/gsd-core/workflows/edit-phase.md +4 -3
  256. package/gsd-core/workflows/eval-review.md +5 -3
  257. package/gsd-core/workflows/execute-phase/detail/elaboration.md +1 -1
  258. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +4 -2
  259. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  260. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  261. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  263. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  264. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  265. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  266. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  267. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  268. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  269. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  270. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  271. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  272. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  273. package/gsd-core/workflows/execute-phase.md +78 -170
  274. package/gsd-core/workflows/execute-plan.md +13 -19
  275. package/gsd-core/workflows/explore.md +2 -2
  276. package/gsd-core/workflows/extract-learnings.md +3 -2
  277. package/gsd-core/workflows/fast.md +1 -1
  278. package/gsd-core/workflows/forensics.md +1 -1
  279. package/gsd-core/workflows/graduation.md +1 -1
  280. package/gsd-core/workflows/health.md +2 -1
  281. package/gsd-core/workflows/import.md +3 -2
  282. package/gsd-core/workflows/inbox.md +1 -1
  283. package/gsd-core/workflows/ingest-docs.md +1 -1
  284. package/gsd-core/workflows/insert-phase.md +4 -3
  285. package/gsd-core/workflows/list-seeds.md +1 -1
  286. package/gsd-core/workflows/list-workspaces.md +1 -1
  287. package/gsd-core/workflows/manager.md +5 -3
  288. package/gsd-core/workflows/map-codebase.md +4 -3
  289. package/gsd-core/workflows/milestone-summary.md +3 -2
  290. package/gsd-core/workflows/mvp-phase.md +14 -14
  291. package/gsd-core/workflows/new-milestone.md +10 -10
  292. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  293. package/gsd-core/workflows/new-project.md +1 -1
  294. package/gsd-core/workflows/new-workspace.md +1 -1
  295. package/gsd-core/workflows/next.md +1 -1
  296. package/gsd-core/workflows/pause-work.md +2 -2
  297. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  298. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  299. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  300. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  301. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +1 -1
  302. package/gsd-core/workflows/plan-phase.md +27 -18
  303. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  304. package/gsd-core/workflows/plant-seed.md +1 -1
  305. package/gsd-core/workflows/pr-branch.md +1 -1
  306. package/gsd-core/workflows/profile-user.md +1 -1
  307. package/gsd-core/workflows/progress.md +19 -49
  308. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -2
  309. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  310. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  311. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  312. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  313. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  314. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  315. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  316. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  317. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  318. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  319. package/gsd-core/workflows/quick-batch.md +15 -10
  320. package/gsd-core/workflows/quick.md +42 -31
  321. package/gsd-core/workflows/remove-phase.md +3 -2
  322. package/gsd-core/workflows/remove-workspace.md +1 -1
  323. package/gsd-core/workflows/resume-project.md +3 -2
  324. package/gsd-core/workflows/review.md +3 -2
  325. package/gsd-core/workflows/scan.md +3 -2
  326. package/gsd-core/workflows/secure-phase.md +11 -12
  327. package/gsd-core/workflows/settings-advanced.md +1 -1
  328. package/gsd-core/workflows/settings-integrations.md +1 -1
  329. package/gsd-core/workflows/settings.md +1 -1
  330. package/gsd-core/workflows/ship.md +12 -12
  331. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  332. package/gsd-core/workflows/sketch.md +1 -1
  333. package/gsd-core/workflows/smart-entry.md +1 -1
  334. package/gsd-core/workflows/spec-phase.md +1 -1
  335. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  336. package/gsd-core/workflows/spike.md +1 -1
  337. package/gsd-core/workflows/stats.md +1 -1
  338. package/gsd-core/workflows/sync-skills.md +1 -1
  339. package/gsd-core/workflows/thread.md +2 -2
  340. package/gsd-core/workflows/transition.md +13 -23
  341. package/gsd-core/workflows/ui-phase.md +5 -4
  342. package/gsd-core/workflows/ui-review.md +4 -3
  343. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  344. package/gsd-core/workflows/undo.md +1 -1
  345. package/gsd-core/workflows/validate-phase.md +10 -12
  346. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  347. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  348. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  349. package/gsd-core/workflows/verify-work.md +68 -59
  350. package/hooks/dist/gsd-agent-isolation-guard.js +8 -0
  351. package/hooks/dist/gsd-check-update-worker.js +8 -0
  352. package/hooks/dist/gsd-check-update.js +8 -0
  353. package/hooks/dist/gsd-context-monitor.js +31 -8
  354. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  355. package/hooks/dist/gsd-secret-read-guard.js +147 -16
  356. package/hooks/dist/gsd-statusline.js +15 -7
  357. package/hooks/dist/gsd-update-banner.js +8 -0
  358. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  359. package/hooks/dist/gsd-workflow-guard.js +5 -4
  360. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  361. package/hooks/dist/lib/cli-exit.js +12 -0
  362. package/hooks/dist/lib/git-probe.js +17 -1
  363. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  364. package/hooks/gsd-agent-isolation-guard.js +8 -0
  365. package/hooks/gsd-check-update-worker.js +8 -0
  366. package/hooks/gsd-check-update.js +8 -0
  367. package/hooks/gsd-context-monitor.js +31 -8
  368. package/hooks/gsd-cursor-subagent-start.js +8 -0
  369. package/hooks/gsd-secret-read-guard.js +147 -16
  370. package/hooks/gsd-statusline.js +15 -7
  371. package/hooks/gsd-update-banner.js +8 -0
  372. package/hooks/gsd-windsurf-pre-write.js +11 -2
  373. package/hooks/gsd-workflow-guard.js +5 -4
  374. package/hooks/gsd-worktree-path-guard.js +6 -2
  375. package/hooks/hooks.json +5 -5
  376. package/hooks/lib/cli-exit.js +12 -0
  377. package/hooks/lib/git-probe.js +17 -1
  378. package/hooks/lib/isolation-sentinel.js +2 -2
  379. package/package.json +21 -4
  380. package/scripts/changeset/parse.cjs +52 -4
  381. package/scripts/ci-timeout-report.cjs +770 -4
  382. package/scripts/command-contract-helpers.cjs +12 -8
  383. package/scripts/docs-guard-registry.cjs +6 -0
  384. package/scripts/gen-features.cjs +13 -8
  385. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  386. package/scripts/gen-loop-host-contract.cjs +10 -1
  387. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  388. package/scripts/gen-plugin-skills.cjs +87 -1
  389. package/scripts/gen-research-agents.cjs +24 -31
  390. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  391. package/scripts/gen-test-timings.cjs +32 -7
  392. package/scripts/lib/cli-exit.cjs +12 -0
  393. package/scripts/lib/macos-conformance-tier.generated.cjs +20 -2
  394. package/scripts/lib/ndjson-reporter.cjs +28 -3
  395. package/scripts/lib/platform-conformance-tier.generated.cjs +34 -5
  396. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  397. package/scripts/lib/vendor-bundle.cjs +59 -0
  398. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  399. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  400. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  401. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  402. package/scripts/lint-descriptions.cjs +7 -3
  403. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +10 -0
  404. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  405. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  406. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  407. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  408. package/scripts/lint-phase-id-drift.cjs +29 -0
  409. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  410. package/scripts/lint-response-language-coverage.cjs +3 -0
  411. package/scripts/lint-skill-deps.cjs +7 -3
  412. package/scripts/lint-test-file-count.allowlist.json +11 -1
  413. package/scripts/lint-test-file-count.cjs +34 -1
  414. package/scripts/lint-vendored-deps.cjs +41 -5
  415. package/scripts/lint-workflow-shellcheck-baseline.json +10 -10
  416. package/scripts/mutation-matrix.cjs +50 -5
  417. package/scripts/require-issue-link-policy.cjs +6 -2
  418. package/scripts/sync-runtime-launcher.cjs +184 -2
  419. package/scripts/verify-npm-publish.cjs +76 -20
  420. package/skills/gsd-add-tests/SKILL.md +6 -1
  421. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  422. package/skills/gsd-audit-fix/SKILL.md +5 -0
  423. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  424. package/skills/gsd-autonomous/SKILL.md +5 -0
  425. package/skills/gsd-capture/SKILL.md +8 -4
  426. package/skills/gsd-code-review/SKILL.md +7 -2
  427. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  428. package/skills/gsd-config/SKILL.md +8 -4
  429. package/skills/gsd-debug/SKILL.md +11 -7
  430. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  431. package/skills/gsd-docs-update/SKILL.md +12 -7
  432. package/skills/gsd-eval-review/SKILL.md +6 -1
  433. package/skills/gsd-execute-phase/SKILL.md +12 -7
  434. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  435. package/skills/gsd-fast/SKILL.md +4 -0
  436. package/skills/gsd-forensics/SKILL.md +5 -1
  437. package/skills/gsd-graphify/SKILL.md +10 -6
  438. package/skills/gsd-health/SKILL.md +5 -0
  439. package/skills/gsd-help/SKILL.md +7 -2
  440. package/skills/gsd-import/SKILL.md +7 -3
  441. package/skills/gsd-inbox/SKILL.md +5 -0
  442. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  443. package/skills/gsd-manager/SKILL.md +6 -1
  444. package/skills/gsd-map-codebase/SKILL.md +7 -3
  445. package/skills/gsd-mempalace-capture/SKILL.md +5 -1
  446. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  447. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  448. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  449. package/skills/gsd-new-milestone/SKILL.md +6 -1
  450. package/skills/gsd-new-project/SKILL.md +5 -0
  451. package/skills/gsd-next/SKILL.md +6 -1
  452. package/skills/gsd-ns-context/SKILL.md +4 -0
  453. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  454. package/skills/gsd-ns-manage/SKILL.md +4 -0
  455. package/skills/gsd-ns-project/SKILL.md +4 -0
  456. package/skills/gsd-ns-review/SKILL.md +4 -0
  457. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  458. package/skills/gsd-onboard/SKILL.md +6 -1
  459. package/skills/gsd-pause-work/SKILL.md +5 -1
  460. package/skills/gsd-phase/SKILL.md +8 -4
  461. package/skills/gsd-plan-phase/SKILL.md +6 -1
  462. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  463. package/skills/gsd-pr-branch/SKILL.md +4 -0
  464. package/skills/gsd-profile-user/SKILL.md +5 -1
  465. package/skills/gsd-progress/SKILL.md +6 -1
  466. package/skills/gsd-quick/SKILL.md +16 -10
  467. package/skills/gsd-quick-batch/SKILL.md +20 -8
  468. package/skills/gsd-review/SKILL.md +5 -1
  469. package/skills/gsd-review-backlog/SKILL.md +3 -2
  470. package/skills/gsd-secure-phase/SKILL.md +6 -1
  471. package/skills/gsd-ship/SKILL.md +5 -0
  472. package/skills/gsd-sketch/SKILL.md +7 -2
  473. package/skills/gsd-spec-phase/SKILL.md +5 -1
  474. package/skills/gsd-spike/SKILL.md +8 -3
  475. package/skills/gsd-surface/SKILL.md +5 -1
  476. package/skills/gsd-thread/SKILL.md +4 -0
  477. package/skills/gsd-ui-phase/SKILL.md +6 -1
  478. package/skills/gsd-ui-review/SKILL.md +6 -1
  479. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  480. package/skills/gsd-undo/SKILL.md +5 -1
  481. package/skills/gsd-update/SKILL.md +6 -2
  482. package/skills/gsd-validate-phase/SKILL.md +6 -1
  483. package/skills/gsd-verify-work/SKILL.md +6 -1
  484. package/skills/gsd-workspace/SKILL.md +7 -3
  485. package/skills/gsd-workstreams/SKILL.md +6 -6
  486. package/vscode/package.json +1 -1
  487. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +0 -24
@@ -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,8 +1233,8 @@ 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
+ const variantPresent = /^variant:/m.test(fmMatch.body);
1237
+ const variantMatch = /^variant:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
1290
1238
  // `null` (key absent) and `''` (key present, value empty) are distinct
1291
1239
  // states this code deliberately tracks via `variantPresent` above — a
1292
1240
  // reported `from` that collapses both to `null` would make "no key" and
@@ -2883,13 +2831,38 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
2883
2831
  if (!summaryPath) {
2884
2832
  error('summary-path required for summary-extract');
2885
2833
  }
2886
- 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);
2887
2848
  if (!node_fs_1.default.existsSync(fullPath)) {
2888
2849
  output({ error: 'File not found', path: summaryPath }, raw, undefined);
2889
2850
  return;
2890
2851
  }
2891
2852
  const content = node_fs_1.default.readFileSync(fullPath, 'utf-8');
2892
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
+ }
2893
2866
  // Parse key-decisions into structured format
2894
2867
  const parseDecisions = (decisionsList) => {
2895
2868
  if (!decisionsList || !Array.isArray(decisionsList))
@@ -3060,6 +3033,8 @@ function cmdProgressRender(cwd, format, raw) {
3060
3033
  let totalPlans = 0;
3061
3034
  let totalSummaries = 0;
3062
3035
  let phaseScope = null;
3036
+ // #5118: carried out of the scan below and failed on after it.
3037
+ let statusError;
3063
3038
  try {
3064
3039
  // #3185 (ADR-3180 Decision 1): the single owner applies the milestone
3065
3040
  // window AND the sentinel filter and returns dirs already sorted by
@@ -3096,14 +3071,16 @@ function cmdProgressRender(cwd, format, raw) {
3096
3071
  phaseNum = dm ? dm[1] : dir;
3097
3072
  phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
3098
3073
  }
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;
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;
3104
3081
  totalPlans += plans;
3105
3082
  totalSummaries += summaries;
3106
- const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Pending');
3083
+ const status = (0, phase_status_cjs_1.toDisplayLabel)(ps.value.status, { pendingWord: 'Pending' });
3107
3084
  phases.push({
3108
3085
  number: phaseNum,
3109
3086
  ...(displayId ? { display_id: displayId } : {}),
@@ -3115,6 +3092,9 @@ function cmdProgressRender(cwd, format, raw) {
3115
3092
  }
3116
3093
  }
3117
3094
  catch { /* intentionally empty */ }
3095
+ // #5118: a read-only aggregate over a refused report prints nothing.
3096
+ if (statusError)
3097
+ failOnVerificationStatusError(statusError);
3118
3098
  // #3217 (ADR-3180 §7.6 rule 4): `phaseScope` was already computed above
3119
3099
  // (Phase 3, #3222) but never consulted before rendering — a percentage was
3120
3100
  // rendered from counts the scope said were not answers (TRUNCATED /
@@ -3273,29 +3253,35 @@ function cmdTodoMatchPhase(cwd, phase, raw) {
3273
3253
  // #4096: upsert completion keys INSIDE the leading frontmatter block. Never a
3274
3254
  // bare prefix line above the opening `---` (that displaces the fence to line 2
3275
3255
  // and breaks every fence-locating reader). A file with no well-formed block
3276
- // (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.
3277
3259
  function upsertTodoCompletionFields(content, today) {
3278
- const lines = content.split('\n');
3279
3260
  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) {
3261
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content);
3262
+ if (!fence?.closed) {
3283
3263
  // 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);
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' : '';
3288
3274
  for (const field of fields) {
3289
3275
  const key = `${field.slice(0, field.indexOf(':'))}:`;
3290
3276
  const idx = block.findIndex(l => l.startsWith(key));
3291
3277
  if (idx === -1) {
3292
- block.push(field);
3278
+ block.push(field + cr);
3293
3279
  }
3294
3280
  else {
3295
- block[idx] = field;
3281
+ block[idx] = field + cr;
3296
3282
  }
3297
3283
  }
3298
- 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)}`;
3299
3285
  }
3300
3286
  function cmdTodoComplete(cwd, filename, options, raw) {
3301
3287
  if (!filename) {
@@ -3313,7 +3299,7 @@ function cmdTodoComplete(cwd, filename, options, raw) {
3313
3299
  // to a location inside todosRoot (or inside pending/) and would pass
3314
3300
  // containment, yet none of them is a bare filename. Reject on basename
3315
3301
  // shape FIRST, before any path is even joined — same predicate shape as
3316
- // findPhaseArtifact in check-command-router.cts. Checking both `/` and
3302
+ // findPhaseArtifact in gate-predicate.cts. Checking both `/` and
3317
3303
  // `\` explicitly (not just path.basename) matters on POSIX, where a
3318
3304
  // literal backslash is just an ordinary filename character to
3319
3305
  // path.basename but not to path.win32.basename or to the user's intent.
@@ -3448,6 +3434,9 @@ function cmdStats(cwd, format, raw) {
3448
3434
  : null;
3449
3435
  const milestoneVersion = milestoneDisplay ?? milestone?.version ?? null;
3450
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).
3451
3440
  const phasesByNumber = new Map();
3452
3441
  let totalPlans = 0;
3453
3442
  let totalSummaries = 0;
@@ -3505,11 +3494,14 @@ function cmdStats(cwd, format, raw) {
3505
3494
  name: phaseName.replace(/\(INSERTED\)/i, '').trim(),
3506
3495
  plans: 0,
3507
3496
  summaries: 0,
3508
- status: 'Not Started',
3497
+ phaseStatus: phase_status_cjs_1.PHASE_STATUS.NOT_STARTED,
3509
3498
  });
3510
3499
  }
3511
3500
  }
3512
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;
3513
3505
  try {
3514
3506
  // #3185 (ADR-3180 Decision 1): route through the single owner. This
3515
3507
  // previously applied the milestone window but NOT a directory-level
@@ -3546,14 +3538,15 @@ function cmdStats(cwd, format, raw) {
3546
3538
  const afterToken = dir.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
3547
3539
  phaseName = afterToken ? afterToken.replace(/-/g, ' ') : '';
3548
3540
  }
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;
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;
3554
3548
  totalPlans += plans;
3555
3549
  totalSummaries += summaries;
3556
- const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Not Started');
3557
3550
  const normalizedNum = normalizePhaseName(phaseNum);
3558
3551
  const existing = phasesByNumber.get(normalizedNum);
3559
3552
  phasesByNumber.set(normalizedNum, {
@@ -3569,13 +3562,26 @@ function cmdStats(cwd, format, raw) {
3569
3562
  // platforms, so a naive overwrite can report a Complete phase as Not
3570
3563
  // Started (or vice versa) depending on read order. The fold picks the
3571
3564
  // furthest-along status, matching what an operator expects.
3572
- status: existing ? foldPhaseStatus(existing.status, status) : status,
3565
+ phaseStatus: existing ? (0, phase_status_cjs_1.foldPhaseStatuses)(existing.phaseStatus, ps.value.status) : ps.value.status,
3573
3566
  });
3574
3567
  }
3575
3568
  }
3576
3569
  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;
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
+ }));
3579
3585
  // #3217 (ADR-3180 §7.6 rule 4): both percentages here are derived from the
3580
3586
  // same `phaseScope`-carrying directory enumeration above (Phase 3, #3222) —
3581
3587
  // withhold both when that scope is not COMPLETE, same rationale as
@@ -3901,9 +3907,6 @@ function cmdCommitDocsGuardDisable(cwd, raw) {
3901
3907
  module.exports = {
3902
3908
  effortSurfaceForHost,
3903
3909
  groupFilesBySubrepo,
3904
- determinePhaseStatus,
3905
- foldPhaseStatus,
3906
- PHASE_STATUS_PRECEDENCE,
3907
3910
  cmdGenerateSlug,
3908
3911
  cmdCurrentTimestamp,
3909
3912
  cmdListTodos,
@@ -3,7 +3,9 @@
3
3
  * Complexity-triggered refactor extension point — analyzer, evaluator, and
4
4
  * baseline persistence (issue #1953).
5
5
  *
6
- * LEAF MODULE — imports ONLY: node:fs, node:path. No other src/ imports.
6
+ * LEAF MODULE — imports ONLY: node:fs, node:path, and the zero-import leaf
7
+ * frontmatter-fence.cjs (the one frontmatter fence owner, read by parseProposal). No other
8
+ * src/ imports.
7
9
  *
8
10
  * Pipeline: analyzeSource (decision-point complexity per function, via
9
11
  * stripLiterals to blank comments/string/regex content while preserving
@@ -49,6 +51,7 @@ exports.readBaseline = readBaseline;
49
51
  exports.writeBaseline = writeBaseline;
50
52
  const node_fs_1 = __importDefault(require("node:fs"));
51
53
  const node_path_1 = __importDefault(require("node:path"));
54
+ const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
52
55
  // ─── Constants ─────────────────────────────────────────────────────────────
53
56
  exports.BASELINE_FILE_NAME = 'complexity-baseline.json'; // under .planning/
54
57
  exports.PROPOSAL_SUFFIX = '-REFACTOR.md'; // ${PHASE_DIR}/${PADDED}-REFACTOR.md
@@ -1018,13 +1021,11 @@ function renderProposal(p) {
1018
1021
  */
1019
1022
  function parseProposal(text) {
1020
1023
  try {
1021
- if (!text.startsWith('---\n') && !text.startsWith('---\r\n'))
1024
+ // The block is the one the one fence owner finds (`locateFrontmatterFence`).
1025
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(text);
1026
+ if (!fence?.closed)
1022
1027
  return null;
1023
- const headerEnd = text.startsWith('---\r\n') ? 5 : 4;
1024
- const closeIdx = text.indexOf('\n---', headerEnd);
1025
- if (closeIdx === -1)
1026
- return null;
1027
- const yamlBody = text.slice(headerEnd, closeIdx);
1028
+ const yamlBody = text.slice(fence.openEnd, fence.bodyEnd);
1028
1029
  const fm = {};
1029
1030
  for (const rawLine of yamlBody.split(/\r?\n/)) {
1030
1031
  const line = rawLine.replace(/\r$/, '');