@opengsd/gsd-core 1.10.0 → 1.12.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 (544) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +12 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-doc-synthesizer.md +2 -4
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +22 -14
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +5 -3
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +23 -2
  15. package/agents/gsd-plan-checker.md +50 -53
  16. package/agents/gsd-planner.md +50 -50
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +63 -4
  21. package/agents/gsd-ui-researcher.md +41 -3
  22. package/agents/gsd-user-profiler.md +3 -0
  23. package/agents/gsd-verifier.md +13 -4
  24. package/bin/install.js +1448 -1103
  25. package/commands/gsd/code-review.md +1 -1
  26. package/commands/gsd/discuss-phase.md +1 -1
  27. package/commands/gsd/execute-phase.md +1 -1
  28. package/commands/gsd/import.md +1 -1
  29. package/commands/gsd/map-codebase.md +1 -1
  30. package/commands/gsd/mempalace-capture.md +1 -1
  31. package/commands/gsd/mempalace-recall.md +1 -1
  32. package/commands/gsd/new-milestone.md +1 -1
  33. package/commands/gsd/quick.md +9 -5
  34. package/commands/gsd/review-backlog.md +2 -1
  35. package/commands/gsd/verify-work.md +1 -1
  36. package/gsd-core/bin/gsd-tools.cjs +1035 -138
  37. package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
  38. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  39. package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
  40. package/gsd-core/bin/lib/api-coverage.cjs +33 -14
  41. package/gsd-core/bin/lib/artifacts.cjs +5 -0
  42. package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
  43. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  44. package/gsd-core/bin/lib/audit.cjs +1026 -268
  45. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  46. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  47. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  48. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  49. package/gsd-core/bin/lib/capability-registry.cjs +845 -130
  50. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  51. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  52. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  53. package/gsd-core/bin/lib/capability-validator.cjs +700 -40
  54. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  55. package/gsd-core/bin/lib/check-command-router.cjs +216 -42
  56. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  57. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  58. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  59. package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
  60. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  61. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  62. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  63. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  64. package/gsd-core/bin/lib/commands.cjs +1172 -108
  65. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  66. package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
  67. package/gsd-core/bin/lib/config-loader.cjs +187 -23
  68. package/gsd-core/bin/lib/config.cjs +102 -3
  69. package/gsd-core/bin/lib/configuration.cjs +129 -37
  70. package/gsd-core/bin/lib/core-utils.cjs +208 -33
  71. package/gsd-core/bin/lib/decisions.cjs +23 -0
  72. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  73. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  74. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  75. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  76. package/gsd-core/bin/lib/frontmatter.cjs +899 -229
  77. package/gsd-core/bin/lib/gap-checker.cjs +95 -10
  78. package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
  79. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  80. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  81. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  82. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
  83. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  84. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  85. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  86. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
  87. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  88. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  89. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
  90. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  91. package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
  92. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  93. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  94. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  95. package/gsd-core/bin/lib/init.cjs +439 -168
  96. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  97. package/gsd-core/bin/lib/install-engine.cjs +811 -259
  98. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  99. package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
  100. package/gsd-core/bin/lib/install-profiles.cjs +212 -61
  101. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  102. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  103. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  104. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  105. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  106. package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
  107. package/gsd-core/bin/lib/intel.cjs +101 -26
  108. package/gsd-core/bin/lib/io.cjs +170 -15
  109. package/gsd-core/bin/lib/learnings.cjs +85 -14
  110. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  111. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  112. package/gsd-core/bin/lib/markdown-table.cjs +183 -22
  113. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  114. package/gsd-core/bin/lib/milestone.cjs +842 -73
  115. package/gsd-core/bin/lib/model-catalog.cjs +232 -16
  116. package/gsd-core/bin/lib/model-resolver.cjs +193 -68
  117. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  118. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  119. package/gsd-core/bin/lib/pattern.cjs +122 -0
  120. package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
  121. package/gsd-core/bin/lib/phase-id.cjs +514 -40
  122. package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
  123. package/gsd-core/bin/lib/phase-locator.cjs +262 -34
  124. package/gsd-core/bin/lib/phase.cjs +1038 -214
  125. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  126. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  127. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  128. package/gsd-core/bin/lib/plan-scan.cjs +98 -3
  129. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  130. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  131. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  132. package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
  133. package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
  134. package/gsd-core/bin/lib/probe-core.cjs +5 -2
  135. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  136. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  137. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  138. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  139. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
  140. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  141. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
  142. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  143. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  144. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  145. package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
  146. package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
  147. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  148. package/gsd-core/bin/lib/roadmap.cjs +442 -96
  149. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
  150. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  151. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
  152. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  153. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  154. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
  155. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  156. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  157. package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
  158. package/gsd-core/bin/lib/security.cjs +104 -5
  159. package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
  160. package/gsd-core/bin/lib/smart-entry.cjs +133 -23
  161. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  162. package/gsd-core/bin/lib/state-command-router.cjs +52 -19
  163. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  164. package/gsd-core/bin/lib/state-document.cjs +338 -8
  165. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  166. package/gsd-core/bin/lib/state-transition.cjs +846 -176
  167. package/gsd-core/bin/lib/state.cjs +2589 -369
  168. package/gsd-core/bin/lib/surface.cjs +33 -11
  169. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  170. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  171. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  172. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  173. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  174. package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
  175. package/gsd-core/bin/lib/uat.cjs +1761 -167
  176. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  177. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  178. package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
  179. package/gsd-core/bin/lib/unusable-input.cjs +37 -0
  180. package/gsd-core/bin/lib/update-context.cjs +8 -2
  181. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  182. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  183. package/gsd-core/bin/lib/validate.cjs +20 -6
  184. package/gsd-core/bin/lib/vendor/README.md +75 -0
  185. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  186. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  187. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  188. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  189. package/gsd-core/bin/lib/verification.cjs +272 -9
  190. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  191. package/gsd-core/bin/lib/verify.cjs +453 -918
  192. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  193. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  194. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  195. package/gsd-core/bin/lib/workstream.cjs +2 -2
  196. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  197. package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
  198. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
  199. package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
  200. package/gsd-core/bin/shared/exit-codes.json +8 -0
  201. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  202. package/gsd-core/bin/shared/model-catalog.json +8 -1
  203. package/gsd-core/references/agent-contracts.md +44 -26
  204. package/gsd-core/references/api-coverage.md +24 -2
  205. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  206. package/gsd-core/references/checkpoints.md +39 -21
  207. package/gsd-core/references/context-budget.md +1 -1
  208. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  209. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  210. package/gsd-core/references/doc-conflict-engine.md +1 -1
  211. package/gsd-core/references/edge-probe.md +8 -0
  212. package/gsd-core/references/execute-mvp-tdd.md +4 -6
  213. package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
  214. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  215. package/gsd-core/references/execute-phase-response-language.md +1 -1
  216. package/gsd-core/references/execute-phase-wave-guard.md +17 -11
  217. package/gsd-core/references/failing-direction.md +78 -0
  218. package/gsd-core/references/gate-prompts.md +1 -1
  219. package/gsd-core/references/git-integration.md +5 -5
  220. package/gsd-core/references/git-planning-commit.md +5 -4
  221. package/gsd-core/references/gsd-run-resolver.md +1 -1
  222. package/gsd-core/references/loop-hook-dispatch.md +61 -2
  223. package/gsd-core/references/model-profiles.md +12 -4
  224. package/gsd-core/references/mvp-concepts.md +9 -9
  225. package/gsd-core/references/nyquist-compliance.md +74 -0
  226. package/gsd-core/references/offer-next.md +3 -5
  227. package/gsd-core/references/phase-argument-parsing.md +3 -3
  228. package/gsd-core/references/planner-failing-direction.md +53 -0
  229. package/gsd-core/references/planner-guidance.md +3 -9
  230. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  231. package/gsd-core/references/planner-preconditions.md +1 -1
  232. package/gsd-core/references/planner-reviews.md +1 -1
  233. package/gsd-core/references/planner-revision.md +1 -1
  234. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  235. package/gsd-core/references/planning-config.md +44 -13
  236. package/gsd-core/references/reviewer-instances.md +31 -0
  237. package/gsd-core/references/revision-loop.md +1 -1
  238. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  239. package/gsd-core/references/specless-probe-fallback.md +1 -1
  240. package/gsd-core/references/tdd.md +1 -3
  241. package/gsd-core/references/ui-brand.md +65 -21
  242. package/gsd-core/references/ui-consideration-probe.md +1 -1
  243. package/gsd-core/references/universal-anti-patterns.md +5 -5
  244. package/gsd-core/references/verifier-phase-gates.md +192 -0
  245. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  246. package/gsd-core/references/verify-mvp-mode.md +2 -2
  247. package/gsd-core/references/workstream-flag.md +33 -17
  248. package/gsd-core/templates/README.md +1 -1
  249. package/gsd-core/templates/SECURITY.md +3 -3
  250. package/gsd-core/templates/UI-SPEC.md +25 -3
  251. package/gsd-core/templates/VALIDATION.md +3 -3
  252. package/gsd-core/templates/discussion-log.md +1 -1
  253. package/gsd-core/templates/phase-prompt.md +5 -4
  254. package/gsd-core/templates/state.md +11 -4
  255. package/gsd-core/templates/verification-report.md +9 -1
  256. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  257. package/gsd-core/workflows/add-backlog.md +1 -1
  258. package/gsd-core/workflows/add-phase.md +3 -3
  259. package/gsd-core/workflows/add-tests.md +3 -8
  260. package/gsd-core/workflows/add-todo.md +1 -1
  261. package/gsd-core/workflows/ai-integration-phase.md +13 -20
  262. package/gsd-core/workflows/audit-fix.md +12 -3
  263. package/gsd-core/workflows/audit-milestone.md +9 -9
  264. package/gsd-core/workflows/audit-uat.md +17 -2
  265. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  266. package/gsd-core/workflows/autonomous.md +11 -27
  267. package/gsd-core/workflows/check-todos.md +1 -1
  268. package/gsd-core/workflows/cleanup.md +64 -5
  269. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
  270. package/gsd-core/workflows/code-review-fix.md +38 -11
  271. package/gsd-core/workflows/code-review.md +159 -52
  272. package/gsd-core/workflows/complete-milestone.md +151 -23
  273. package/gsd-core/workflows/debug.md +12 -8
  274. package/gsd-core/workflows/diagnose-issues.md +47 -15
  275. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  276. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
  277. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  278. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  279. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  280. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  281. package/gsd-core/workflows/discuss-phase.md +1 -1
  282. package/gsd-core/workflows/do.md +3 -6
  283. package/gsd-core/workflows/docs-update.md +5 -4
  284. package/gsd-core/workflows/edit-phase.md +27 -2
  285. package/gsd-core/workflows/eval-review.md +7 -14
  286. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  287. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
  288. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  289. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  290. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  291. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -4
  292. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  293. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  294. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  295. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  296. package/gsd-core/workflows/execute-phase.md +72 -100
  297. package/gsd-core/workflows/execute-plan.md +52 -15
  298. package/gsd-core/workflows/explore.md +131 -4
  299. package/gsd-core/workflows/extract-learnings.md +1 -1
  300. package/gsd-core/workflows/fast.md +10 -2
  301. package/gsd-core/workflows/forensics.md +1 -1
  302. package/gsd-core/workflows/graduation.md +5 -5
  303. package/gsd-core/workflows/health.md +76 -10
  304. package/gsd-core/workflows/import.md +18 -15
  305. package/gsd-core/workflows/inbox.md +4 -5
  306. package/gsd-core/workflows/ingest-docs.md +49 -16
  307. package/gsd-core/workflows/insert-phase.md +5 -5
  308. package/gsd-core/workflows/list-seeds.md +5 -3
  309. package/gsd-core/workflows/list-workspaces.md +1 -1
  310. package/gsd-core/workflows/manager.md +12 -23
  311. package/gsd-core/workflows/map-codebase.md +1 -1
  312. package/gsd-core/workflows/milestone-summary.md +1 -1
  313. package/gsd-core/workflows/mvp-phase.md +8 -5
  314. package/gsd-core/workflows/new-milestone.md +22 -29
  315. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  316. package/gsd-core/workflows/new-project.md +26 -40
  317. package/gsd-core/workflows/new-workspace.md +1 -1
  318. package/gsd-core/workflows/next.md +14 -2
  319. package/gsd-core/workflows/pause-work.md +1 -1
  320. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  321. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  322. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  323. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  324. package/gsd-core/workflows/plan-phase.md +162 -59
  325. package/gsd-core/workflows/plan-review-convergence.md +96 -11
  326. package/gsd-core/workflows/plant-seed.md +2 -2
  327. package/gsd-core/workflows/pr-branch.md +187 -51
  328. package/gsd-core/workflows/profile-user.md +16 -14
  329. package/gsd-core/workflows/progress.md +61 -18
  330. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  331. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
  332. package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
  333. package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
  334. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  335. package/gsd-core/workflows/quick.md +55 -44
  336. package/gsd-core/workflows/remove-phase.md +4 -4
  337. package/gsd-core/workflows/remove-workspace.md +2 -2
  338. package/gsd-core/workflows/resume-project.md +8 -12
  339. package/gsd-core/workflows/review.md +219 -20
  340. package/gsd-core/workflows/scan.md +1 -1
  341. package/gsd-core/workflows/secure-phase.md +3 -3
  342. package/gsd-core/workflows/session-report.md +2 -1
  343. package/gsd-core/workflows/settings-advanced.md +7 -9
  344. package/gsd-core/workflows/settings-integrations.md +64 -31
  345. package/gsd-core/workflows/settings.md +69 -7
  346. package/gsd-core/workflows/ship.md +116 -50
  347. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  348. package/gsd-core/workflows/sketch.md +12 -18
  349. package/gsd-core/workflows/smart-entry.md +3 -5
  350. package/gsd-core/workflows/spec-phase.md +53 -13
  351. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  352. package/gsd-core/workflows/spike.md +20 -31
  353. package/gsd-core/workflows/stats.md +2 -2
  354. package/gsd-core/workflows/sync-skills.md +64 -9
  355. package/gsd-core/workflows/thread.md +11 -7
  356. package/gsd-core/workflows/transition.md +49 -14
  357. package/gsd-core/workflows/ui-phase.md +15 -21
  358. package/gsd-core/workflows/ui-review.md +8 -12
  359. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  360. package/gsd-core/workflows/undo.md +8 -16
  361. package/gsd-core/workflows/update.md +7 -11
  362. package/gsd-core/workflows/validate-phase.md +3 -3
  363. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  364. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  365. package/gsd-core/workflows/verify-work.md +66 -25
  366. package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
  367. package/hooks/dist/gsd-check-update-worker.js +56 -13
  368. package/hooks/dist/gsd-check-update.js +19 -1
  369. package/hooks/dist/gsd-config-reload.js +18 -12
  370. package/hooks/dist/gsd-context-monitor.js +19 -10
  371. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  372. package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
  373. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  374. package/hooks/dist/gsd-cursor-stop.js +2 -1
  375. package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
  376. package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
  377. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  378. package/hooks/dist/gsd-graphify-update.sh +22 -18
  379. package/hooks/dist/gsd-node-runner.sh +76 -0
  380. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  381. package/hooks/dist/gsd-prompt-guard.js +37 -27
  382. package/hooks/dist/gsd-read-guard.js +16 -7
  383. package/hooks/dist/gsd-read-injection-scanner.js +55 -32
  384. package/hooks/dist/gsd-session-state.sh +1 -0
  385. package/hooks/dist/gsd-statusline.js +231 -24
  386. package/hooks/dist/gsd-update-banner.js +22 -1
  387. package/hooks/dist/gsd-validate-commit.sh +80 -6
  388. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  389. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  390. package/hooks/dist/gsd-workflow-guard.js +162 -46
  391. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  392. package/hooks/dist/gsd-write-guard.js +35 -25
  393. package/hooks/dist/lib/cli-exit.js +560 -0
  394. package/hooks/dist/lib/exit-code-registry.js +98 -0
  395. package/hooks/dist/lib/git-cmd.js +92 -59
  396. package/hooks/dist/lib/git-probe.js +84 -0
  397. package/hooks/dist/lib/hook-exit.js +81 -0
  398. package/hooks/dist/lib/injection-patterns.js +45 -0
  399. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  400. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  401. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  402. package/hooks/gsd-agent-isolation-guard.js +158 -30
  403. package/hooks/gsd-check-update-worker.js +56 -13
  404. package/hooks/gsd-check-update.js +19 -1
  405. package/hooks/gsd-config-reload.js +18 -12
  406. package/hooks/gsd-context-monitor.js +19 -10
  407. package/hooks/gsd-cursor-post-tool.js +3 -1
  408. package/hooks/gsd-cursor-pre-tool.js +2 -3
  409. package/hooks/gsd-cursor-session-start.js +2 -1
  410. package/hooks/gsd-cursor-stop.js +2 -1
  411. package/hooks/gsd-cursor-subagent-start.js +83 -3
  412. package/hooks/gsd-cursor-subagent-stop.js +6 -3
  413. package/hooks/gsd-ensure-canonical-path.js +2 -1
  414. package/hooks/gsd-graphify-update.sh +22 -18
  415. package/hooks/gsd-node-runner.sh +76 -0
  416. package/hooks/gsd-phase-boundary.sh +1 -0
  417. package/hooks/gsd-prompt-guard.js +37 -27
  418. package/hooks/gsd-read-guard.js +16 -7
  419. package/hooks/gsd-read-injection-scanner.js +55 -32
  420. package/hooks/gsd-session-state.sh +1 -0
  421. package/hooks/gsd-statusline.js +231 -24
  422. package/hooks/gsd-update-banner.js +22 -1
  423. package/hooks/gsd-validate-commit.sh +80 -6
  424. package/hooks/gsd-windsurf-pre-command.js +16 -11
  425. package/hooks/gsd-windsurf-pre-write.js +22 -13
  426. package/hooks/gsd-workflow-guard.js +162 -46
  427. package/hooks/gsd-worktree-path-guard.js +36 -21
  428. package/hooks/gsd-write-guard.js +35 -25
  429. package/hooks/lib/cli-exit.js +560 -0
  430. package/hooks/lib/exit-code-registry.js +98 -0
  431. package/hooks/lib/git-cmd.js +92 -59
  432. package/hooks/lib/git-probe.js +84 -0
  433. package/hooks/lib/hook-exit.js +81 -0
  434. package/hooks/lib/injection-patterns.js +45 -0
  435. package/hooks/lib/isolation-deny-reason.js +39 -0
  436. package/hooks/lib/isolation-sentinel.js +9 -0
  437. package/hooks/managed-hooks-registry.cjs +3 -0
  438. package/package.json +28 -11
  439. package/pi/gsd.cjs +19 -5
  440. package/scripts/base64-scan.sh +74 -12
  441. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  442. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  443. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  444. package/scripts/build-hooks.js +5 -0
  445. package/scripts/changeset/lint.cjs +60 -5
  446. package/scripts/check-alias-drift.cjs +7 -43
  447. package/scripts/check-contract-drift.cjs +297 -0
  448. package/scripts/check-glossary-refs.cjs +77 -15
  449. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  450. package/scripts/ci-check-job-near-cap.cjs +49 -0
  451. package/scripts/ci-pr-mergeability.cjs +262 -0
  452. package/scripts/ci-test-scope.cjs +64 -14
  453. package/scripts/ci-timeout-report.cjs +230 -0
  454. package/scripts/command-contract-helpers.cjs +903 -1
  455. package/scripts/docs-guard-registry.cjs +396 -0
  456. package/scripts/gen-adr-index.cjs +728 -38
  457. package/scripts/gen-capability-registry.cjs +11 -21
  458. package/scripts/gen-context-index.cjs +2 -11
  459. package/scripts/gen-exit-code-docs.cjs +318 -0
  460. package/scripts/gen-exit-code-registry.cjs +891 -0
  461. package/scripts/gen-features.cjs +836 -0
  462. package/scripts/gen-health-docs.cjs +390 -0
  463. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  464. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  465. package/scripts/gen-inventory-manifest.cjs +50 -4
  466. package/scripts/gen-loop-host-contract.cjs +138 -25
  467. package/scripts/gen-registry.cjs +3 -14
  468. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  469. package/scripts/gen-state-md-docs.cjs +727 -0
  470. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  471. package/scripts/lib/alias-drift-families.cjs +46 -0
  472. package/scripts/lib/ci-job-timing.cjs +72 -0
  473. package/scripts/lib/cli-exit.cjs +546 -44
  474. package/scripts/lib/drift-scan.cjs +308 -0
  475. package/scripts/lib/exit-code-registry.cjs +98 -0
  476. package/scripts/lib/ndjson-reporter.cjs +119 -0
  477. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  478. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  479. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  480. package/scripts/lint-canary-version-leak.cjs +73 -0
  481. package/scripts/lint-command-contract.cjs +96 -13
  482. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  483. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  484. package/scripts/lint-default-flip-documentation.cjs +193 -0
  485. package/scripts/lint-docs-guard-registration.cjs +495 -0
  486. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  487. package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
  488. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  489. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  490. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  491. package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
  492. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  493. package/scripts/lint-milestone-window-drift.cjs +468 -0
  494. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  495. package/scripts/lint-phase-enumeration-drift.cjs +492 -0
  496. package/scripts/lint-plan-count-drift.cjs +318 -0
  497. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  498. package/scripts/lint-planning-prompt-drift.cjs +471 -0
  499. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  500. package/scripts/lint-regression-test-names.cjs +15 -13
  501. package/scripts/lint-removed-but-needed.cjs +488 -0
  502. package/scripts/lint-seam-enforcement.cjs +182 -0
  503. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  504. package/scripts/lint-source-test-name-collision.cjs +241 -0
  505. package/scripts/lint-state-field-drift.cjs +805 -0
  506. package/scripts/lint-state-write-path-drift.cjs +950 -0
  507. package/scripts/lint-test-file-count.allowlist.json +137 -8
  508. package/scripts/lint-test-file-count.cjs +25 -3
  509. package/scripts/lint-unreachable-guard-drift.cjs +830 -0
  510. package/scripts/lint-vendored-deps.cjs +297 -0
  511. package/scripts/mutation-matrix.cjs +599 -50
  512. package/scripts/pr-changed-files.cjs +63 -0
  513. package/scripts/pr-template-policy.cjs +14 -4
  514. package/scripts/prompt-injection-scan.sh +100 -14
  515. package/scripts/require-issue-link-policy.cjs +192 -0
  516. package/scripts/secret-scan.sh +75 -13
  517. package/scripts/select-docs-guards.cjs +56 -0
  518. package/scripts/sync-runtime-launcher.cjs +24 -7
  519. package/skills/gsd-autonomous/SKILL.md +0 -1
  520. package/skills/gsd-code-review/SKILL.md +1 -1
  521. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  522. package/skills/gsd-execute-phase/SKILL.md +1 -2
  523. package/skills/gsd-import/SKILL.md +1 -1
  524. package/skills/gsd-map-codebase/SKILL.md +1 -1
  525. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  526. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  527. package/skills/gsd-new-milestone/SKILL.md +1 -1
  528. package/skills/gsd-next/SKILL.md +0 -1
  529. package/skills/gsd-plan-phase/SKILL.md +0 -1
  530. package/skills/gsd-progress/SKILL.md +0 -1
  531. package/skills/gsd-quick/SKILL.md +9 -5
  532. package/skills/gsd-review-backlog/SKILL.md +2 -1
  533. package/skills/gsd-stats/SKILL.md +0 -1
  534. package/skills/gsd-verify-work/SKILL.md +1 -1
  535. package/vscode/package.json +1 -1
  536. package/bin/lib/ui-safety-gate.cjs +0 -107
  537. package/gsd-core/workflows/discovery-phase.md +0 -298
  538. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  539. package/gsd-core/workflows/verify-phase.md +0 -574
  540. package/scripts/affected-tests-lib.cjs +0 -554
  541. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  542. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  543. package/scripts/run-affected-tests.cjs +0 -7
  544. package/scripts/run-tests.cjs +0 -1051
