@opengsd/gsd-core 1.11.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 (395) 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 +1 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-dom-verifier.md +169 -0
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-executor.md +17 -9
  9. package/agents/gsd-framework-selector.md +1 -3
  10. package/agents/gsd-intel-updater.md +1 -1
  11. package/agents/gsd-mempalace-curator.md +0 -1
  12. package/agents/gsd-pattern-mapper.md +11 -0
  13. package/agents/gsd-phase-researcher.md +3 -1
  14. package/agents/gsd-plan-checker.md +15 -55
  15. package/agents/gsd-planner.md +6 -4
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +2 -2
  18. package/agents/gsd-roadmapper.md +15 -11
  19. package/agents/gsd-ui-checker.md +63 -4
  20. package/agents/gsd-ui-researcher.md +41 -3
  21. package/agents/gsd-verifier.md +1 -1
  22. package/bin/install.js +609 -134
  23. package/commands/gsd/discuss-phase.md +1 -1
  24. package/commands/gsd/import.md +1 -1
  25. package/commands/gsd/quick.md +8 -4
  26. package/gsd-core/bin/gsd-tools.cjs +567 -51
  27. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  28. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  29. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  30. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  31. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  32. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  33. package/gsd-core/bin/lib/audit.cjs +163 -41
  34. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  35. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  36. package/gsd-core/bin/lib/capability-registry.cjs +336 -95
  37. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  38. package/gsd-core/bin/lib/capability-validator.cjs +205 -18
  39. package/gsd-core/bin/lib/check-command-router.cjs +145 -5
  40. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  41. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  43. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  44. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  45. package/gsd-core/bin/lib/commands.cjs +543 -44
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  47. package/gsd-core/bin/lib/config-loader.cjs +118 -29
  48. package/gsd-core/bin/lib/config.cjs +92 -2
  49. package/gsd-core/bin/lib/configuration.cjs +129 -37
  50. package/gsd-core/bin/lib/core-utils.cjs +84 -7
  51. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  52. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  53. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  54. package/gsd-core/bin/lib/frontmatter.cjs +840 -305
  55. package/gsd-core/bin/lib/gap-checker.cjs +27 -3
  56. package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
  57. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  58. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
  59. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  60. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  61. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  62. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  63. package/gsd-core/bin/lib/init.cjs +120 -41
  64. package/gsd-core/bin/lib/install-engine.cjs +68 -3
  65. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
  66. package/gsd-core/bin/lib/install-profiles.cjs +78 -4
  67. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  68. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  69. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  70. package/gsd-core/bin/lib/intel.cjs +101 -26
  71. package/gsd-core/bin/lib/io.cjs +160 -15
  72. package/gsd-core/bin/lib/learnings.cjs +85 -14
  73. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  74. package/gsd-core/bin/lib/markdown-table.cjs +52 -4
  75. package/gsd-core/bin/lib/milestone.cjs +90 -5
  76. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  77. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  78. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  79. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  80. package/gsd-core/bin/lib/phase-id.cjs +70 -4
  81. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  82. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  83. package/gsd-core/bin/lib/phase.cjs +405 -84
  84. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  85. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  86. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  87. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  88. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
  89. package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
  90. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  91. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  92. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  93. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  94. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  95. package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
  96. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  97. package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
  98. package/gsd-core/bin/lib/roadmap.cjs +74 -19
  99. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
  100. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
  101. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
  102. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  103. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  104. package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
  105. package/gsd-core/bin/lib/smart-entry.cjs +12 -22
  106. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  107. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  108. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  109. package/gsd-core/bin/lib/state-document.cjs +186 -0
  110. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  111. package/gsd-core/bin/lib/state-transition.cjs +517 -101
  112. package/gsd-core/bin/lib/state.cjs +946 -163
  113. package/gsd-core/bin/lib/surface.cjs +10 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  115. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  116. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  117. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  118. package/gsd-core/bin/lib/uat.cjs +1376 -125
  119. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  120. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  121. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  122. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  123. package/gsd-core/bin/lib/vendor/README.md +43 -5
  124. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  125. package/gsd-core/bin/lib/verification.cjs +14 -1
  126. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  127. package/gsd-core/bin/lib/verify.cjs +95 -40
  128. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  129. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  130. package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
  131. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  132. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  133. package/gsd-core/bin/shared/exit-codes.json +8 -0
  134. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  135. package/gsd-core/bin/shared/model-catalog.json +8 -1
  136. package/gsd-core/references/agent-contracts.md +3 -2
  137. package/gsd-core/references/api-coverage.md +24 -2
  138. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  139. package/gsd-core/references/checkpoints.md +37 -19
  140. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  141. package/gsd-core/references/edge-probe.md +8 -0
  142. package/gsd-core/references/execute-mvp-tdd.md +1 -3
  143. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  144. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  145. package/gsd-core/references/failing-direction.md +78 -0
  146. package/gsd-core/references/gate-prompts.md +1 -1
  147. package/gsd-core/references/git-integration.md +5 -5
  148. package/gsd-core/references/git-planning-commit.md +3 -3
  149. package/gsd-core/references/gsd-run-resolver.md +1 -1
  150. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  151. package/gsd-core/references/model-profiles.md +1 -1
  152. package/gsd-core/references/nyquist-compliance.md +74 -0
  153. package/gsd-core/references/offer-next.md +3 -5
  154. package/gsd-core/references/phase-argument-parsing.md +3 -3
  155. package/gsd-core/references/planner-failing-direction.md +53 -0
  156. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  157. package/gsd-core/references/planner-revision.md +1 -1
  158. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  159. package/gsd-core/references/planning-config.md +37 -8
  160. package/gsd-core/references/reviewer-instances.md +31 -0
  161. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  162. package/gsd-core/references/tdd.md +1 -3
  163. package/gsd-core/references/ui-brand.md +65 -21
  164. package/gsd-core/references/ui-consideration-probe.md +1 -1
  165. package/gsd-core/references/universal-anti-patterns.md +2 -2
  166. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  167. package/gsd-core/references/verify-mvp-mode.md +1 -1
  168. package/gsd-core/references/workstream-flag.md +11 -11
  169. package/gsd-core/templates/README.md +1 -1
  170. package/gsd-core/templates/SECURITY.md +3 -3
  171. package/gsd-core/templates/UI-SPEC.md +25 -3
  172. package/gsd-core/templates/VALIDATION.md +3 -3
  173. package/gsd-core/templates/phase-prompt.md +3 -0
  174. package/gsd-core/templates/state.md +7 -0
  175. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  176. package/gsd-core/workflows/add-backlog.md +1 -1
  177. package/gsd-core/workflows/add-phase.md +3 -3
  178. package/gsd-core/workflows/add-tests.md +3 -8
  179. package/gsd-core/workflows/add-todo.md +1 -1
  180. package/gsd-core/workflows/ai-integration-phase.md +4 -9
  181. package/gsd-core/workflows/audit-fix.md +12 -3
  182. package/gsd-core/workflows/audit-milestone.md +9 -9
  183. package/gsd-core/workflows/audit-uat.md +17 -2
  184. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  185. package/gsd-core/workflows/autonomous.md +10 -26
  186. package/gsd-core/workflows/check-todos.md +1 -1
  187. package/gsd-core/workflows/cleanup.md +2 -2
  188. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  189. package/gsd-core/workflows/code-review-fix.md +1 -1
  190. package/gsd-core/workflows/code-review.md +121 -40
  191. package/gsd-core/workflows/complete-milestone.md +15 -10
  192. package/gsd-core/workflows/debug.md +5 -3
  193. package/gsd-core/workflows/diagnose-issues.md +12 -6
  194. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  195. package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
  196. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  197. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  198. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
  199. package/gsd-core/workflows/discuss-phase.md +1 -1
  200. package/gsd-core/workflows/do.md +3 -6
  201. package/gsd-core/workflows/docs-update.md +5 -4
  202. package/gsd-core/workflows/edit-phase.md +1 -1
  203. package/gsd-core/workflows/eval-review.md +4 -9
  204. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  205. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
  206. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  207. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  208. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  209. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
  210. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  211. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  212. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  213. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  214. package/gsd-core/workflows/execute-phase.md +38 -54
  215. package/gsd-core/workflows/execute-plan.md +17 -12
  216. package/gsd-core/workflows/explore.md +1 -1
  217. package/gsd-core/workflows/extract-learnings.md +1 -1
  218. package/gsd-core/workflows/fast.md +2 -2
  219. package/gsd-core/workflows/forensics.md +1 -1
  220. package/gsd-core/workflows/graduation.md +5 -5
  221. package/gsd-core/workflows/health.md +3 -6
  222. package/gsd-core/workflows/import.md +14 -11
  223. package/gsd-core/workflows/inbox.md +4 -5
  224. package/gsd-core/workflows/ingest-docs.md +44 -11
  225. package/gsd-core/workflows/insert-phase.md +5 -5
  226. package/gsd-core/workflows/list-seeds.md +5 -3
  227. package/gsd-core/workflows/list-workspaces.md +1 -1
  228. package/gsd-core/workflows/manager.md +12 -23
  229. package/gsd-core/workflows/map-codebase.md +1 -1
  230. package/gsd-core/workflows/milestone-summary.md +1 -1
  231. package/gsd-core/workflows/mvp-phase.md +2 -2
  232. package/gsd-core/workflows/new-milestone.md +9 -21
  233. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  234. package/gsd-core/workflows/new-project.md +12 -26
  235. package/gsd-core/workflows/new-workspace.md +1 -1
  236. package/gsd-core/workflows/next.md +2 -2
  237. package/gsd-core/workflows/pause-work.md +1 -1
  238. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  239. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  240. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  241. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  242. package/gsd-core/workflows/plan-phase.md +121 -42
  243. package/gsd-core/workflows/plan-review-convergence.md +46 -9
  244. package/gsd-core/workflows/plant-seed.md +2 -2
  245. package/gsd-core/workflows/pr-branch.md +187 -51
  246. package/gsd-core/workflows/profile-user.md +16 -14
  247. package/gsd-core/workflows/progress.md +27 -12
  248. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  249. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
  250. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  251. package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
  252. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  253. package/gsd-core/workflows/quick.md +20 -29
  254. package/gsd-core/workflows/remove-phase.md +4 -4
  255. package/gsd-core/workflows/remove-workspace.md +2 -2
  256. package/gsd-core/workflows/resume-project.md +8 -12
  257. package/gsd-core/workflows/review.md +193 -15
  258. package/gsd-core/workflows/scan.md +1 -1
  259. package/gsd-core/workflows/secure-phase.md +2 -2
  260. package/gsd-core/workflows/settings-advanced.md +7 -9
  261. package/gsd-core/workflows/settings-integrations.md +64 -31
  262. package/gsd-core/workflows/settings.md +3 -5
  263. package/gsd-core/workflows/ship.md +12 -6
  264. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  265. package/gsd-core/workflows/sketch.md +12 -18
  266. package/gsd-core/workflows/smart-entry.md +3 -5
  267. package/gsd-core/workflows/spec-phase.md +23 -1
  268. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  269. package/gsd-core/workflows/spike.md +20 -31
  270. package/gsd-core/workflows/stats.md +2 -2
  271. package/gsd-core/workflows/sync-skills.md +1 -1
  272. package/gsd-core/workflows/thread.md +11 -7
  273. package/gsd-core/workflows/transition.md +5 -5
  274. package/gsd-core/workflows/ui-phase.md +10 -16
  275. package/gsd-core/workflows/ui-review.md +6 -10
  276. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  277. package/gsd-core/workflows/undo.md +8 -16
  278. package/gsd-core/workflows/update.md +6 -10
  279. package/gsd-core/workflows/validate-phase.md +2 -2
  280. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  281. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  282. package/gsd-core/workflows/verify-work.md +57 -18
  283. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  284. package/hooks/dist/gsd-config-reload.js +18 -12
  285. package/hooks/dist/gsd-context-monitor.js +19 -10
  286. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  287. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  288. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  289. package/hooks/dist/gsd-cursor-stop.js +2 -1
  290. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  291. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  292. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  293. package/hooks/dist/gsd-graphify-update.sh +22 -18
  294. package/hooks/dist/gsd-node-runner.sh +76 -0
  295. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  296. package/hooks/dist/gsd-prompt-guard.js +16 -7
  297. package/hooks/dist/gsd-read-guard.js +16 -7
  298. package/hooks/dist/gsd-read-injection-scanner.js +17 -8
  299. package/hooks/dist/gsd-session-state.sh +1 -0
  300. package/hooks/dist/gsd-statusline.js +215 -26
  301. package/hooks/dist/gsd-validate-commit.sh +80 -6
  302. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  303. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  304. package/hooks/dist/gsd-workflow-guard.js +34 -16
  305. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  306. package/hooks/dist/gsd-write-guard.js +35 -25
  307. package/hooks/dist/lib/cli-exit.js +560 -0
  308. package/hooks/dist/lib/exit-code-registry.js +98 -0
  309. package/hooks/dist/lib/git-probe.js +84 -0
  310. package/hooks/dist/lib/hook-exit.js +81 -0
  311. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  312. package/hooks/gsd-agent-isolation-guard.js +77 -38
  313. package/hooks/gsd-config-reload.js +18 -12
  314. package/hooks/gsd-context-monitor.js +19 -10
  315. package/hooks/gsd-cursor-post-tool.js +3 -1
  316. package/hooks/gsd-cursor-pre-tool.js +3 -1
  317. package/hooks/gsd-cursor-session-start.js +2 -1
  318. package/hooks/gsd-cursor-stop.js +2 -1
  319. package/hooks/gsd-cursor-subagent-start.js +28 -23
  320. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  321. package/hooks/gsd-ensure-canonical-path.js +2 -1
  322. package/hooks/gsd-graphify-update.sh +22 -18
  323. package/hooks/gsd-node-runner.sh +76 -0
  324. package/hooks/gsd-phase-boundary.sh +1 -0
  325. package/hooks/gsd-prompt-guard.js +16 -7
  326. package/hooks/gsd-read-guard.js +16 -7
  327. package/hooks/gsd-read-injection-scanner.js +17 -8
  328. package/hooks/gsd-session-state.sh +1 -0
  329. package/hooks/gsd-statusline.js +215 -26
  330. package/hooks/gsd-validate-commit.sh +80 -6
  331. package/hooks/gsd-windsurf-pre-command.js +16 -11
  332. package/hooks/gsd-windsurf-pre-write.js +22 -13
  333. package/hooks/gsd-workflow-guard.js +34 -16
  334. package/hooks/gsd-worktree-path-guard.js +36 -21
  335. package/hooks/gsd-write-guard.js +35 -25
  336. package/hooks/lib/cli-exit.js +560 -0
  337. package/hooks/lib/exit-code-registry.js +98 -0
  338. package/hooks/lib/git-probe.js +84 -0
  339. package/hooks/lib/hook-exit.js +81 -0
  340. package/hooks/managed-hooks-registry.cjs +3 -0
  341. package/package.json +12 -7
  342. package/scripts/base64-scan.sh +74 -12
  343. package/scripts/build-hooks.js +5 -0
  344. package/scripts/check-glossary-refs.cjs +77 -15
  345. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  346. package/scripts/ci-check-job-near-cap.cjs +49 -0
  347. package/scripts/ci-pr-mergeability.cjs +262 -0
  348. package/scripts/ci-test-scope.cjs +45 -12
  349. package/scripts/ci-timeout-report.cjs +230 -0
  350. package/scripts/docs-guard-registry.cjs +396 -0
  351. package/scripts/gen-capability-registry.cjs +8 -6
  352. package/scripts/gen-exit-code-docs.cjs +318 -0
  353. package/scripts/gen-exit-code-registry.cjs +891 -0
  354. package/scripts/gen-features.cjs +836 -0
  355. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  356. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  357. package/scripts/gen-loop-host-contract.cjs +134 -1
  358. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  359. package/scripts/gen-state-md-docs.cjs +727 -0
  360. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  361. package/scripts/lib/ci-job-timing.cjs +72 -0
  362. package/scripts/lib/cli-exit.cjs +546 -44
  363. package/scripts/lib/drift-scan.cjs +32 -2
  364. package/scripts/lib/exit-code-registry.cjs +98 -0
  365. package/scripts/lib/ndjson-reporter.cjs +119 -0
  366. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  367. package/scripts/lint-docs-guard-registration.cjs +495 -0
  368. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  369. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  370. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  371. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  372. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  373. package/scripts/lint-phase-enumeration-drift.cjs +21 -8
  374. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  375. package/scripts/lint-removed-but-needed.cjs +184 -16
  376. package/scripts/lint-seam-enforcement.cjs +182 -0
  377. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  378. package/scripts/lint-source-test-name-collision.cjs +241 -0
  379. package/scripts/lint-state-write-path-drift.cjs +337 -432
  380. package/scripts/lint-test-file-count.allowlist.json +122 -4
  381. package/scripts/lint-test-file-count.cjs +25 -3
  382. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  383. package/scripts/lint-vendored-deps.cjs +208 -35
  384. package/scripts/mutation-matrix.cjs +599 -50
  385. package/scripts/prompt-injection-scan.sh +75 -14
  386. package/scripts/secret-scan.sh +75 -13
  387. package/scripts/select-docs-guards.cjs +56 -0
  388. package/scripts/sync-runtime-launcher.cjs +22 -3
  389. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  390. package/skills/gsd-import/SKILL.md +1 -1
  391. package/skills/gsd-quick/SKILL.md +8 -4
  392. package/vscode/package.json +1 -1
  393. package/bin/lib/ui-safety-gate.cjs +0 -109
  394. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  395. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -29,7 +29,7 @@ const cliExitMod = require("./cli-exit.cjs");
