@opengsd/gsd-core 1.9.1 → 1.11.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 (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -12,20 +12,26 @@ 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
19
  const { output, error } = 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 } = 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");
@@ -34,13 +40,13 @@ const { planningPaths, withPlanningLock, findContextMdIn } = planningWorkspace;
34
40
  const scanPhasePlans = require("./plan-scan.cjs");
35
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports
36
42
  const coreUtils = require("./core-utils.cjs");
37
- const { countMatchedSummaries } = coreUtils;
43
+ const { countMatchedSummaries, findUnsummarizedPlans } = coreUtils;
38
44
  // eslint-disable-next-line @typescript-eslint/no-require-imports
39
45
  const frontmatter = require("./frontmatter.cjs");
40
46
  const { extractFrontmatter, parseMustHavesBlock } = frontmatter;
41
47
  // eslint-disable-next-line @typescript-eslint/no-require-imports
42
48
  const verificationMod = require("./verification.cjs");
43
- const { readVerificationStatus } = verificationMod;
49
+ const { isPhaseComplete } = verificationMod;
44
50
  // ─── coerceTruthToString ──────────────────────────────────────────────────────
45
51
  /**
46
52
  * Coerce an arbitrary YAML scalar/object into a string for cross-cutting
@@ -87,23 +93,37 @@ function countPhasePlansAndSummaries(phaseDir) {
87
93
  phaseFiles = node_fs_1.default.readdirSync(phaseDir);
88
94
  }
89
95
  catch { /* empty */ }
96
+ // #3511: scope the raw listing to this phase dir before the
97
+ // phase-numbered-artifact predicates (hasContext/hasResearch) — planCount/
98
+ // summaryCount above stay on scanPhasePlans's own unscoped listing since a
99
+ // PLAN/SUMMARY leading number is a plan sequence number, not a phase
100
+ // number. Mirrors core-utils.cts's getPhaseFileStats.
101
+ const scopedFiles = scopeToPhase(phaseFiles, node_path_1.default.basename(phaseDir));
90
102
  return {
91
103
  planCount,
92
104
  summaryCount,
93
- hasContext: findContextMdIn(phaseFiles) !== null,
94
- hasResearch: phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
105
+ hasContext: findContextMdIn(scopedFiles) !== null,
106
+ hasResearch: scopedFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
95
107
  };
96
108
  }
97
109
  // `phaseMarkdownRegexSource` lives in phase-id.cjs (#3537) and is imported above.
98
110
  // ─── searchPhaseInContent ─────────────────────────────────────────────────────
111
+ /**
112
+ * Build the phase-heading regex used by `searchPhaseInContent` for a given
113
+ * pre-escaped phase source. Extracted (#3412) so tests can assert against the
114
+ * exact production pattern instead of hand-duplicating it.
115
+ * #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
116
+ */
117
+ function buildPhaseHeadingRegex(escapedPhase) {
118
+ return new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
119
+ }
99
120
  /**
100
121
  * Search for a phase header (and its section) within the given content string.
101
122
  * Returns a result object if found (either a full match or a malformed_roadmap
102
123
  * checklist-only match), or null if the phase is not present at all.
103
124
  */
104
125
  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');
126
+ const headingPattern = buildPhaseHeadingRegex(escapedPhase);
107
127
  const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
108
128
  const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
109
129
  const headerMatch = headingIndex === -1 ? null : headings[headingIndex].text.match(headingPattern);
@@ -173,7 +193,8 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
173
193
  * phase resolution as `roadmap.get-phase` — not a milestone-only subset.
174
194
  */
175
195
  function getRoadmapPhaseWithFallback(cwd, phaseNum) {
176
- if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum)))
196
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
197
+ if (isSentinelPhaseId(stripProjectCodePrefix(phaseNum)))
177
198
  return null;
178
199
  const roadmapPath = planningPaths(cwd).roadmap;
179
200
  // Read directly rather than gating on fs.existsSync: existsSync returns false
@@ -206,7 +227,8 @@ function getRoadmapPhaseWithFallback(cwd, phaseNum) {
206
227
  }
207
228
  // ─── cmdRoadmapGetPhase ───────────────────────────────────────────────────────
208
229
  function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
209
- if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum))) {
230
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
231
+ if (isSentinelPhaseId(stripProjectCodePrefix(phaseNum))) {
210
232
  output({ found: false, phase_number: phaseNum }, raw, '');
211
233
  return;
212
234
  }
@@ -242,6 +264,17 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
242
264
  if (!malformed)
243
265
  malformed = (milestoneResult?.error ? milestoneResult : (fullResult?.error ? fullResult : null));
244
266
  }
267
+ // #3577: no heading or checklist entry matched — fall back to a
268
+ // markdown-table row declaration (the same last-resort tier
269
+ // getRoadmapPhaseInternal gained). Zero-pad-tolerant id compare (#3572
270
+ // lesson: the declared form may be padded).
271
+ const stripPad = (s) => s.replace(/^0+(?=.)/, '');
272
+ const tableHit = collectTablePhaseRows(milestoneContent).find((tr) => stripPad(tr.id) === stripPad(phaseNum))
273
+ ?? collectTablePhaseRows(fullContent).find((tr) => stripPad(tr.id) === stripPad(phaseNum));
274
+ if (tableHit) {
275
+ output({ found: true, phase_number: phaseNum, phase_name: tableHit.name ?? `Phase ${tableHit.id}`, goal: null, section: tableHit.row.trim() }, raw, tableHit.row.trim());
276
+ return;
277
+ }
245
278
  if (malformed) {
246
279
  output(malformed, raw, '');
247
280
  return;
@@ -252,53 +285,38 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
252
285
  error('Failed to read ROADMAP.md: ' + e.message);
253
286
  }