@@ -12,35 +12,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const clock_cjs_1 = require("./clock.cjs");
15
+ const pattern_cjs_1 = require("./pattern.cjs");
16
+ const text_lines_cjs_1 = require("./text-lines.cjs");
15
17
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
18
  const ioMod = require("./io.cjs");
17
- const { output, error } = ioMod;
19
+ const { output, error, formatDiagnosticToken } = ioMod;
18
20
  // eslint-disable-next-line @typescript-eslint/no-require-imports
19
21
  const phaseIdMod = require("./phase-id.cjs");
20
- const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseTokenMatches, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE, roadmapPhaseLookupSources } = phaseIdMod;
22
+ const { normalizePhaseName, phaseMarkdownRegexSource, matchPhaseDirs, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE, roadmapPhaseLookupSources, isSentinelPhaseId, scopeToPhase } = phaseIdMod;
21
23
  // eslint-disable-next-line @typescript-eslint/no-require-imports
22
24
  const phaseLocatorMod = require("./phase-locator.cjs");
23
- const { findPhaseInternal } = phaseLocatorMod;
25
+ const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocatorMod;
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ const planningScopeMod = require("./planning-scope.cjs");
28
+ const { SCOPE } = planningScopeMod;
24
29
  // eslint-disable-next-line @typescript-eslint/no-require-imports