29
29
  const { ExitError } = cliExitMod;
30
30
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
31
  const roadmapParserMod = require("./roadmap-parser.cjs");
32
- const { extractCurrentMilestoneScoped } = roadmapParserMod;
32
+ const { extractCurrentMilestoneScoped, hasPhaseEntries } = roadmapParserMod;
33
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
34
34
  const planningScopeMod = require("./planning-scope.cjs");
35
35
  const { SCOPE } = planningScopeMod;
@@ -151,11 +151,45 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) {
151
151
  warnings.push({ code: 'V003', message: 'ROADMAP.md frontmatter is malformed (unterminated --- fence)' });
152
152
  }
153
153
  }
154
- // No recognizable phase structure — at least one `### Phase N:` heading.
155
- // Mirrors the phase-heading pattern used across roadmap-parser.cts.
156
- const hasPhaseEntry = /^#{2,4}\s*Phase\s+\S/im.test(roadmapContent);
154
+ // #3641: resolve phase_id_convention ONCE, ahead of every consumer in
155
+ // this validate pass — V004's entry check and V005's scope classifier
156
+ // below, and the W021 milestone-prefix check after them.
157
+ // Authoritative source: .planning/config.json (set by the upgrade
158
+ // command). Fallback: ROADMAP.md frontmatter (for projects that set
159
+ // the field there directly).
160
+ let convention;
161
+ try {
162
+ const cfg = loadConfig(cwd);
163
+ convention = cfg['phase_id_convention'];
164
+ }
165
+ catch {
166
+ convention = undefined;
167
+ }
168
+ if (convention === undefined || convention === null) {
169
+ // Fallback: read from ROADMAP.md frontmatter. Bounded to match
170
+ // cmdRoadmapMilestoneScope's copy exactly (#3641 review NEW-1: an
171
+ // unbounded capture here read past 4KB frontmatters the probe's
172
+ // bounded copy could not, diverging validate from the probe).
173
+ const fmMatch = roadmapContent.match(/^---\r?\n([\s\S]{0,4000}?)\r?\n---/);
174
+ if (fmMatch) {
175
+ const kvMatch = fmMatch[1].match(/^phase_id_convention:\s*(.*)$/m);
176
+ if (kvMatch) {
177
+ const val = kvMatch[1].trim();
178
+ if (val !== 'null' && val !== '') {
179
+ convention = val.replace(/^["']|["']$/g, '');
180
+ }
181
+ }
182
+ }
183
+ }
184
+ // No recognizable phase structure. #3641: routed through the
185
+ // roadmap-parser owner (`hasPhaseEntries` — headings, #2199 bullets,
186
+ // #3577 table rows) instead of a private inline heading regex, so the
187
+ // document-level check and the V005 scope axis below can never
188
+ // disagree about what a phase entry is — and so a bracket-convention
189
+ // project's `### [GSD.04] 01:` entries are entries here too.
190
+ const hasPhaseEntry = hasPhaseEntries(roadmapContent, convention);
157
191
  if (!hasPhaseEntry && !warnings.some((w) => w.code === 'V002')) {
158
- warnings.push({ code: 'V004', message: 'ROADMAP.md contains no recognizable phase entries (no "### Phase N:" headings)' });
192
+ warnings.push({ code: 'V004', message: 'ROADMAP.md contains no recognizable phase entries (no phase headings, bullet entries, or table rows)' });
159
193
  }
160
194
  // #3263: a whole-document phase check (V004 above) is satisfied by a
161
195
  // document whose phase entries live OUTSIDE the active milestone's
@@ -169,12 +203,14 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) {
169
203
  // an unscoped/unreadable window is a different failure mode with a
170
204
  // different remediation, deliberately not warned here.
171
205
  try {
172
- const scoped = extractCurrentMilestoneScoped(contentAfterBom, cwd);
206
+ // #3641: thread the resolved convention so the scope axis's
207
+ // hasPhaseEntries comparison recognizes bracket phase entries.
208
+ const scoped = extractCurrentMilestoneScoped(contentAfterBom, cwd, undefined, convention);
173
209
  if (scoped.scope === SCOPE.TRUNCATED) {
174
210
  warnings.push({
175
211
  code: 'V005',
176
212
  message: 'Active milestone window is truncated: phase entries exist in ROADMAP.md but are excluded from the ' +
177
- 'active milestone\'s resolved window (check for a heading between the milestone heading and its "### Phase N:" sections)',
213
+ 'active milestone\'s resolved window (check for a heading between the milestone heading and its phase-entry sections)',
178
214
  });
179
215
  }
180
216
  }
@@ -182,30 +218,8 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) {
182
218
  // The classifier is best-effort here — a throw must not mask the
183
219
  // structural warnings already collected above.
184
220
  }
185
- // W021 only fires when phase_id_convention is explicitly 'milestone-prefixed'.
186
- // Authoritative source: .planning/config.json (set by the upgrade command).
187
- // Fallback: ROADMAP.md frontmatter (for projects that set the field there directly).
188
- let convention;
189
- try {
190
- const cfg = loadConfig(cwd);
191
- convention = cfg['phase_id_convention'];
192
- }
193
- catch {
194
- convention = undefined;
195
- }
196
- if (convention === undefined || convention === null) {
197
- // Fallback: read from ROADMAP.md frontmatter
198
- const fmMatch = roadmapContent.match(/^---\r?\n([\s\S]+?)\r?\n---/);
199
- if (fmMatch) {
200
- const kvMatch = fmMatch[1].match(/^phase_id_convention:\s*(.*)$/m);
201
- if (kvMatch) {
202
- const val = kvMatch[1].trim();
203
- if (val !== 'null' && val !== '') {
204
- convention = val.replace(/^["']|["']$/g, '');
205
- }
206
- }
207
- }
208
- }
221
+ // W021 only fires when phase_id_convention is explicitly
222
+ // 'milestone-prefixed' — the same hoisted resolution above (#3641).
209
223
  if (convention === 'milestone-prefixed') {
210
224
  warnings.push(...checkW021(roadmapContent));
211
225
  }
@@ -29,7 +29,10 @@ const phaseIdModule = require("./phase-id.cjs");
29
29
  const { phaseMarkdownRegexSource, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE,
30
30
  // #2121: roadmapPhaseLookupSources now lives in phase-id.cjs (single owner of
31
31
  // the lookup-source ordering); imported here rather than defined locally.
32
- roadmapPhaseLookupSources, extractPhaseToken, isSentinelPhaseId, } = phaseIdModule;
32
+ roadmapPhaseLookupSources, extractPhaseToken, isSentinelPhaseId,
33
+ // #3641: the single-owner heading-intro and digit-token grammar sources —
34
+ // see BRACKET_PHASE_ENTRY_HEADING_RE below.
35
+ PHASE_HEADING_PREFIX_SRC, PHASE_NUMBER_TOKEN_SOURCE, } = phaseIdModule;
33
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
34
37
  const planningWorkspace = require("./planning-workspace.cjs");
35
38
  const { planningDir } = planningWorkspace;
@@ -340,7 +343,7 @@ const MILESTONE_HEADING_SIGNAL_PATTERN = /v\d+\.\d+|✅|📋|🚧|\bMilestone\b/
340
343
  * template or the #3204/#1761/#3185 reports exercises that shape; it is
341
344
  * recorded here rather than hidden.
342
345
  */
343
- function hasMilestoneSectioning(content) {
346
+ function countMilestoneHeadings(content) {
344
347
  const isPhaseHeading = (text) => /^Phase\s+\S/i.test(text);
345
348
  let milestoneHeadingCount = 0;
346
349
  for (const heading of (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content)) {
@@ -351,10 +354,29 @@ function hasMilestoneSectioning(content) {
351
354
  if (!MILESTONE_HEADING_SIGNAL_PATTERN.test(heading.text))
352
355
  continue;
353
356
  milestoneHeadingCount++;
354
- if (milestoneHeadingCount >= 2)
355
- return true;
356
357
  }
357
- return false;
358
+ return milestoneHeadingCount;
359
+ }
360
+ function hasMilestoneSectioning(content) {
361
+ // The >=2 short-circuit the inline walk used to have is gone — a ROADMAP's
362
+ // heading count is small and tokenizeHeadings materializes the full token
363
+ // array regardless, so the shared walk pays nothing for it.
364
+ return countMilestoneHeadings(content) >= 2;
365
+ }
366
+ /**
367
+ * #3642: the >=1 sibling of `hasMilestoneSectioning`. The >=2 predicate
368
+ * answers SIBLING-conflation ("could two sections' phases mix") and is
369
+ * unchanged; but `buildStateFrontmatter`'s unbounded branch asks a question
370
+ * >=2 under-answers: "is there ANY milestone section whose phases a
371
+ * whole-document count would attribute to a milestone that matches no
372
+ * heading?" With exactly ONE section and an asserted milestone absent from
373
+ * the ROADMAP, >=2 said "flat" and the single section's phases leaked into
374
+ * the asserted milestone's total_phases (silent clobber of the stored
375
+ * value). Same walk, same vocabulary, threshold 1 — exported for that
376
+ * consumer only; every other consumer keeps the >=2 semantics.
377
+ */
378
+ function hasAnyMilestoneSection(content) {
379
+ return countMilestoneHeadings(content) >= 1;
358
380
  }
359
381
  /**
360
382
  * #3184: the sole "which heading is this milestone's" rule — locate the version's
@@ -405,14 +427,39 @@ function sliceMilestoneWindow(content, version) {
405
427
  * exist" — a window containing only sentinel phases still reached the
406
428
  * region and must read COMPLETE, not TRUNCATED.
407
429
  */
408
- function hasPhaseEntries(markdown) {
430
+ // #3641: the bracket-convention phase-ENTRY heading shape — ADR-612 Decision
431
+ // 1's own discriminator: a phase heading is a bracket followed by a
432
+ // DIGIT-then-colon (`### [GSD.04] 01: Name`); a bracket followed by a NAME
433
+ // is a milestone heading and must never count. Every fragment interpolates a
434
+ // single-owner export from phase-id.cts — the heading intro
435
+ // (PHASE_HEADING_PREFIX_SRC: a `[...]` bracket optionally followed by a
436
+ // `Phase ` label, or a bare `Phase ` label), the digit-bearing token
437
+ // (PHASE_NUMBER_TOKEN_SOURCE, which also covers the dotted sub-phase form
438
+ // `[GSD.02] 05.03:`), and the optional pre-colon tag
439
+ // (OPTIONAL_PHASE_TAG_SOURCE) — never a re-typed grammar. Tested IN
440
+ // ADDITION to the legacy pattern below, so bracket mode is a strict
441
+ // superset: mid-migration legacy-labeled headings (`Phase AUTH-101:`-style
442
+ // custom ids included) keep their existing recognition. Review finding: an
443
+ // earlier single-alternative form with a `[\\w]` token admitted
444
+ // `[bracket] Word:` shapes — a colon-bearing MILESTONE heading inside the
445
+ // window read as an entry (defeating V005 outright for that spelling) and a
446
+ // decoy `### [GSD.04] Notes:` outside the window manufactured a false V005
447
+ // while suppressing the correct V004. The digit anchor forecloses both.
448
+ const BRACKET_PHASE_ENTRY_HEADING_RE = new RegExp(`^${PHASE_HEADING_PREFIX_SRC}${PHASE_NUMBER_TOKEN_SOURCE}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'i');
449
+ function hasPhaseEntries(markdown, phaseIdConvention) {
409
450
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
451
+ // #3641: the widened grammar engages ONLY when the resolved convention is
452
+ // 'bracket' — a project that has not opted in runs the legacy pattern
453
+ // alone, byte-identically.
410
454
  const phaseHeadingPattern = /^(?:\[[^\]]{1,200}\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/i;
455
+ const bracketMode = phaseIdConvention === 'bracket';
411
456
  for (const h of (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(markdown)) {
412
457
  if (h.level < 2 || h.level > 4)
413
458
  continue;
414
459
  if (phaseHeadingPattern.test(h.text))
415
460
  return true;
461
+ if (bracketMode && BRACKET_PHASE_ENTRY_HEADING_RE.test(h.text))
462
+ return true;
416
463
  }
417
464
  // #3184 review finding: the bullet fallback must be fence-aware too, or a
418
465
  // FENCED markdown EXAMPLE of the `- [ ] **Phase N — Name**` syntax (e.g. a
@@ -592,8 +639,18 @@ function classifyMilestoneWindow(input) {
592
639
  * symbols, 20 direct callers) means its signature and return type do not
593
640
  * change. This is the real owner; `extractCurrentMilestone` becomes a
594
641
  * one-line wrapper returning `.value` so every existing caller is untouched.
642
+ *
643
+ * @param phaseIdConvention - #3641: the RESOLVED `phase_id_convention`
644
+ * config value, threaded to `hasPhaseEntries` so the scope axis's row-8
645
+ * comparison recognizes bracket-convention phase entries
646
+ * (`### [GSD.04] 01: Name`). Optional: absent, or any value other than
647
+ * `'bracket'`, compiles the legacy entry grammar byte-identically — the
648
+ * widening engages only for a project that resolved the convention
649
+ * explicitly. `extractCurrentMilestone`'s wrapper deliberately does NOT
650
+ * expose it (its 20 callers are not the scope-axis consumers; V005's
651
+ * router site and `getMilestonePhaseFilter` resolve and thread it).
595
652
  */
596
- function extractCurrentMilestoneScoped(content, cwd, ws) {
653
+ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
597
654
  if (!cwd) {
598
655
  // Row 1: a deliberate unscoped read (no cwd supplied) is a real answer —
599
656
  // the caller asked for no scoping, so whole-document is COMPLETE.
@@ -631,12 +688,12 @@ function extractCurrentMilestoneScoped(content, cwd, ws) {
631
688
  versionResolved,
632
689
  hasVersionedMilestones: versionedMilestonesPresent,
633
690
  headingFound: false,
634
- windowHasPhaseEntries: hasPhaseEntries(value),
635
- documentHasPhaseEntries: hasPhaseEntries(value),
691
+ windowHasPhaseEntries: hasPhaseEntries(value, phaseIdConvention),
692
+ documentHasPhaseEntries: hasPhaseEntries(value, phaseIdConvention),
636
693
  }),
637
694
  };
638
695
  }
639
- const documentHasPhaseEntries = hasPhaseEntries(stripShippedMilestones(content));
696
+ const documentHasPhaseEntries = hasPhaseEntries(stripShippedMilestones(content), phaseIdConvention);
640
697
  const summaryPattern = new RegExp(`<summary[^>]*>([^<]*${(0, pattern_cjs_1.escapeRegex)(version)}[^<]*)<\\/summary>`, 'i');
641
698
  const headingMatches = locateMilestoneHeadings(content, version);
642
699
  if (headingMatches.length === 0) {
@@ -666,7 +723,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws) {
666
723
  versionResolved,
667
724
  hasVersionedMilestones: versionedMilestonesPresent,
668
725
  headingFound: true,
669
- windowHasPhaseEntries: hasPhaseEntries(value),
726
+ windowHasPhaseEntries: hasPhaseEntries(value, phaseIdConvention),
670
727
  documentHasPhaseEntries,
671
728
  }),
672
729
  };
@@ -680,7 +737,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws) {
680
737
  versionResolved,
681
738
  hasVersionedMilestones: versionedMilestonesPresent,
682
739
  headingFound: false,
683
- windowHasPhaseEntries: hasPhaseEntries(value),
740
+ windowHasPhaseEntries: hasPhaseEntries(value, phaseIdConvention),
684
741
  documentHasPhaseEntries,
685
742
  }),
686
743
  };