254
287
  }
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;
288
+ /**
289
+ * #3165: scan `content` for phase-detail headings (`##/###/#### Phase N: Name`)
290
+ * and enrich each with its on-disk plan/summary/completion status and ROADMAP
291
+ * checkbox. Pure extraction over `content` + the pre-built `phaseDirNames`
292
+ * lookup index — no milestone windowing of its own; the caller chooses the
293
+ * content (scoped window or fallback). Extracted verbatim from
294
+ * `cmdRoadmapAnalyze`'s former inline loop so the fallback re-runs the EXACT
295
+ * same enrichment, not a second derivation.
296
+ */
297
+ function collectAnalyzePhases(content, phasesDir, phaseDirNames) {
265
298
  // Extract all phase headings: ## Phase N: Name or ### Phase N: Name
266
299
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
267
300
  // 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.
268
- const phasePattern = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)(?:\s*\([^)\n]{0,200}\))?\s*:\s*([^\n]+)/gi;
301
+ // #3036: widen the id capture to accept non-numeric-leading ids (e.g. B7, P0.3-2)
302
+ // that get-phase/execute-phase already resolve. An optional leading letter prefix
303
+ // ([A-Za-z]?) covers letter-prefixed ids without breaking numeric-leading ones.
304
+ // 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.
305
+ 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;
269
306
  const phases = [];
270
307
  let match;
271
- // Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not real
272
- // phases. They legitimately have no directory and must never be surfaced as
273
- // current/next phase or counted in phase_count. Mirrors the engine-wide
274
- // sentinel convention (phase-id getMilestoneFromPhaseId, roadmap-command-router
275
- // SENTINELS, the #1445 /^999/ progress filters). (#1580)
276
- const isSentinelPhase = (num) => {
277
- const major = parseInt(num, 10);
278
- return major === 0 || major === 999;
279
- };
280
- // Build phase directory lookup once (O(1) readdir instead of O(N) per phase)
281
- const _phaseDirNames = (() => {
282
- try {
283
- return node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
284
- .filter(e => e.isDirectory())
285
- .map(e => e.name);
286
- }
287
- catch {
288
- return [];
289
- }
290
- })();
291
308
  while ((match = phasePattern.exec(content)) !== null) {
292
309
  const phaseNum = match[1];
293
- if (isSentinelPhase(phaseNum))
310
+ if (isSentinelPhaseId(phaseNum))
294
311
  continue;
295
312
  const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
296
313
  // Extract goal from the section
297
314
  const sectionStart = match.index;
298
315
  const restOfContent = content.slice(sectionStart);
299
316
  // #3691: `\d` → `\d[\d.]*` so decimal phase headings (e.g. `### Phase 02.3:`) are
300
- // recognised as section boundaries.
301
- const nextHeader = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]{1,200}\]\s*)?Phase\s+\d[\d.-]*/i);
317
+ // recognised as section boundaries. #3036: `[A-Za-z]?\d` so non-numeric-leading ids
318
+ // (e.g. B7) are also recognised.
319
+ const nextHeader = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]{1,200}\]\s*)?Phase\s+[A-Za-z]?\d[\d.-]*/i);
302
320
  const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
303
321
  const section = content.slice(sectionStart, sectionEnd);
304
322
  const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