25
30
  const roadmapParserModule = require("./roadmap-parser.cjs");
26
- const { stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = roadmapParserModule;
31
+ const { stripShippedMilestones, extractCurrentMilestone, extractCurrentMilestoneScoped, replaceInCurrentMilestone, listMilestoneHeadings, scanMilestonePhaseIds, collectTablePhaseRows } = roadmapParserModule;
27
32
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
28
33
  const markdown_table_cjs_1 = require("./markdown-table.cjs");
34
+ const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
29
35
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
30
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
37
  const planningWorkspace = require("./planning-workspace.cjs");
32
38
  const { planningPaths, withPlanningLock, findContextMdIn } = planningWorkspace;
39
+ // #3641: milestone-scope's convention resolution reads the project config
40
+ // (no cycle — config-loader does not import this module).
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
42
+ const configLoaderForScope = require("./config-loader.cjs");
43
+ const { loadConfig: loadConfigForScope } = configLoaderForScope;
33
44
  // eslint-disable-next-line @typescript-eslint/no-require-imports
34
45
  const scanPhasePlans = require("./plan-scan.cjs");
35
46
  // eslint-disable-next-line @typescript-eslint/no-require-imports
36
47
  const coreUtils = require("./core-utils.cjs");
37
- const { countMatchedSummaries } = coreUtils;
48
+ const { countMatchedSummaries, findUnsummarizedPlans } = coreUtils;
38
49
  // eslint-disable-next-line @typescript-eslint/no-require-imports
39
50
  const frontmatter = require("./frontmatter.cjs");
40
51
  const { extractFrontmatter, parseMustHavesBlock } = frontmatter;
41
52
  // eslint-disable-next-line @typescript-eslint/no-require-imports
42
53
  const verificationMod = require("./verification.cjs");
43
- const { readVerificationStatus } = verificationMod;
54
+ const { isPhaseComplete } = verificationMod;
44
55
  // ─── coerceTruthToString ──────────────────────────────────────────────────────
45
56
  /**
46
57
  * Coerce an arbitrary YAML scalar/object into a string for cross-cutting
@@ -83,27 +94,53 @@ function countPhasePlansAndSummaries(phaseDir) {
83
94
  // hasContext and hasResearch are not plan-scan concerns — read the directory
84
95
  // once and share the listing for all non-plan metadata that cmdRoadmapAnalyze needs.
85
96
  let phaseFiles = [];
97
+ // #3885 (ADR-3473 §8.5): distinguish "genuinely absent" (ENOENT) from
98
+ // "could not read" (EACCES/EIO/...) — the collapse of both to an empty
99
+ // listing is exactly the defect class this item closes. Mirrors
100
+ // core-utils.cts's getPhaseFileStats / phase-locator.cts's
101
+ // listMilestonePhaseDirs SCOPE.UNREADABLE discriminator.
102
+ let contextReadError = null;
86
103
  try {
87
104
  phaseFiles = node_fs_1.default.readdirSync(phaseDir);
88
105
  }
89
- catch { /* empty */ }
106
+ catch (err) {
107
+ const code = err?.code;
108
+ if (code !== 'ENOENT') {
109
+ contextReadError = `Could not read phase directory ${formatDiagnosticToken(phaseDir)}: ${formatDiagnosticToken(err?.message ?? String(err))}`;
110
+ }
111
+ }
112
+ // #3511: scope the raw listing to this phase dir before the
113
+ // phase-numbered-artifact predicates (hasContext/hasResearch) — planCount/
114
+ // summaryCount above stay on scanPhasePlans's own unscoped listing since a
115
+ // PLAN/SUMMARY leading number is a plan sequence number, not a phase
116
+ // number. Mirrors core-utils.cts's getPhaseFileStats.
117
+ const scopedFiles = scopeToPhase(phaseFiles, node_path_1.default.basename(phaseDir));
90
118
  return {
91
119
  planCount,
92
120
  summaryCount,
93
- hasContext: findContextMdIn(phaseFiles) !== null,
94
- hasResearch: phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
121
+ hasContext: findContextMdIn(scopedFiles) !== null,
122
+ hasResearch: scopedFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
123
+ contextReadError,
95
124
  };