@@ -756,7 +813,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws) {
756
813
  versionResolved,
757
814
  hasVersionedMilestones: versionedMilestonesPresent,
758
815
  headingFound: true,
759
- windowHasPhaseEntries: hasPhaseEntries(value),
816
+ windowHasPhaseEntries: hasPhaseEntries(value, phaseIdConvention),
760
817
  documentHasPhaseEntries,
761
818
  }),
762
819
  };
@@ -1257,7 +1314,7 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
1257
1314
  const roadmapContent = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
1258
1315
  if (roadmapContent === null)
1259
1316
  throw new Error('missing');
1260
- const scopedResult = extractCurrentMilestoneScoped(roadmapContent, cwd, ws);
1317
+ const scopedResult = extractCurrentMilestoneScoped(roadmapContent, cwd, ws, phaseIdConvention);
1261
1318
  let roadmap = scopedResult.value;
1262
1319
  // Default: the filter's window IS extractCurrentMilestoneScoped's own
1263
1320
  // window (reused verbatim, not re-derived — ADR-3180 Decision 4c).
@@ -1289,7 +1346,7 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
1289
1346
  // via `currentVersion`), and `buildStateFrontmatter` (state.cts:1700,
1290
1347
  // via `storedMilestone`).
1291
1348
  const sliced = sliceMilestoneWindow(roadmapContent, versionOverride);