@@ -314,20 +332,27 @@ function cmdRoadmapAnalyze(cwd, raw) {
314
332
  let summaryCount = 0;
315
333
  let hasContext = false;
316
334
  let hasResearch = false;
317
- // DEAD catch removed (#2245 audit): _phaseDirNames.find(...) is a pure
335
+ // DEAD catch removed (#2245 audit): matchPhaseDirs(...) is a pure
318
336
  // array lookup on an already-resolved string array, and
319
337
  // countPhasePlansAndSummaries is itself fully defensive (its own
320
338
  // readdirSync is self-guarded, and it delegates to scanPhasePlans, which
321
339
  // never throws) — nothing in this block can throw, so the try/catch could
322
340
  // never be triggered.
323
- const dirMatch = _phaseDirNames.find(d => phaseTokenMatches(d, normalized));
341
+ const dirMatch = matchPhaseDirs(phaseDirNames, normalized).matches[0];
324
342
  if (dirMatch) {
325
343
  const counts = countPhasePlansAndSummaries(node_path_1.default.join(phasesDir, dirMatch));
326
344
  planCount = counts.planCount;
327
345
  summaryCount = counts.summaryCount;
328
346
  hasContext = counts.hasContext;
329
347
  hasResearch = counts.hasResearch;
330
- if (summaryCount >= planCount && planCount > 0)
348
+ // ADR-3180 §7.4 (issue #3186, disk-strict, #3168 fix): route "is this
349
+ // phase complete" through the canonical owner (`isPhaseComplete`),
350
+ // which calls readVerificationStatus UNCONDITIONALLY — plan count is
351
+ // NOT a precondition, so a zero-plan phase with a passing
352
+ // `*-VERIFICATION.md` reports complete here too, not just via
353
+ // `phase.complete`.
354
+ const completionResult = isPhaseComplete(node_path_1.default.join(phasesDir, dirMatch));
355
+ if (completionResult.value.complete)
331
356
  diskStatus = 'complete';
332
357
  else if (summaryCount > 0)
333
358
  diskStatus = 'partial';
@@ -340,19 +365,23 @@ function cmdRoadmapAnalyze(cwd, raw) {
340
365
  else
341
366
  diskStatus = 'empty';
342
367
  }
343
- // Check ROADMAP checkbox status.
344
- // #3537: padding-tolerant fragment — the heading discovered above may use
345
- // a different padding than the summary-bullet checkbox below it (mixed
346
- // padding inside one ROADMAP is legal and seen in real projects).
368
+ // Check ROADMAP checkbox status. #3537: padding-tolerant fragment — the
369
+ // heading discovered above may use a different padding than the
370
+ // summary-bullet checkbox below it (mixed padding inside one ROADMAP is
371
+ // legal and seen in real projects).
372
+ //
373
+ // ADR-3180 §7.4 (disk-strict, #2957, maintainer decision 2026-08-08):
374
+ // `roadmapComplete` is reported below as metadata ONLY — it carries NO
375
+ // machine authority over `diskStatus`. The override that used to trust a
376
+ // ticked checkbox over disk file structure is DELETED, not generalized
377
+ // (#2957: "a ticked ROADMAP checkbox is a human annotation with no
378
+ // machine authority"). A phase marked complete solely by a ticked
379
+ // checkbox — no passing `*-VERIFICATION.md`, plans outstanding — now
380
+ // reports incomplete; this is the deliberate Tier-2 break (ADR-3180 §7.4
381
+ // Decision 3).
347
382
  const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseMarkdownRegexSource(phaseNum)}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
348
383
  const checkboxMatch = content.match(checkboxPattern);
349
384
  const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
350
- // If roadmap marks phase complete, trust that over disk file structure.
351
- // Phases completed before GSD tracking (or via external tools) may lack
352
- // the standard PLAN/SUMMARY pairs but are still done.
353
- if (roadmapComplete && diskStatus !== 'complete') {
354
- diskStatus = 'complete';
355
- }
356
385
  phases.push({
357
386
  number: phaseNum,
358
387
  name: phaseName,
@@ -367,16 +396,110 @@ function cmdRoadmapAnalyze(cwd, raw) {
367
396
  roadmap_complete: roadmapComplete,
368
397
  });
369
398
  }
370
- // Extract milestone info
371
- const milestones = [];
372
- const milestonePattern = /##\s*(.*v(\d+(?:\.\d+)+)[^(\n]*)/gi;
373
- let mMatch;
374
- while ((mMatch = milestonePattern.exec(content)) !== null) {
375
- milestones.push({
376
- heading: mMatch[1].trim(),
377
- version: 'v' + mMatch[2],
399
+ // #3577: markdown-table row declarations join the enumeration — same
400
+ // enrichment contract as headings (disk counts when the directory exists),
401
+ // zero-pad-tolerant duplicate guard so an id declared in BOTH a heading and
402
+ // a table counts once.
403
+ const stripPadA = (s) => s.replace(/^0+(?=.)/, '');
404
+ const seen = new Set(phases.map((ph) => stripPadA(ph.number)));
405
+ for (const tr of collectTablePhaseRows(content)) {
406
+ if (seen.has(stripPadA(tr.id)))
407
+ continue;
408
+ const dirMatchA = matchPhaseDirs(phaseDirNames, normalizePhaseName(tr.id)).matches[0];
409
+ let tPlanCount = 0;
410
+ let tSummaryCount = 0;
411
+ let tHasContext = false;
412
+ let tHasResearch = false;
413
+ if (dirMatchA) {
414
+ const counts = countPhasePlansAndSummaries(node_path_1.default.join(phasesDir, dirMatchA));
415
+ tPlanCount = counts.planCount;
416
+ tSummaryCount = counts.summaryCount;
417
+ tHasContext = node_fs_1.default.existsSync(node_path_1.default.join(phasesDir, dirMatchA, 'CONTEXT.md'));
418
+ tHasResearch = node_fs_1.default.existsSync(node_path_1.default.join(phasesDir, dirMatchA, 'RESEARCH.md'));
419
+ }
420
+ phases.push({
421
+ number: tr.id,
422
+ name: tr.name ?? `Phase ${tr.id}`,
423
+ goal: null,
424
+ mode: null,
425
+ depends_on: null,
426
+ plan_count: tPlanCount,
427
+ summary_count: tSummaryCount,
428
+ has_context: tHasContext,
429
+ has_research: tHasResearch,
430
+ disk_status: dirMatchA ? 'ok' : 'no_directory',
431
+ roadmap_complete: false,
378
432
  });
379
433
  }
434
+ return phases;
435
+ }
436
+ function cmdRoadmapAnalyze(cwd, raw) {
437
+ const roadmapPath = planningPaths(cwd).roadmap;
438
+ if (!node_fs_1.default.existsSync(roadmapPath)) {
439
+ output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw, undefined);
440
+ return;
441
+ }
442
+ const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
443
+ // #3184/#3165: use the scoped variant so a truncated window is a
444
+ // distinguishable signal in the output instead of a silent `phase_count: 0`
445
+ // indistinguishable from a genuinely empty milestone.
446
+ const { value: content, scope } = extractCurrentMilestoneScoped(rawContent, cwd);
447
+ const phasesDir = planningPaths(cwd).phases;
448
+ // Build phase directory lookup once (O(1) readdir instead of O(N) per phase)
449
+ // #3185 exemption (documented reason, not a file allowlist — ADR-3180
450
+ // Decision 4a): this is a heading->directory LOOKUP INDEX, not a milestone
451
+ // enumeration. It must see the PHYSICAL set so a heading already scoped by
452
+ // extractCurrentMilestoneScoped above can find its directory; filtering it
453
+ // through listMilestonePhaseDirs would scope the same set twice.
454
+ const _phaseDirNames = (() => {
455
+ try {
456
+ return node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
457
+ .filter(e => e.isDirectory())
458
+ .map(e => e.name);
459
+ }
460
+ catch {
461
+ return [];
462
+ }
463
+ })();
464
+ // Scan the scoped milestone window for phase-detail headings and enrich each
465
+ // with its on-disk status. Extracted into `collectAnalyzePhases` (#3165) so
466
+ // the SAME enrichment re-runs on the fallback below — not a second copy.
467
+ let phases = collectAnalyzePhases(content, phasesDir, _phaseDirNames);
468
+ // `effectiveContent` is what the downstream checklist scan (missing_details)
469
+ // iterates. Defaults to the scoped window; switched to the fallback document
470
+ // when the recovery path below fires, so a phase found via fallback is not
471
+ // falsely reported as "in checklist but missing a detail section."
472
+ let effectiveContent = content;
473
+ // #3165: recover phase_count when the scoped window came back empty. A
474
+ // CLOSED milestone heading sitting between the active milestone heading and
475
+ // its own phase-detail sections closes `extractCurrentMilestoneScoped`'s
476
+ // window over prose only — `phases` is empty, and the consuming resume gate
477
+ // (`workflows/next.md` Route 0) iterates `.phases[]` so a safety invariant
478
+ // silently never runs. When the window is suspect (non-COMPLETE scope), the
479
+ // scoped scan found nothing, AND phase directories exist on disk (real
480
+ // evidence phases exist), re-scan the shipped-milestone-stripped document so
481
+ // the phase list reflects the real phases instead of a silent zero. The
482
+ // `scope` field retains its non-COMPLETE value downstream so consumers can
483
+ // still tell this is a best-effort count, not a cleanly scoped one. Position
484
+ // alone cannot attribute phases to the active vs the intervening closed
485
+ // milestone, so this never claims COMPLETE — it converts silence into a
486
+ // populated, flagged result.
487
+ if (phases.length === 0 && scope !== SCOPE.COMPLETE && _phaseDirNames.length > 0) {
488
+ const fallbackContent = stripShippedMilestones(rawContent);
489
+ const fallbackPhases = collectAnalyzePhases(fallbackContent, phasesDir, _phaseDirNames);
490
+ if (fallbackPhases.length > 0) {
491
+ phases = fallbackPhases;
492
+ effectiveContent = fallbackContent;
493
+ }
494
+ }
495
+ // Extract milestone info. #3216: routed through the canonical
496
+ // `listMilestoneHeadings` owner (deleted the inline `##…` regex, which
497
+ // truncated names at a parenthetical and had no phase-heading exclusion)
498
+ // rather than re-deriving the enumeration here.
499
+ const milestones = listMilestoneHeadings(content).map((m) => ({
500
+ heading: m.heading,
501
+ version: m.version,
502
+ }));
380
503
  // Find current and next phase
381
504
  const currentPhase = phases.find(p => p.disk_status === 'planned' || p.disk_status === 'partial') || null;
382
505
  const nextPhase = phases.find(p => p.disk_status === 'empty' || p.disk_status === 'no_directory' || p.disk_status === 'discussed' || p.disk_status === 'researched') || null;
@@ -389,14 +512,48 @@ function cmdRoadmapAnalyze(cwd, raw) {
389
512
  // IDs (e.g. `1-01`) match the detail-heading scanner above; otherwise they truncate
390
513
  // at the dash (`1-01` -> `1`) and every such phase reports a phantom missing detail.
391
514
  // 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.
392
- const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)/gi;
515
+ // #3036: widen to accept non-numeric-leading ids (same widening as the detail-heading pattern above).
516
+ // 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.
517
+ const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+([A-Za-z]?\d+[A-Z]?(?:[.-]\d+)*)/gi;
393
518
  const checklistPhases = new Set();
394
519
  let checklistMatch;
395
- while ((checklistMatch = checklistPattern.exec(content)) !== null) {
520
+ while ((checklistMatch = checklistPattern.exec(effectiveContent)) !== null) {
396
521
  checklistPhases.add(checklistMatch[1]);
397
522
  }
398
523
  const detailPhases = new Set(phases.map(p => p.number));
399
- const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p) && !isSentinelPhase(p));
524
+ const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p) && !isSentinelPhaseId(p));
525
+ // #3217 (ADR-3180 §7.6 rules 3-4): `progress_percent` used to accumulate
526
+ // `totalPlans`/`totalSummaries` above — a heading-matched enumeration
527
+ // (`phasePattern` over the milestone-windowed `content`) paired against
528
+ // `_phaseDirNames`, a DELIBERATELY unscoped physical directory listing
529
+ // (see its own comment above: it is a heading->directory lookup index,
530
+ // not a milestone enumeration). That set is not the same set
531
+ // `listMilestonePhaseDirs` scopes for `query progress` / `stats` (#3185
532
+ // Phase 3), so `progress_percent` could silently diverge from both siblings
533
+ // on the same project (rule 3). Route `progress_percent`'s own
534
+ // numerator/denominator through the single scoped owner instead — mirrors
535
+ // cmdProgressRender/cmdStats's own aggregation — and withhold the
536
+ // percentage entirely when THAT scope is not COMPLETE (rule 4), never
537
+ // returning `0` for "could not compute". This does not touch `total_plans`
538
+ // / `total_summaries` / `phases` / `completed_phases` above — those stay
539
+ // the heading-matched detail view; only `progress_percent`'s own inputs
540
+ // move onto the scoped owner.
541
+ let scopedTotalPlans = 0;
542
+ let scopedTotalSummaries = 0;
543
+ let progressScope = SCOPE.UNREADABLE;
544
+ try {
545
+ const { value: progressDirs, scope: scopedResult } = listMilestonePhaseDirs(phasesDir, { cwd });
546
+ progressScope = scopedResult;
547
+ for (const dir of progressDirs) {
548
+ const scan = scanPhasePlans(node_path_1.default.join(phasesDir, dir));
549
+ scopedTotalPlans += scan.planCount;
550
+ scopedTotalSummaries += scan.summaryCount;
551
+ }
552
+ }
553
+ catch { /* progressScope stays the pessimistic SCOPE.UNREADABLE default */ }
554
+ const progressPercent = progressScope === SCOPE.COMPLETE
555
+ ? (0, phase_lifecycle_cjs_1.clampPercent)(scopedTotalSummaries, scopedTotalPlans)
556
+ : null;
400
557
  const result = {
401
558
  milestones,
402
559
  phases,
@@ -404,13 +561,65 @@ function cmdRoadmapAnalyze(cwd, raw) {
404
561
  completed_phases: completedPhases,
405
562
  total_plans: totalPlans,
406
563
  total_summaries: totalSummaries,
407
- progress_percent: totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0,
564
+ progress_percent: progressPercent,
565
+ // #3217 finding 2: `progress_percent` is gated by a SECOND, independently
566
+ // computed `listMilestonePhaseDirs` scope (`progressScope` above) — not
567
+ // by the top-level `scope` field, which describes the heading-windowing
568
+ // identity `phases`/`total_plans`/`total_summaries`/`completed_phases`
569
+ // were built from. Those two scopes can legitimately disagree (e.g.
570
+ // `scope: "complete"` alongside a genuinely unreadable phases directory),
571
+ // and per the documented contract "scope tells you whether the counts
572
+ // are trustworthy", a consumer seeing `progress_percent: null` needs a
573
+ // field to tell WHY without reading source. Exposing `progress_scope`
574
+ // (rather than reconciling the two scopes into one, or re-deriving
575
+ // `total_plans`/`phases`/etc. from the scoped set) preserves the
576
+ // deliberate, already-documented choice a few lines up: `phases`/
577
+ // `total_plans`/`total_summaries`/`completed_phases` stay the
578
+ // heading-matched detail view (`_phaseDirNames` is a lookup index, not a
579
+ // milestone enumeration — see its comment); only `progress_percent`'s own
580
+ // inputs move onto the scoped owner.
581
+ progress_scope: progressScope,
408
582
  current_phase: currentPhase ? currentPhase.number : null,
409
583
  next_phase: nextPhase ? nextPhase.number : null,
410
584
  missing_phase_details: missingDetails.length > 0 ? missingDetails : null,
585
+ // #3184/#3165: distinguishes a genuinely empty milestone (`scope:
586
+ // "complete"`, `phase_count: 0`) from a window that could not be fully
587
+ // resolved (`"truncated"` / `"unscoped"` / `"unreadable"`) — those cases
588
+ // were previously output-identical.
589
+ scope,
411
590
  };
412
591
  output(result, raw, undefined);
413
592
  }