96
125
  }
97
126
  // `phaseMarkdownRegexSource` lives in phase-id.cjs (#3537) and is imported above.
98
127
  // ─── searchPhaseInContent ─────────────────────────────────────────────────────
128
+ /**
129
+ * Build the phase-heading regex used by `searchPhaseInContent` for a given
130
+ * pre-escaped phase source. Extracted (#3412) so tests can assert against the
131
+ * exact production pattern instead of hand-duplicating it.
132
+ * #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
133
+ */
134
+ function buildPhaseHeadingRegex(escapedPhase) {
135
+ return new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
136
+ }
99
137
  /**
100
138
  * Search for a phase header (and its section) within the given content string.
101
139
  * Returns a result object if found (either a full match or a malformed_roadmap
102
140
  * checklist-only match), or null if the phase is not present at all.
103
141
  */
104
142
  function searchPhaseInContent(content, escapedPhase, phaseNum) {
105
- // #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
106
- const headingPattern = new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
143
+ const headingPattern = buildPhaseHeadingRegex(escapedPhase);
107
144
  const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
108
145
  const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
109
146
  const headerMatch = headingIndex === -1 ? null : headings[headingIndex].text.match(headingPattern);
@@ -173,7 +210,8 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
173
210
  * phase resolution as `roadmap.get-phase` — not a milestone-only subset.
174
211
  */
175
212
  function getRoadmapPhaseWithFallback(cwd, phaseNum) {
176
- if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum)))
213
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
214
+ if (isSentinelPhaseId(stripProjectCodePrefix(phaseNum)))
177
215
  return null;
178
216
  const roadmapPath = planningPaths(cwd).roadmap;
179
217
  // Read directly rather than gating on fs.existsSync: existsSync returns false
@@ -206,7 +244,8 @@ function getRoadmapPhaseWithFallback(cwd, phaseNum) {
206
244
  }
207
245
  // ─── cmdRoadmapGetPhase ───────────────────────────────────────────────────────
208
246
  function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
209
- if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum))) {
247
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
248
+ if (isSentinelPhaseId(stripProjectCodePrefix(phaseNum))) {
210
249
  output({ found: false, phase_number: phaseNum }, raw, '');
211
250
  return;
212
251
  }
@@ -242,6 +281,17 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
242
281
  if (!malformed)
243
282
  malformed = (milestoneResult?.error ? milestoneResult : (fullResult?.error ? fullResult : null));
244
283
  }
284
+ // #3577: no heading or checklist entry matched — fall back to a
285
+ // markdown-table row declaration (the same last-resort tier
286
+ // getRoadmapPhaseInternal gained). Zero-pad-tolerant id compare (#3572
287
+ // lesson: the declared form may be padded).
288
+ const stripPad = (s) => s.replace(/^0+(?=.)/, '');
289
+ const tableHit = collectTablePhaseRows(milestoneContent).find((tr) => stripPad(tr.id) === stripPad(phaseNum))
290
+ ?? collectTablePhaseRows(fullContent).find((tr) => stripPad(tr.id) === stripPad(phaseNum));
291
+ if (tableHit) {
292
+ output({ found: true, phase_number: phaseNum, phase_name: tableHit.name ?? `Phase ${tableHit.id}`, goal: null, section: tableHit.row.trim() }, raw, tableHit.row.trim());
293
+ return;
294
+ }
245
295
  if (malformed) {
246
296
  output(malformed, raw, '');
247
297
  return;
@@ -252,16 +302,16 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
252
302
  error('Failed to read ROADMAP.md: ' + e.message);
253
303
  }
254
304
  }