1292
- const documentHasPhaseEntries = hasPhaseEntries(stripShippedMilestones(roadmapContent));
1349
+ const documentHasPhaseEntries = hasPhaseEntries(stripShippedMilestones(roadmapContent), phaseIdConvention);
1293
1350
  if (sliced !== null) {
1294
1351
  versionScoped = true;
1295
1352
  versionSectionFound = true;
@@ -1312,7 +1369,7 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
1312
1369
  versionResolved: true,
1313
1370
  hasVersionedMilestones: hasVersionedMilestonesGlobal,
1314
1371
  headingFound: sliced !== null,
1315
- windowHasPhaseEntries: hasPhaseEntries(roadmap),
1372
+ windowHasPhaseEntries: hasPhaseEntries(roadmap, phaseIdConvention),
1316
1373
  documentHasPhaseEntries,
1317
1374
  });
1318
1375
  }
@@ -1508,6 +1565,8 @@ module.exports = {
1508
1565
  sliceMilestoneWindow,
1509
1566
  hasVersionedMilestones,
1510
1567
  hasMilestoneSectioning,
1568
+ // #3642: the >=1 sibling buildStateFrontmatter's unbounded branch consumes.
1569
+ hasAnyMilestoneSection,
1511
1570
  // #1956: sole owner of the #2012 decoy-avoidance scope for the
1512
1571
  // `drift-guard phase-status` CLI seam.
1513
1572
  findRoadmapProgressTable,
@@ -1518,4 +1577,8 @@ module.exports = {
1518
1577
  scanMilestonePhaseIds,
1519
1578
  collectTablePhaseRows,
1520
1579
  findMilestoneScopeHeadingLines,
1580
+ // #3641: the scope axis's phase-ENTRY predicate, exported so roadmap
1581
+ // validate's V004 document-level check routes through the same single
1582
+ // owner (and its convention gate) instead of a private inline copy.
1583
+ hasPhaseEntries,
1521
1584
  };
@@ -16,13 +16,13 @@ const pattern_cjs_1 = require("./pattern.cjs");
16
16
  const text_lines_cjs_1 = require("./text-lines.cjs");
17
17
  // eslint-disable-next-line @typescript-eslint/no-require-imports
18
18
  const ioMod = require("./io.cjs");
19
- const { output, error } = ioMod;
19
+ const { output, error, formatDiagnosticToken } = ioMod;
20
20
  // eslint-disable-next-line @typescript-eslint/no-require-imports
21
21
  const phaseIdMod = require("./phase-id.cjs");
22
22
  const { normalizePhaseName, phaseMarkdownRegexSource, matchPhaseDirs, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE, roadmapPhaseLookupSources, isSentinelPhaseId, scopeToPhase } = phaseIdMod;
23
23
  // eslint-disable-next-line @typescript-eslint/no-require-imports
24
24
  const phaseLocatorMod = require("./phase-locator.cjs");
25
- const { findPhaseInternal, listMilestonePhaseDirs } = phaseLocatorMod;
25
+ const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocatorMod;
26
26
  // eslint-disable-next-line @typescript-eslint/no-require-imports
27
27
  const planningScopeMod = require("./planning-scope.cjs");
28
28
  const { SCOPE } = planningScopeMod;
@@ -36,6 +36,11 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
36
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
37
37
  const planningWorkspace = require("./planning-workspace.cjs");
38
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;
39
44
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
45
  const scanPhasePlans = require("./plan-scan.cjs");
41
46
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -89,10 +94,21 @@ function countPhasePlansAndSummaries(phaseDir) {
89
94
  // hasContext and hasResearch are not plan-scan concerns — read the directory
90
95
  // once and share the listing for all non-plan metadata that cmdRoadmapAnalyze needs.
91
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;
92
103
  try {
93
104
  phaseFiles = node_fs_1.default.readdirSync(phaseDir);
94
105
  }
95
- 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
+ }
96
112
  // #3511: scope the raw listing to this phase dir before the
97
113
  // phase-numbered-artifact predicates (hasContext/hasResearch) — planCount/
98
114
  // summaryCount above stay on scanPhasePlans's own unscoped listing since a
@@ -104,6 +120,7 @@ function countPhasePlansAndSummaries(phaseDir) {
104
120
  summaryCount,
105
121
  hasContext: findContextMdIn(scopedFiles) !== null,
106
122
  hasResearch: scopedFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
123
+ contextReadError,
107
124
  };
108
125
  }