593
+ // ─── cmdRoadmapMilestoneScope ────────────────────────────────────────────────
594
+ /**
595
+ * #3262 (write-time milestone-scope guard): read-only probe emitting the
596
+ * current milestone window's IDENTITY — its scope classification and the
597
+ * phase ids it declares — so the edit-phase workflow can capture it before
598
+ * its in-place section write, re-derive it after, and roll back on any
599
+ * change. This is the milestone-scope sibling of the workflow's existing
600
+ * `depends_on` gate, expressed as a command because the workflow's write is
601
+ * assistant-driven free-text surgery, not a code path.
602
+ *
603
+ * Deliberately NOT `cmdRoadmapAnalyze`: analyze's #3165 recovery re-populates
604
+ * `phases` from the shipped-milestone-stripped document when the scoped
605
+ * window is suspect, which is right for a human-facing progress report and
606
+ * wrong for a before/after equality probe — the refill would mask exactly
607
+ * the narrowing this guard exists to detect. This probe reports the RAW
608
+ * window (`extractCurrentMilestoneScoped` + `scanMilestonePhaseIds`), no
609
+ * fallback, so a narrowed window is always visible as a changed phase set.
610
+ */
611
+ function cmdRoadmapMilestoneScope(cwd, raw) {
612
+ const roadmapPath = planningPaths(cwd).roadmap;
613
+ if (!node_fs_1.default.existsSync(roadmapPath)) {
614
+ output({ error: 'ROADMAP.md not found', scope: SCOPE.UNREADABLE, phases: [], phase_count: 0 }, raw, undefined);
615
+ return;
616
+ }
617
+ const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
618
+ const { value: window, scope } = extractCurrentMilestoneScoped(rawContent, cwd);
619
+ // Document order (Set insertion order) — deterministic for a given document.
620
+ const phases = [...scanMilestonePhaseIds(window)];
621
+ output({ scope, phases, phase_count: phases.length }, raw, undefined);
622
+ }
414
623
  // ─── cmdRoadmapUpdatePlanProgress ─────────────────────────────────────────────