255
- // ─── cmdRoadmapAnalyze ────────────────────────────────────────────────────────
256
- function cmdRoadmapAnalyze(cwd, raw) {
257
- const roadmapPath = planningPaths(cwd).roadmap;
258
- if (!node_fs_1.default.existsSync(roadmapPath)) {
259
- output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw, undefined);
260
- return;
261
- }
262
- const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
263
- const content = extractCurrentMilestone(rawContent, cwd);
264
- const phasesDir = planningPaths(cwd).phases;
305
+ /**
306
+ * #3165: scan `content` for phase-detail headings (`##/###/#### Phase N: Name`)
307
+ * and enrich each with its on-disk plan/summary/completion status and ROADMAP
308
+ * checkbox. Pure extraction over `content` + the pre-built `phaseDirNames`
309
+ * lookup index — no milestone windowing of its own; the caller chooses the
310
+ * content (scoped window or fallback). Extracted verbatim from
311
+ * `cmdRoadmapAnalyze`'s former inline loop so the fallback re-runs the EXACT
312
+ * same enrichment, not a second derivation.
313
+ */
314
+ function collectAnalyzePhases(content, phasesDir, phaseDirNames) {
265
315
  // Extract all phase headings: ## Phase N: Name or ### Phase N: Name
266
316
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
267
317
  // phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
@@ -272,29 +322,9 @@ function cmdRoadmapAnalyze(cwd, raw) {
272
322
  const phasePattern = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+([A-Za-z]?\d+[A-Z]?(?:[.-]\d+)*)(?:\s*\([^)\n]{0,200}\))?\s*:\s*([^\n]+)/gi;
273
323
  const phases = [];
274
324
  let match;
275
- // Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not real
276
- // phases. They legitimately have no directory and must never be surfaced as
277
- // current/next phase or counted in phase_count. Mirrors the engine-wide
278
- // sentinel convention (phase-id getMilestoneFromPhaseId, roadmap-command-router
279
- // SENTINELS, the #1445 /^999/ progress filters). (#1580)
280
- const isSentinelPhase = (num) => {
281
- const major = parseInt(num, 10);
282
- return major === 0 || major === 999;
283
- };
284
- // Build phase directory lookup once (O(1) readdir instead of O(N) per phase)
285
- const _phaseDirNames = (() => {
286
- try {
287
- return node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
288
- .filter(e => e.isDirectory())
289
- .map(e => e.name);
290
- }
291
- catch {
292
- return [];
293
- }
294
- })();
295
325
  while ((match = phasePattern.exec(content)) !== null) {
296
326
  const phaseNum = match[1];
297
- if (isSentinelPhase(phaseNum))
327
+ if (isSentinelPhaseId(phaseNum))
298
328
  continue;
299
329
  const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
300
330
  // Extract goal from the section
@@ -319,20 +349,32 @@ function cmdRoadmapAnalyze(cwd, raw) {
319
349
  let summaryCount = 0;
320
350
  let hasContext = false;
321
351
  let hasResearch = false;
322
- // DEAD catch removed (#2245 audit): _phaseDirNames.find(...) is a pure
352
+ // #3885 (ADR-3473 §8.5): null unless dirMatch resolves and its readdirSync
353
+ // hit a non-ENOENT error — no directory at all is `disk_status:
354
+ // 'no_directory'`, a real (if uninteresting) answer, not a read error.
355
+ let contextReadError = null;
356
+ // DEAD catch removed (#2245 audit): matchPhaseDirs(...) is a pure
323
357
  // array lookup on an already-resolved string array, and
324
358
  // countPhasePlansAndSummaries is itself fully defensive (its own
325
359
  // readdirSync is self-guarded, and it delegates to scanPhasePlans, which
326
360
  // never throws) — nothing in this block can throw, so the try/catch could
327
361
  // never be triggered.
328
- const dirMatch = _phaseDirNames.find(d => phaseTokenMatches(d, normalized));
362
+ const dirMatch = matchPhaseDirs(phaseDirNames, normalized).matches[0];
329
363
  if (dirMatch) {
330
364
  const counts = countPhasePlansAndSummaries(node_path_1.default.join(phasesDir, dirMatch));
331
365
  planCount = counts.planCount;
332
366
  summaryCount = counts.summaryCount;
333
367
  hasContext = counts.hasContext;
334
368
  hasResearch = counts.hasResearch;
335
- if (summaryCount >= planCount && planCount > 0)
369
+ contextReadError = counts.contextReadError;
370
+ // ADR-3180 §7.4 (issue #3186, disk-strict, #3168 fix): route "is this
371
+ // phase complete" through the canonical owner (`isPhaseComplete`),
372
+ // which calls readVerificationStatus UNCONDITIONALLY — plan count is
373
+ // NOT a precondition, so a zero-plan phase with a passing
374
+ // `*-VERIFICATION.md` reports complete here too, not just via
375
+ // `phase.complete`.
376
+ const completionResult = isPhaseComplete(node_path_1.default.join(phasesDir, dirMatch));
377
+ if (completionResult.value.complete)
336
378
  diskStatus = 'complete';
337
379
  else if (summaryCount > 0)
338
380
  diskStatus = 'partial';
@@ -345,19 +387,23 @@ function cmdRoadmapAnalyze(cwd, raw) {
345
387
  else
346
388
  diskStatus = 'empty';
347
389
  }
348
- // Check ROADMAP checkbox status.
349
- // #3537: padding-tolerant fragment — the heading discovered above may use
350
- // a different padding than the summary-bullet checkbox below it (mixed
351
- // padding inside one ROADMAP is legal and seen in real projects).
390
+ // Check ROADMAP checkbox status. #3537: padding-tolerant fragment — the
391
+ // heading discovered above may use a different padding than the
392
+ // summary-bullet checkbox below it (mixed padding inside one ROADMAP is
393
+ // legal and seen in real projects).
394
+ //
395
+ // ADR-3180 §7.4 (disk-strict, #2957, maintainer decision 2026-08-08):
396
+ // `roadmapComplete` is reported below as metadata ONLY — it carries NO
397
+ // machine authority over `diskStatus`. The override that used to trust a
398
+ // ticked checkbox over disk file structure is DELETED, not generalized
399
+ // (#2957: "a ticked ROADMAP checkbox is a human annotation with no
400
+ // machine authority"). A phase marked complete solely by a ticked
401
+ // checkbox — no passing `*-VERIFICATION.md`, plans outstanding — now
402
+ // reports incomplete; this is the deliberate Tier-2 break (ADR-3180 §7.4
403
+ // Decision 3).
352
404
  const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseMarkdownRegexSource(phaseNum)}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
353
405
  const checkboxMatch = content.match(checkboxPattern);
354
406
  const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
355
- // If roadmap marks phase complete, trust that over disk file structure.
356
- // Phases completed before GSD tracking (or via external tools) may lack
357
- // the standard PLAN/SUMMARY pairs but are still done.
358
- if (roadmapComplete && diskStatus !== 'complete') {
359
- diskStatus = 'complete';
360
- }
361
407
  phases.push({
362
408
  number: phaseNum,
363
409
  name: phaseName,
@@ -370,18 +416,116 @@ function cmdRoadmapAnalyze(cwd, raw) {
370
416
  has_research: hasResearch,
371
417
  disk_status: diskStatus,
372
418
  roadmap_complete: roadmapComplete,
419
+ context_read_error: contextReadError,
373
420
  });
374
421
  }
375
- // Extract milestone info
376
- const milestones = [];
377
- const milestonePattern = /##\s*(.*v(\d+(?:\.\d+)+)[^(\n]*)/gi;
378
- let mMatch;
379
- while ((mMatch = milestonePattern.exec(content)) !== null) {
380
- milestones.push({
381
- heading: mMatch[1].trim(),
382
- version: 'v' + mMatch[2],
422
+ // #3577: markdown-table row declarations join the enumeration — same
423
+ // enrichment contract as headings (disk counts when the directory exists),
424
+ // zero-pad-tolerant duplicate guard so an id declared in BOTH a heading and
425
+ // a table counts once.
426
+ const stripPadA = (s) => s.replace(/^0+(?=.)/, '');
427
+ const seen = new Set(phases.map((ph) => stripPadA(ph.number)));
428
+ for (const tr of collectTablePhaseRows(content)) {
429
+ if (seen.has(stripPadA(tr.id)))
430
+ continue;
431
+ const dirMatchA = matchPhaseDirs(phaseDirNames, normalizePhaseName(tr.id)).matches[0];
432
+ let tPlanCount = 0;
433
+ let tSummaryCount = 0;
434
+ let tHasContext = false;
435
+ let tHasResearch = false;
436
+ let tContextReadError = null;
437
+ if (dirMatchA) {
438
+ const counts = countPhasePlansAndSummaries(node_path_1.default.join(phasesDir, dirMatchA));
439
+ tPlanCount = counts.planCount;
440
+ tSummaryCount = counts.summaryCount;
441
+ tHasContext = node_fs_1.default.existsSync(node_path_1.default.join(phasesDir, dirMatchA, 'CONTEXT.md'));
442
+ tHasResearch = node_fs_1.default.existsSync(node_path_1.default.join(phasesDir, dirMatchA, 'RESEARCH.md'));
443
+ // #3885 (ADR-3473 §8.5): reuse the SAME countPhasePlansAndSummaries call's
444
+ // discriminator — this row's hasContext/hasResearch are read via a direct
445
+ // existsSync (which cannot itself distinguish EACCES from absent), but
446
+ // an unreadable phase directory is still surfaced via the sibling call.
447
+ tContextReadError = counts.contextReadError;
448
+ }
449
+ phases.push({
450
+ number: tr.id,
451
+ name: tr.name ?? `Phase ${tr.id}`,
452
+ goal: null,
453
+ mode: null,
454
+ depends_on: null,
455
+ plan_count: tPlanCount,
456
+ summary_count: tSummaryCount,
457
+ has_context: tHasContext,
458
+ has_research: tHasResearch,
459
+ disk_status: dirMatchA ? 'ok' : 'no_directory',
460
+ roadmap_complete: false,
461
+ context_read_error: tContextReadError,
383
462
  });
384
463
  }
464
+ return phases;
465
+ }
466
+ function cmdRoadmapAnalyze(cwd, raw) {
467
+ const roadmapPath = planningPaths(cwd).roadmap;
468
+ if (!node_fs_1.default.existsSync(roadmapPath)) {
469
+ output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw, undefined);
470
+ return;
471
+ }
472
+ const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
473
+ // #3184/#3165: use the scoped variant so a truncated window is a
474
+ // distinguishable signal in the output instead of a silent `phase_count: 0`
475
+ // indistinguishable from a genuinely empty milestone.
476
+ const { value: content, scope } = extractCurrentMilestoneScoped(rawContent, cwd);
477
+ const phasesDir = planningPaths(cwd).phases;
478
+ // Build phase directory lookup once (O(1) readdir instead of O(N) per phase)
479
+ // #3185 exemption reason (ADR-3180 Decision 4a): this is a heading->directory
480
+ // LOOKUP INDEX, not a milestone enumeration. It must see the PHYSICAL set so
481
+ // a heading already scoped by extractCurrentMilestoneScoped above can find
482
+ // its directory; filtering it through listMilestonePhaseDirs would scope
483
+ // the same set twice. #3882 (ADR-3473 §8.2): routed through the named
484
+ // "physical set, sentinels included" axis instead of a hand-rolled
485
+ // readdirSync — every heading matched below already excludes sentinel
486
+ // phase numbers via isSentinelPhaseId before it ever consults this list
487
+ // (collectAnalyzePhases), so a sentinel directory's presence here is
488
+ // output-invariant; this only removes the re-derivation, not the reason.
489
+ const _phaseDirNames = listAllPhaseDirs(phasesDir, { includeSentinels: true }).value;
490
+ // Scan the scoped milestone window for phase-detail headings and enrich each
491
+ // with its on-disk status. Extracted into `collectAnalyzePhases` (#3165) so
492
+ // the SAME enrichment re-runs on the fallback below — not a second copy.
493
+ let phases = collectAnalyzePhases(content, phasesDir, _phaseDirNames);
494
+ // `effectiveContent` is what the downstream checklist scan (missing_details)
495
+ // iterates. Defaults to the scoped window; switched to the fallback document
496
+ // when the recovery path below fires, so a phase found via fallback is not
497
+ // falsely reported as "in checklist but missing a detail section."
498
+ let effectiveContent = content;
499
+ // #3165: recover phase_count when the scoped window came back empty. A
500
+ // CLOSED milestone heading sitting between the active milestone heading and
501
+ // its own phase-detail sections closes `extractCurrentMilestoneScoped`'s
502
+ // window over prose only — `phases` is empty, and the consuming resume gate
503
+ // (`workflows/next.md` Route 0) iterates `.phases[]` so a safety invariant
504
+ // silently never runs. When the window is suspect (non-COMPLETE scope), the
505
+ // scoped scan found nothing, AND phase directories exist on disk (real
506
+ // evidence phases exist), re-scan the shipped-milestone-stripped document so
507
+ // the phase list reflects the real phases instead of a silent zero. The
508
+ // `scope` field retains its non-COMPLETE value downstream so consumers can
509
+ // still tell this is a best-effort count, not a cleanly scoped one. Position
510
+ // alone cannot attribute phases to the active vs the intervening closed
511
+ // milestone, so this never claims COMPLETE — it converts silence into a
512
+ // populated, flagged result.
513
+ if (phases.length === 0 && scope !== SCOPE.COMPLETE && _phaseDirNames.length > 0) {
514
+ const fallbackContent = stripShippedMilestones(rawContent);
515
+ const fallbackPhases = collectAnalyzePhases(fallbackContent, phasesDir, _phaseDirNames);
516
+ if (fallbackPhases.length > 0) {
517
+ phases = fallbackPhases;
518
+ effectiveContent = fallbackContent;
519
+ }
520
+ }
521
+ // Extract milestone info. #3216: routed through the canonical
522
+ // `listMilestoneHeadings` owner (deleted the inline `##…` regex, which
523
+ // truncated names at a parenthetical and had no phase-heading exclusion)
524
+ // rather than re-deriving the enumeration here.
525
+ const milestones = listMilestoneHeadings(content).map((m) => ({
526
+ heading: m.heading,
527
+ version: m.version,
528
+ }));
385
529
  // Find current and next phase
386
530
  const currentPhase = phases.find(p => p.disk_status === 'planned' || p.disk_status === 'partial') || null;
387
531
  const nextPhase = phases.find(p => p.disk_status === 'empty' || p.disk_status === 'no_directory' || p.disk_status === 'discussed' || p.disk_status === 'researched') || null;
@@ -399,11 +543,43 @@ function cmdRoadmapAnalyze(cwd, raw) {
399
543
  const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+([A-Za-z]?\d+[A-Z]?(?:[.-]\d+)*)/gi;
400
544
  const checklistPhases = new Set();
401
545
  let checklistMatch;
402
- while ((checklistMatch = checklistPattern.exec(content)) !== null) {
546
+ while ((checklistMatch = checklistPattern.exec(effectiveContent)) !== null) {
403
547
  checklistPhases.add(checklistMatch[1]);
404
548
  }
405
549
  const detailPhases = new Set(phases.map(p => p.number));
406
- const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p) && !isSentinelPhase(p));
550
+ const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p) && !isSentinelPhaseId(p));
551
+ // #3217 (ADR-3180 §7.6 rules 3-4): `progress_percent` used to accumulate
552
+ // `totalPlans`/`totalSummaries` above — a heading-matched enumeration
553
+ // (`phasePattern` over the milestone-windowed `content`) paired against
554
+ // `_phaseDirNames`, a DELIBERATELY unscoped physical directory listing
555
+ // (see its own comment above: it is a heading->directory lookup index,
556
+ // not a milestone enumeration). That set is not the same set
557
+ // `listMilestonePhaseDirs` scopes for `query progress` / `stats` (#3185
558
+ // Phase 3), so `progress_percent` could silently diverge from both siblings
559
+ // on the same project (rule 3). Route `progress_percent`'s own
560
+ // numerator/denominator through the single scoped owner instead — mirrors
561
+ // cmdProgressRender/cmdStats's own aggregation — and withhold the
562
+ // percentage entirely when THAT scope is not COMPLETE (rule 4), never
563
+ // returning `0` for "could not compute". This does not touch `total_plans`
564
+ // / `total_summaries` / `phases` / `completed_phases` above — those stay
565
+ // the heading-matched detail view; only `progress_percent`'s own inputs
566
+ // move onto the scoped owner.
567
+ let scopedTotalPlans = 0;
568
+ let scopedTotalSummaries = 0;
569
+ let progressScope = SCOPE.UNREADABLE;
570
+ try {
571
+ const { value: progressDirs, scope: scopedResult } = listMilestonePhaseDirs(phasesDir, { cwd });
572
+ progressScope = scopedResult;
573
+ for (const dir of progressDirs) {
574
+ const scan = scanPhasePlans(node_path_1.default.join(phasesDir, dir));
575
+ scopedTotalPlans += scan.planCount;
576
+ scopedTotalSummaries += scan.summaryCount;
577
+ }
578
+ }
579
+ catch { /* progressScope stays the pessimistic SCOPE.UNREADABLE default */ }
580
+ const progressPercent = progressScope === SCOPE.COMPLETE
581
+ ? (0, phase_lifecycle_cjs_1.clampPercent)(scopedTotalSummaries, scopedTotalPlans)
582
+ : null;
407
583
  const result = {
408
584
  milestones,
409
585
  phases,
@@ -411,13 +587,94 @@ function cmdRoadmapAnalyze(cwd, raw) {
411
587
  completed_phases: completedPhases,
412
588
  total_plans: totalPlans,
413
589
  total_summaries: totalSummaries,
414
- progress_percent: totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0,
590
+ progress_percent: progressPercent,
591
+ // #3217 finding 2: `progress_percent` is gated by a SECOND, independently
592
+ // computed `listMilestonePhaseDirs` scope (`progressScope` above) — not
593
+ // by the top-level `scope` field, which describes the heading-windowing
594
+ // identity `phases`/`total_plans`/`total_summaries`/`completed_phases`
595
+ // were built from. Those two scopes can legitimately disagree (e.g.
596
+ // `scope: "complete"` alongside a genuinely unreadable phases directory),
597
+ // and per the documented contract "scope tells you whether the counts
598
+ // are trustworthy", a consumer seeing `progress_percent: null` needs a
599
+ // field to tell WHY without reading source. Exposing `progress_scope`
600
+ // (rather than reconciling the two scopes into one, or re-deriving
601
+ // `total_plans`/`phases`/etc. from the scoped set) preserves the
602
+ // deliberate, already-documented choice a few lines up: `phases`/
603
+ // `total_plans`/`total_summaries`/`completed_phases` stay the
604
+ // heading-matched detail view (`_phaseDirNames` is a lookup index, not a
605
+ // milestone enumeration — see its comment); only `progress_percent`'s own
606
+ // inputs move onto the scoped owner.
607
+ progress_scope: progressScope,
415
608
  current_phase: currentPhase ? currentPhase.number : null,
416
609
  next_phase: nextPhase ? nextPhase.number : null,
417
610
  missing_phase_details: missingDetails.length > 0 ? missingDetails : null,
611
+ // #3184/#3165: distinguishes a genuinely empty milestone (`scope:
612
+ // "complete"`, `phase_count: 0`) from a window that could not be fully
613
+ // resolved (`"truncated"` / `"unscoped"` / `"unreadable"`) — those cases
614
+ // were previously output-identical.
615
+ scope,
418
616
  };
419
617
  output(result, raw, undefined);
420
618
  }
619
+ // ─── cmdRoadmapMilestoneScope ────────────────────────────────────────────────
620
+ /**
621
+ * #3262 (write-time milestone-scope guard): read-only probe emitting the
622
+ * current milestone window's IDENTITY — its scope classification and the
623
+ * phase ids it declares — so the edit-phase workflow can capture it before
624
+ * its in-place section write, re-derive it after, and roll back on any
625
+ * change. This is the milestone-scope sibling of the workflow's existing
626
+ * `depends_on` gate, expressed as a command because the workflow's write is
627
+ * assistant-driven free-text surgery, not a code path.
628
+ *
629
+ * Deliberately NOT `cmdRoadmapAnalyze`: analyze's #3165 recovery re-populates
630
+ * `phases` from the shipped-milestone-stripped document when the scoped
631
+ * window is suspect, which is right for a human-facing progress report and
632
+ * wrong for a before/after equality probe — the refill would mask exactly
633
+ * the narrowing this guard exists to detect. This probe reports the RAW
634
+ * window (`extractCurrentMilestoneScoped` + `scanMilestonePhaseIds`), no
635
+ * fallback, so a narrowed window is always visible as a changed phase set.
636
+ */
637
+ function cmdRoadmapMilestoneScope(cwd, raw) {
638
+ const roadmapPath = planningPaths(cwd).roadmap;
639
+ if (!node_fs_1.default.existsSync(roadmapPath)) {
640
+ output({ error: 'ROADMAP.md not found', scope: SCOPE.UNREADABLE, phases: [], phase_count: 0 }, raw, undefined);
641
+ return;
642
+ }
643
+ const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
644
+ // #3641: resolve phase_id_convention and thread it into the scope axis, so
645
+ // this probe and `roadmap validate`'s V005 answer the SAME question the
646
+ // SAME way for a bracket-convention project — a window the classifier
647
+ // calls TRUNCATED in validate must never read COMPLETE here (the #3262
648
+ // capture/compare guard consumes this scope). Resolution mirrors the
649
+ // validate router's: .planning/config.json first, ROADMAP.md frontmatter
650
+ // as fallback.
651
+ let phaseIdConvention;
652
+ try {
653
+ const cfg = loadConfigForScope(cwd);
654
+ phaseIdConvention = cfg['phase_id_convention'];
655
+ }
656
+ catch {
657
+ phaseIdConvention = undefined;
658
+ }
659
+ if (phaseIdConvention === undefined || phaseIdConvention === null) {
660
+ // Bounded per local/no-unbounded-quantifier (#2128): frontmatter is a
661
+ // short header block — 4KB is orders of magnitude beyond any real one.
662
+ const fmMatch = rawContent.match(/^---\r?\n([\s\S]{0,4000}?)\r?\n---/);
663
+ if (fmMatch) {
664
+ const kvMatch = fmMatch[1].match(/^phase_id_convention:\s*(.*)$/m);
665
+ if (kvMatch) {
666
+ const val = kvMatch[1].trim();
667
+ if (val !== 'null' && val !== '') {
668
+ phaseIdConvention = val.replace(/^["']|["']$/g, '');
669
+ }
670
+ }
671
+ }
672
+ }
673
+ const { value: window, scope } = extractCurrentMilestoneScoped(rawContent, cwd, undefined, phaseIdConvention);
674
+ // Document order (Set insertion order) — deterministic for a given document.
675
+ const phases = [...scanMilestonePhaseIds(window)];
676
+ output({ scope, phases, phase_count: phases.length }, raw, undefined);
677
+ }
421
678
  // ─── cmdRoadmapUpdatePlanProgress ─────────────────────────────────────────────
422
679
  /**
423
680
  * Scope a ROADMAP.md content string down to its "Progress table" writable
@@ -474,10 +731,34 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
474
731
  // completion date until the phase's verification status is 'passed', matching
475
732
  // cmdPhaseComplete's gate (phase.cts:1436). Previously the checkbox fired the
476
733
  // moment the last plan summary landed — before gsd-verifier had verified.
734
+ //
735
+ // ADR-3180 §7.4 (issue #3186, disk-strict): routed through the canonical
736
+ // owner (`isPhaseComplete`) instead of hand-rolling `summaryCount >=
737
+ // planCount && verificationPassed` locally — the owner calls
738
+ // readVerificationStatus UNCONDITIONALLY, so `isComplete` here always
739
+ // agrees with `roadmap analyze` / `init manager` / `phase complete` for
740
+ // the same phase (ADR-3180 §7.4's headline: one predicate for the read
741
+ // path and the write path).
477
742
  const phaseDir = node_path_1.default.join(cwd, phaseInfo.directory);
478
- const verificationResult = readVerificationStatus(phaseDir);
479
- const verificationPassed = verificationResult.status === 'passed';
480
- const isComplete = summaryCount >= planCount && verificationPassed;
743
+ const completionResult = isPhaseComplete(phaseDir);
744
+ const verificationResult = completionResult.value.verification;
745
+ // #2648 precedent, applied at this write site (ADR-3180 §7.4 / #3186):
746
+ // `isPhaseComplete` deliberately carries NO plan-count precondition — the
747
+ // owner's `complete` is exactly `verification.status === 'passed'`, and
748
+ // that must stay true (disk-strict: a zero-plan phase with a passing
749
+ // `*-VERIFICATION.md` IS complete, #3168). But `readVerificationStatus`'s
750
+ // staleness check only compares SUMMARY mtimes against the verification
751
+ // file — it has no idea a NEW plan was added after the file was written,
752
+ // so a still-fresh `passed` verification says nothing about a plan added
753
+ // afterward. This command WRITES a checkbox and a completion date into
754
+ // ROADMAP.md, a materially stronger claim than "verification passed" —
755
+ // mirroring cmdPhaseComplete's own fail-closed plan-coverage gate
756
+ // (phase.cts:~1995, #2648: "a coverage gate that passes when it cannot
757
+ // read the plans is no gate at all"), composed explicitly here rather than
758
+ // folded into the predicate: complete AND all plans executed.
759
+ const coverageScan = scanPhasePlans(phaseDir);
760
+ const unsummarizedPlans = findUnsummarizedPlans(coverageScan.planFiles, coverageScan.summaryFiles);
761
+ const isComplete = completionResult.value.complete && unsummarizedPlans.length === 0;
481
762
  // #3057 B3: routing above is unchanged (an indeterminate staleness check
482
763
  // still routes as if nothing were stale) — this only makes the fact visible
483
764
  // to whatever reads this command's JSON output.
@@ -539,26 +820,65 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
539
820
  // `**Plans:** N plans` — bold "Plans:" (colon inside bold)
540
821
  // `Plans: N plans` — plain text header
541
822
  //
542
- // #2853: the verb owns the count token ONLY — it must not destroy hand-written
543
- // prose a human placed after the count (e.g. "(11-16 are gap closure ...)").
544
- // Group $1 = phase header → `Plans:` label + trailing whitespace (unchanged).
545
- // Group $2 = the existing count token to replace: matches `N/N plans complete`,
546
- // `N/N plans executed`, or the bare template `N plans` form. Group $3 = whatever
547
- // else is on the line (`[^\r\n]*`, so CRLF `\r` is preserved).
823
+ // #2853 / #3584: the verb owns the count token ONLY — it must not destroy
824
+ // hand-written prose a human placed on the line. Group $1 = phase header →
825
+ // `Plans:` label + trailing whitespace (unchanged). Group $2 = the existing
826
+ // count token to replace: matches `N/N plans complete`, `N/N plans executed`,
827
+ // or the bare template `N plan(s)` form — singular is part of the tool's OWN
828
+ // grammar (gsd-core/templates/roadmap.md:62 ships `**Plans**: 1 plan` as the
829
+ // documented one-plan-phase shape), so the `s` is optional there (bug #3584
830
+ // Finding B; pre-fix a bare `1 plan` fell into the drop-everything path and
831
+ // was accidentally overwritten with the correct count — post-fix it must be
832
+ // recognised as a token in its own right or it freezes stale forever). Group
833
+ // $3 = whatever else is on the line (`[^\r\n]*`, so a CRLF `\r` is never part
834
+ // of the match and rides along untouched in the unmatched remainder of the
835
+ // string — never stranded, never duplicated).
548
836
  //
549
- // Trailing text is preserved ONLY when a real count token ($2) was present —
550
- // i.e. an annotation a human wrote after a real count. When $2 is absent the
551
- // line is the fresh-template bracketed placeholder (`[Number of plans…]`) or
552
- // other freeform guidance, not user prose: the count replaces the whole token,
553
- // preserving the pre-#2853 clean-output behaviour on the template path.
554
- const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)(\\d+\\s*\\/\\s*\\d+\\s+plans(?:\\s+(?:complete|executed))?|\\d+\\s+plans)?([^\\r\\n]*)`, 'i');
837
+ // Three arms, in order:
838
+ // 1. $2 present (a real count token) → rewrite the token, preserve $3
839
+ // verbatim (an annotation a human wrote after a real count; #2853).
840
+ // 2. $2 absent AND $3, trimmed, is the fresh-template PLACEHOLDER shipped
841
+ // by gsd-core/templates/roadmap.md — either
842
+ // `[Number of plans, e.g., "3 plans" or "TBD"]` (line 37) or
843
+ // `[Number of plans]` (lines 51/75/88) → replace it with the computed
844
+ // count. Detected POSITIVELY on the distinctive `Number of plans`
845
+ // wording (anchored, case-insensitive), NEVER on "wholly bracketed" —
846
+ // a bracketed HUMAN annotation such as `[Deferred pending re-scope]`
847
+ // is structurally identical but must be arm-3 preserved (bug #3584
848
+ // Finding A).
849
+ // 3. Anything else (freeform prose, `TBD` / `TBD — annotation`, a
850
+ // bracketed human note, the first line of a wrapped sentence, an
851
+ // empty value) → leave the whole matched line untouched by returning
852
+ // `_match` unchanged. An untouched first line cannot orphan its own
853
+ // continuation on the next line, since the pattern never spans past
854
+ // `\n` in the first place.
855
+ const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)(\\d+\\s*\\/\\s*\\d+\\s+plans(?:\\s+(?:complete|executed))?|\\d+\\s+plans?)?([^\\r\\n]*)`, 'i');
555
856
  const planCountText = isComplete
556
857
  ? `${summaryCount}/${planCount} plans complete`
557
858
  : `${summaryCount}/${planCount} plans executed`;
859
+ // Positive detector for the fresh-template placeholder ONLY (bug #3584
860
+ // Finding A). Anchored to the distinctive `Number of plans` wording that
861
+ // gsd-core/templates/roadmap.md actually ships, not to "anything in
862
+ // brackets" — a bracketed human annotation like `[Deferred pending
863
+ // re-scope]` is structurally bracketed too but carries none of this
864
+ // wording, so it correctly falls through to arm 3 untouched.
865
+ const isTemplatePlaceholder = (value) => {
866
+ const trimmed = value.trim();
867
+ return /^\[\s*Number of plans\b[\s\S]*\]$/i.test(trimmed);
868
+ };
558
869
  roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, (_match, label, existingCount, trailing) => {
559
- // Preserve trailing text only when a real count preceded it.
560
- const suffix = existingCount ? trailing : '';
561
- return `${label}${planCountText}${suffix}`;
870
+ if (existingCount) {
871
+ // Arm 1: real count token — rewrite it, preserve the trailing annotation.
872
+ return `${label}${planCountText}${trailing}`;
873
+ }
874
+ if (isTemplatePlaceholder(trailing)) {
875
+ // Arm 2: fresh-template placeholder — replace with the count.
876
+ return `${label}${planCountText}`;
877
+ }
878
+ // Arm 3: freeform prose, TBD, a bracketed human annotation, a wrapped
879
+ // sentence's first line, or an empty value — leave the line exactly as
880
+ // it was.
881
+ return _match;
562
882
  });
563
883
  // If complete: check checkbox
564
884
  if (isComplete) {
@@ -570,7 +890,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
570
890
  const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
571
891
  if (!planId)
572
892
  continue;
573
- const planEscaped = escapeRegex(planId);
893
+ const planEscaped = (0, pattern_cjs_1.escapeRegex)(planId);
574
894
  const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
575
895
  roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
576
896
  }
@@ -594,7 +914,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
594
914
  // archived <details> block is still correctly detected as missing from the
595
915
  // active milestone section.
596
916
  const missingPlans = phaseInfo.plans.filter((planFile) => {
597
- const planEscaped = escapeRegex(planFile);
917
+ const planEscaped = (0, pattern_cjs_1.escapeRegex)(planFile);
598
918
  return !new RegExp(`-\\s*\\[[x ]\\]\\s*(?:\\*\\*)?${planEscaped}`, 'i').test(activeRegion);
599
919
  });
600
920
  if (missingPlans.length > 0) {
@@ -642,7 +962,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
642
962
  const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
643
963
  if (!planId)
644
964
  continue;
645
- const planEscaped = escapeRegex(planId);
965
+ const planEscaped = (0, pattern_cjs_1.escapeRegex)(planId);
646
966
  const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
647
967
  roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
648
968
  }
@@ -747,6 +1067,11 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
747
1067
  let updated = false;
748
1068
  withPlanningLock(cwd, () => {
749
1069
  const content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
1070
+ // #3413: preserve the file's own EOL style when the checklist block below
1071
+ // is rebuilt and spliced back in — splitLines() cleans each captured line
1072
+ // of any dangling \r, so rejoining with a bare '\n' would silently
1073
+ // downgrade a CRLF ROADMAP.md's rewritten block to LF only.
1074
+ const eol = (0, text_lines_cjs_1.detectEol)(content);
750
1075
  // Find the phase section.
751
1076
  // #3537: padding-tolerant fragment so the caller's resolved padded id
752
1077
  // matches un-padded ROADMAP headings.
@@ -776,12 +1101,20 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
776
1101
  // Review fix (F2): `(?:^|\n)` anchors the match to start-of-line so mid-line
777
1102
  // occurrences like `***Plans:***` embedded in a sentence or `OpenPlans: foo`
778
1103
  // do not trigger a false match. Groups 1 and 2 retain the same semantics.
779
- const plansBlockMatch = phaseSection.match(/(?:^|\n)(\*{0,2}Plans\*{0,2}:[^\n]*\n)((?:\s*-\s*\[[ x]\][^\n]*\n?)+)/i);
1104
+ // #3415: empirically verified linear-time to 10.9MB / 320,000 lines of adversarial
1105
+ // checklist input (0.31ms@1000 lines -> 8.4ms@320,000 lines). The outer `+` group has
1106
+ // no trailing constraint after it in the pattern, so a successful greedy pass never
1107
+ // needs to explore alternate `\r?\n?` boundary partitions to satisfy something later —
1108
+ // it accepts the first complete parse and stops, which rules out the #2128-class
1109
+ // ambiguous-boundary blowup despite the nested-quantifier shape. Non-global match on
1110
+ // already phase-sliced content, not the whole file.
1111
+ // eslint-disable-next-line local/no-unbounded-quantifier -- outer `+` has no trailing constraint to force re-partitioning; measured linear to 10.9MB
1112
+ const plansBlockMatch = phaseSection.match(/(?:^|\r?\n)(\*{0,2}Plans\*{0,2}:[^\r\n]*\r?\n)((?:\s*-\s*\[[ x]\][^\r\n]*\r?\n?)+)/i);
780
1113
  if (!plansBlockMatch)
781
1114
  return;
782
1115
  const plansHeader = plansBlockMatch[1];
783
1116
  const existingList = plansBlockMatch[2];
784
- const listLines = existingList.split('\n').filter(l => /^\s*-\s*\[/.test(l));
1117
+ const listLines = (0, text_lines_cjs_1.splitLines)(existingList).filter(l => /^\s*-\s*\[/.test(l));
785
1118
  if (listLines.length === 0)
786
1119
  return;
787
1120
  // #314 perf: build a first-wins Map so per-line lookup is O(1) instead of O(plans).
@@ -834,11 +1167,22 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
834
1167
  annotatedLines.push(`- ${t}`);
835
1168
  }
836
1169
  }
837
- const newListBlock = annotatedLines.join('\n') + '\n';
838
- // #1103: when `(?:^|\n)` consumed a leading `\n` (mid-string match), re-emit it
839
- // so the line preceding the Plans: header is not fused onto it.
840
- const leadingNewline = plansBlockMatch[0].startsWith('\n') ? '\n' : '';
841
- const newPhaseSection = phaseSection.replace(plansBlockMatch[0], leadingNewline + plansHeader + newListBlock);
1170
+ const newListBlock = (0, text_lines_cjs_1.joinLines)(annotatedLines, eol) + eol;
1171
+ // #1103: when `(?:^|\r?\n)` consumed a leading terminator (mid-string
1172
+ // match), re-emit it verbatim so the line preceding the Plans: header is
1173
+ // not fused onto it. #3413: the widened `(?:^|\r?\n)` can now consume a
1174
+ // 2-char `\r\n` — re-emit whatever was actually captured (`''`, `'\n'`,
1175
+ // or `'\r\n'`), not a hardcoded `'\n'`, or a CRLF file loses its `\r`.
1176
+ const leadingMatch = /^\r?\n/.exec(plansBlockMatch[0]);
1177
+ const leadingNewline = leadingMatch ? leadingMatch[0] : '';
1178
+ // Review fix (#3413 security): use the FUNCTION-replacement form. The
1179
+ // string-replacement form expands String#replace's special patterns
1180
+ // (`$&`, `` $` ``, `$'`, `$$`, `$1`-`$9`) inside the replacement — and
1181
+ // newListBlock is built from author-controlled truths/plan-file content,
1182
+ // so a line containing a literal `` $` `` (etc.) would splice unrelated
1183
+ // surrounding phaseSection text into the result. A function replacer is
1184
+ // never pattern-interpreted.
1185
+ const newPhaseSection = phaseSection.replace(plansBlockMatch[0], () => leadingNewline + plansHeader + newListBlock);
842
1186
  const nextContent = content.slice(0, phaseStart) + newPhaseSection + content.slice(phaseEnd);
843
1187
  if (nextContent === content)
844
1188
  return;
@@ -856,6 +1200,8 @@ module.exports = {
856
1200
  cmdRoadmapGetPhase,
857
1201
  getRoadmapPhaseWithFallback,
858
1202
  cmdRoadmapAnalyze,
1203
+ cmdRoadmapMilestoneScope,
859
1204
  cmdRoadmapUpdatePlanProgress,
860
1205
  cmdRoadmapAnnotateDependencies,
1206
+ buildPhaseHeadingRegex,
861
1207
  };