109
126
  // `phaseMarkdownRegexSource` lives in phase-id.cjs (#3537) and is imported above.
@@ -332,6 +349,10 @@ function collectAnalyzePhases(content, phasesDir, phaseDirNames) {
332
349
  let summaryCount = 0;
333
350
  let hasContext = false;
334
351
  let hasResearch = false;
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;
335
356
  // DEAD catch removed (#2245 audit): matchPhaseDirs(...) is a pure
336
357
  // array lookup on an already-resolved string array, and
337
358
  // countPhasePlansAndSummaries is itself fully defensive (its own
@@ -345,6 +366,7 @@ function collectAnalyzePhases(content, phasesDir, phaseDirNames) {
345
366
  summaryCount = counts.summaryCount;
346
367
  hasContext = counts.hasContext;
347
368
  hasResearch = counts.hasResearch;
369
+ contextReadError = counts.contextReadError;
348
370
  // ADR-3180 §7.4 (issue #3186, disk-strict, #3168 fix): route "is this
349
371
  // phase complete" through the canonical owner (`isPhaseComplete`),
350
372
  // which calls readVerificationStatus UNCONDITIONALLY — plan count is
@@ -394,6 +416,7 @@ function collectAnalyzePhases(content, phasesDir, phaseDirNames) {
394
416
  has_research: hasResearch,
395
417
  disk_status: diskStatus,
396
418
  roadmap_complete: roadmapComplete,
419
+ context_read_error: contextReadError,
397
420
  });
398
421
  }