415
624
  /**
416
625
  * Scope a ROADMAP.md content string down to its "Progress table" writable
@@ -467,9 +676,38 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
467
676
  // completion date until the phase's verification status is 'passed', matching
468
677
  // cmdPhaseComplete's gate (phase.cts:1436). Previously the checkbox fired the
469
678
  // moment the last plan summary landed — before gsd-verifier had verified.
679
+ //
680
+ // ADR-3180 §7.4 (issue #3186, disk-strict): routed through the canonical
681
+ // owner (`isPhaseComplete`) instead of hand-rolling `summaryCount >=
682
+ // planCount && verificationPassed` locally — the owner calls
683
+ // readVerificationStatus UNCONDITIONALLY, so `isComplete` here always
684
+ // agrees with `roadmap analyze` / `init manager` / `phase complete` for
685
+ // the same phase (ADR-3180 §7.4's headline: one predicate for the read
686
+ // path and the write path).
470
687
  const phaseDir = node_path_1.default.join(cwd, phaseInfo.directory);
471
- const verificationPassed = readVerificationStatus(phaseDir).status === 'passed';
472
- const isComplete = summaryCount >= planCount && verificationPassed;
688
+ const completionResult = isPhaseComplete(phaseDir);
689
+ const verificationResult = completionResult.value.verification;
690
+ // #2648 precedent, applied at this write site (ADR-3180 §7.4 / #3186):
691
+ // `isPhaseComplete` deliberately carries NO plan-count precondition — the
692
+ // owner's `complete` is exactly `verification.status === 'passed'`, and
693
+ // that must stay true (disk-strict: a zero-plan phase with a passing
694
+ // `*-VERIFICATION.md` IS complete, #3168). But `readVerificationStatus`'s
695
+ // staleness check only compares SUMMARY mtimes against the verification
696
+ // file — it has no idea a NEW plan was added after the file was written,
697
+ // so a still-fresh `passed` verification says nothing about a plan added
698
+ // afterward. This command WRITES a checkbox and a completion date into
699
+ // ROADMAP.md, a materially stronger claim than "verification passed" —
700
+ // mirroring cmdPhaseComplete's own fail-closed plan-coverage gate
701
+ // (phase.cts:~1995, #2648: "a coverage gate that passes when it cannot
702
+ // read the plans is no gate at all"), composed explicitly here rather than
703
+ // folded into the predicate: complete AND all plans executed.
704
+ const coverageScan = scanPhasePlans(phaseDir);
705
+ const unsummarizedPlans = findUnsummarizedPlans(coverageScan.planFiles, coverageScan.summaryFiles);
706
+ const isComplete = completionResult.value.complete && unsummarizedPlans.length === 0;
707
+ // #3057 B3: routing above is unchanged (an indeterminate staleness check
708
+ // still routes as if nothing were stale) — this only makes the fact visible
709
+ // to whatever reads this command's JSON output.
710
+ const verificationStaleCheckIndeterminate = verificationResult.staleCheckIndeterminate === true;
473
711
  const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned';
474
712
  const today = clock_cjs_1.realClock.localToday();
475
713
  if (!node_fs_1.default.existsSync(roadmapPath)) {
@@ -526,11 +764,67 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
526
764
  // `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
527
765
  // `**Plans:** N plans` — bold "Plans:" (colon inside bold)
528
766
  // `Plans: N plans` — plain text header
529
- 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*)[^\\n]+`, 'i');
767
+ //
768
+ // #2853 / #3584: the verb owns the count token ONLY — it must not destroy
769
+ // hand-written prose a human placed on the line. Group $1 = phase header →
770
+ // `Plans:` label + trailing whitespace (unchanged). Group $2 = the existing
771
+ // count token to replace: matches `N/N plans complete`, `N/N plans executed`,
772
+ // or the bare template `N plan(s)` form — singular is part of the tool's OWN
773
+ // grammar (gsd-core/templates/roadmap.md:62 ships `**Plans**: 1 plan` as the
774
+ // documented one-plan-phase shape), so the `s` is optional there (bug #3584
775
+ // Finding B; pre-fix a bare `1 plan` fell into the drop-everything path and
776
+ // was accidentally overwritten with the correct count — post-fix it must be
777
+ // recognised as a token in its own right or it freezes stale forever). Group
778
+ // $3 = whatever else is on the line (`[^\r\n]*`, so a CRLF `\r` is never part
779
+ // of the match and rides along untouched in the unmatched remainder of the
780
+ // string — never stranded, never duplicated).
781
+ //
782
+ // Three arms, in order:
783
+ // 1. $2 present (a real count token) → rewrite the token, preserve $3
784
+ // verbatim (an annotation a human wrote after a real count; #2853).
785
+ // 2. $2 absent AND $3, trimmed, is the fresh-template PLACEHOLDER shipped
786
+ // by gsd-core/templates/roadmap.md — either
787
+ // `[Number of plans, e.g., "3 plans" or "TBD"]` (line 37) or
788
+ // `[Number of plans]` (lines 51/75/88) → replace it with the computed
789
+ // count. Detected POSITIVELY on the distinctive `Number of plans`
790
+ // wording (anchored, case-insensitive), NEVER on "wholly bracketed" —
791
+ // a bracketed HUMAN annotation such as `[Deferred pending re-scope]`
792
+ // is structurally identical but must be arm-3 preserved (bug #3584
793
+ // Finding A).
794
+ // 3. Anything else (freeform prose, `TBD` / `TBD — annotation`, a
795
+ // bracketed human note, the first line of a wrapped sentence, an
796
+ // empty value) → leave the whole matched line untouched by returning
797
+ // `_match` unchanged. An untouched first line cannot orphan its own
798
+ // continuation on the next line, since the pattern never spans past
799
+ // `\n` in the first place.
800
+ 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');
530
801
  const planCountText = isComplete
531
802
  ? `${summaryCount}/${planCount} plans complete`
532
803
  : `${summaryCount}/${planCount} plans executed`;
533
- roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, `$1${planCountText}`);
804
+ // Positive detector for the fresh-template placeholder ONLY (bug #3584
805
+ // Finding A). Anchored to the distinctive `Number of plans` wording that
806
+ // gsd-core/templates/roadmap.md actually ships, not to "anything in
807
+ // brackets" — a bracketed human annotation like `[Deferred pending
808
+ // re-scope]` is structurally bracketed too but carries none of this
809
+ // wording, so it correctly falls through to arm 3 untouched.
810
+ const isTemplatePlaceholder = (value) => {
811
+ const trimmed = value.trim();
812
+ return /^\[\s*Number of plans\b[\s\S]*\]$/i.test(trimmed);
813
+ };
814
+ roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, (_match, label, existingCount, trailing) => {
815
+ if (existingCount) {
816
+ // Arm 1: real count token — rewrite it, preserve the trailing annotation.
817
+ return `${label}${planCountText}${trailing}`;
818
+ }
819
+ if (isTemplatePlaceholder(trailing)) {
820
+ // Arm 2: fresh-template placeholder — replace with the count.
821
+ return `${label}${planCountText}`;
822
+ }
823
+ // Arm 3: freeform prose, TBD, a bracketed human annotation, a wrapped
824
+ // sentence's first line, or an empty value — leave the line exactly as
825
+ // it was.
826
+ return _match;
827
+ });
534
828
  // If complete: check checkbox
535
829
  if (isComplete) {
536
830
  const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`, 'i');
@@ -541,7 +835,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
541
835
  const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
542
836
  if (!planId)
543
837
  continue;
544
- const planEscaped = escapeRegex(planId);
838
+ const planEscaped = (0, pattern_cjs_1.escapeRegex)(planId);
545
839
  const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
546
840
  roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
547
841
  }
@@ -565,7 +859,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
565
859
  // archived <details> block is still correctly detected as missing from the
566
860
  // active milestone section.
567
861
  const missingPlans = phaseInfo.plans.filter((planFile) => {
568
- const planEscaped = escapeRegex(planFile);
862
+ const planEscaped = (0, pattern_cjs_1.escapeRegex)(planFile);
569
863
  return !new RegExp(`-\\s*\\[[x ]\\]\\s*(?:\\*\\*)?${planEscaped}`, 'i').test(activeRegion);
570
864
  });
571
865
  if (missingPlans.length > 0) {
@@ -613,7 +907,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
613
907
  const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
614
908
  if (!planId)
615
909
  continue;
616
- const planEscaped = escapeRegex(planId);
910
+ const planEscaped = (0, pattern_cjs_1.escapeRegex)(planId);
617
911
  const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
618
912
  roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
619
913
  }
@@ -628,6 +922,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
628
922
  summary_count: summaryCount,
629
923
  status,
630
924
  complete: isComplete,
925
+ verification_stale_check_indeterminate: verificationStaleCheckIndeterminate,
631
926
  }, raw, `${summaryCount}/${planCount} ${status}`);
632
927
  }
633
928
  // ─── cmdRoadmapAnnotateDependencies ───────────────────────────────────────────
@@ -717,6 +1012,11 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
717
1012
  let updated = false;
718
1013
  withPlanningLock(cwd, () => {
719
1014
  const content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
1015
+ // #3413: preserve the file's own EOL style when the checklist block below
1016
+ // is rebuilt and spliced back in — splitLines() cleans each captured line
1017
+ // of any dangling \r, so rejoining with a bare '\n' would silently
1018
+ // downgrade a CRLF ROADMAP.md's rewritten block to LF only.
1019
+ const eol = (0, text_lines_cjs_1.detectEol)(content);
720
1020
  // Find the phase section.
721
1021
  // #3537: padding-tolerant fragment so the caller's resolved padded id
722
1022
  // matches un-padded ROADMAP headings.
@@ -746,12 +1046,20 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
746
1046
  // Review fix (F2): `(?:^|\n)` anchors the match to start-of-line so mid-line
747
1047
  // occurrences like `***Plans:***` embedded in a sentence or `OpenPlans: foo`
748
1048
  // do not trigger a false match. Groups 1 and 2 retain the same semantics.
749
- const plansBlockMatch = phaseSection.match(/(?:^|\n)(\*{0,2}Plans\*{0,2}:[^\n]*\n)((?:\s*-\s*\[[ x]\][^\n]*\n?)+)/i);
1049
+ // #3415: empirically verified linear-time to 10.9MB / 320,000 lines of adversarial
1050
+ // checklist input (0.31ms@1000 lines -> 8.4ms@320,000 lines). The outer `+` group has
1051
+ // no trailing constraint after it in the pattern, so a successful greedy pass never
1052
+ // needs to explore alternate `\r?\n?` boundary partitions to satisfy something later —
1053
+ // it accepts the first complete parse and stops, which rules out the #2128-class
1054
+ // ambiguous-boundary blowup despite the nested-quantifier shape. Non-global match on
1055
+ // already phase-sliced content, not the whole file.
1056
+ // eslint-disable-next-line local/no-unbounded-quantifier -- outer `+` has no trailing constraint to force re-partitioning; measured linear to 10.9MB
1057
+ const plansBlockMatch = phaseSection.match(/(?:^|\r?\n)(\*{0,2}Plans\*{0,2}:[^\r\n]*\r?\n)((?:\s*-\s*\[[ x]\][^\r\n]*\r?\n?)+)/i);
750
1058
  if (!plansBlockMatch)
751
1059
  return;
752
1060
  const plansHeader = plansBlockMatch[1];
753
1061
  const existingList = plansBlockMatch[2];
754
- const listLines = existingList.split('\n').filter(l => /^\s*-\s*\[/.test(l));
1062
+ const listLines = (0, text_lines_cjs_1.splitLines)(existingList).filter(l => /^\s*-\s*\[/.test(l));
755
1063
  if (listLines.length === 0)
756
1064
  return;
757
1065
  // #314 perf: build a first-wins Map so per-line lookup is O(1) instead of O(plans).
@@ -804,11 +1112,22 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
804
1112
  annotatedLines.push(`- ${t}`);
805
1113
  }
806
1114
  }
807
- const newListBlock = annotatedLines.join('\n') + '\n';
808
- // #1103: when `(?:^|\n)` consumed a leading `\n` (mid-string match), re-emit it
809
- // so the line preceding the Plans: header is not fused onto it.
810
- const leadingNewline = plansBlockMatch[0].startsWith('\n') ? '\n' : '';
811
- const newPhaseSection = phaseSection.replace(plansBlockMatch[0], leadingNewline + plansHeader + newListBlock);
1115
+ const newListBlock = (0, text_lines_cjs_1.joinLines)(annotatedLines, eol) + eol;
1116
+ // #1103: when `(?:^|\r?\n)` consumed a leading terminator (mid-string
1117
+ // match), re-emit it verbatim so the line preceding the Plans: header is
1118
+ // not fused onto it. #3413: the widened `(?:^|\r?\n)` can now consume a
1119
+ // 2-char `\r\n` — re-emit whatever was actually captured (`''`, `'\n'`,
1120
+ // or `'\r\n'`), not a hardcoded `'\n'`, or a CRLF file loses its `\r`.
1121
+ const leadingMatch = /^\r?\n/.exec(plansBlockMatch[0]);
1122
+ const leadingNewline = leadingMatch ? leadingMatch[0] : '';
1123
+ // Review fix (#3413 security): use the FUNCTION-replacement form. The
1124
+ // string-replacement form expands String#replace's special patterns
1125
+ // (`$&`, `` $` ``, `$'`, `$$`, `$1`-`$9`) inside the replacement — and
1126
+ // newListBlock is built from author-controlled truths/plan-file content,
1127
+ // so a line containing a literal `` $` `` (etc.) would splice unrelated
1128
+ // surrounding phaseSection text into the result. A function replacer is
1129
+ // never pattern-interpreted.
1130
+ const newPhaseSection = phaseSection.replace(plansBlockMatch[0], () => leadingNewline + plansHeader + newListBlock);
812
1131
  const nextContent = content.slice(0, phaseStart) + newPhaseSection + content.slice(phaseEnd);
813
1132
  if (nextContent === content)
814
1133
  return;
@@ -826,6 +1145,8 @@ module.exports = {
826
1145
  cmdRoadmapGetPhase,
827
1146
  getRoadmapPhaseWithFallback,
828
1147
  cmdRoadmapAnalyze,
1148
+ cmdRoadmapMilestoneScope,
829
1149
  cmdRoadmapUpdatePlanProgress,
830
1150
  cmdRoadmapAnnotateDependencies,
1151
+ buildPhaseHeadingRegex,
831
1152
  };