399
422
  // #3577: markdown-table row declarations join the enumeration — same
@@ -410,12 +433,18 @@ function collectAnalyzePhases(content, phasesDir, phaseDirNames) {
410
433
  let tSummaryCount = 0;
411
434
  let tHasContext = false;
412
435
  let tHasResearch = false;
436
+ let tContextReadError = null;
413
437
  if (dirMatchA) {
414
438
  const counts = countPhasePlansAndSummaries(node_path_1.default.join(phasesDir, dirMatchA));
415
439
  tPlanCount = counts.planCount;
416
440
  tSummaryCount = counts.summaryCount;
417
441
  tHasContext = node_fs_1.default.existsSync(node_path_1.default.join(phasesDir, dirMatchA, 'CONTEXT.md'));
418
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;
419
448
  }
420
449
  phases.push({
421
450
  number: tr.id,
@@ -429,6 +458,7 @@ function collectAnalyzePhases(content, phasesDir, phaseDirNames) {
429
458
  has_research: tHasResearch,
430
459
  disk_status: dirMatchA ? 'ok' : 'no_directory',
431
460
  roadmap_complete: false,
461
+ context_read_error: tContextReadError,
432
462
  });
433
463
  }
434
464
  return phases;
@@ -446,21 +476,17 @@ function cmdRoadmapAnalyze(cwd, raw) {
446
476
  const { value: content, scope } = extractCurrentMilestoneScoped(rawContent, cwd);
447
477
  const phasesDir = planningPaths(cwd).phases;
448
478
  // 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
- })();
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;
464
490
  // Scan the scoped milestone window for phase-detail headings and enrich each
465
491
  // with its on-disk status. Extracted into `collectAnalyzePhases` (#3165) so
466
492
  // the SAME enrichment re-runs on the fallback below — not a second copy.
@@ -615,7 +641,36 @@ function cmdRoadmapMilestoneScope(cwd, raw) {
615
641
  return;
616
642
  }
617
643
  const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
618
- const { value: window, scope } = extractCurrentMilestoneScoped(rawContent, cwd);
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);
619
674
  // Document order (Set insertion order) — deterministic for a given document.
620
675
  const phases = [...scanMilestonePhaseIds(window)];
621
676
  output({ scope, phases, phase_count: phases.length }, raw, undefined);