@opengsd/gsd-core 1.12.0 โ†’ 1.14.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 (455) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-advisor-researcher.compact.md +85 -0
  5. package/agents/gsd-ai-researcher.compact.md +96 -0
  6. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  7. package/agents/gsd-code-fixer.compact.md +458 -0
  8. package/agents/gsd-code-fixer.md +5 -5
  9. package/agents/gsd-code-reviewer.compact.md +269 -0
  10. package/agents/gsd-code-reviewer.md +15 -3
  11. package/agents/gsd-codebase-mapper.compact.md +760 -0
  12. package/agents/gsd-debug-session-manager.compact.md +345 -0
  13. package/agents/gsd-doc-classifier.compact.md +192 -0
  14. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  15. package/agents/gsd-doc-verifier.compact.md +143 -0
  16. package/agents/gsd-doc-writer.compact.md +440 -0
  17. package/agents/gsd-dom-verifier.compact.md +138 -0
  18. package/agents/gsd-domain-researcher.compact.md +141 -0
  19. package/agents/gsd-eval-auditor.compact.md +160 -0
  20. package/agents/gsd-eval-planner.compact.md +137 -0
  21. package/agents/gsd-executor.md +63 -35
  22. package/agents/gsd-framework-selector.compact.md +82 -0
  23. package/agents/gsd-integration-checker.compact.md +245 -0
  24. package/agents/gsd-intel-updater.compact.md +226 -0
  25. package/agents/gsd-mempalace-curator.compact.md +45 -0
  26. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  27. package/agents/gsd-pattern-mapper.compact.md +275 -0
  28. package/agents/gsd-plan-checker.md +76 -57
  29. package/agents/gsd-planner.md +14 -0
  30. package/agents/gsd-project-researcher.compact.md +587 -0
  31. package/agents/gsd-research-synthesizer.compact.md +212 -0
  32. package/agents/gsd-roadmapper.compact.md +454 -0
  33. package/agents/gsd-roadmapper.md +13 -0
  34. package/agents/gsd-security-auditor.compact.md +162 -0
  35. package/agents/gsd-ui-auditor.compact.md +404 -0
  36. package/agents/gsd-ui-checker.compact.md +277 -0
  37. package/agents/gsd-ui-checker.md +19 -3
  38. package/agents/gsd-ui-researcher.compact.md +282 -0
  39. package/agents/gsd-ui-researcher.md +29 -0
  40. package/agents/gsd-user-profiler.compact.md +108 -0
  41. package/agents/gsd-verifier.md +23 -1
  42. package/bin/install.js +444 -134
  43. package/commands/gsd/cleanup.md +1 -0
  44. package/commands/gsd/code-review.md +2 -1
  45. package/commands/gsd/complete-milestone.md +1 -0
  46. package/commands/gsd/config.md +1 -0
  47. package/commands/gsd/debug.md +1 -0
  48. package/commands/gsd/execute-phase.md +1 -1
  49. package/commands/gsd/graphify.md +1 -0
  50. package/commands/gsd/health.md +1 -0
  51. package/commands/gsd/mempalace-capture.md +1 -0
  52. package/commands/gsd/mempalace-recall.md +1 -0
  53. package/commands/gsd/new-milestone.md +1 -0
  54. package/commands/gsd/new-project.md +1 -0
  55. package/commands/gsd/next.md +1 -0
  56. package/commands/gsd/ns-workflow.md +2 -1
  57. package/commands/gsd/pause-work.md +1 -0
  58. package/commands/gsd/phase.md +2 -1
  59. package/commands/gsd/pr-branch.md +1 -0
  60. package/commands/gsd/quick-batch.md +105 -0
  61. package/commands/gsd/resume-work.md +1 -0
  62. package/commands/gsd/review-backlog.md +1 -0
  63. package/commands/gsd/settings.md +2 -1
  64. package/commands/gsd/stats.md +1 -0
  65. package/commands/gsd/surface.md +18 -8
  66. package/commands/gsd/thread.md +1 -0
  67. package/commands/gsd/workspace.md +1 -0
  68. package/commands/gsd/workstreams.md +1 -0
  69. package/gsd-core/bin/check-latest-version.cjs +8 -3
  70. package/gsd-core/bin/gsd-tools.cjs +532 -174
  71. package/gsd-core/bin/lib/adr-parser.cjs +1 -1
  72. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  73. package/gsd-core/bin/lib/audit.cjs +39 -22
  74. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  75. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  76. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  77. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  78. package/gsd-core/bin/lib/capability-registry.cjs +528 -116
  79. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  80. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  81. package/gsd-core/bin/lib/capability-validator.cjs +134 -5
  82. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  83. package/gsd-core/bin/lib/check-command-router.cjs +198 -38
  84. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  85. package/gsd-core/bin/lib/clusters.cjs +1 -0
  86. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  87. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  88. package/gsd-core/bin/lib/commands.cjs +981 -79
  89. package/gsd-core/bin/lib/config-loader.cjs +4 -0
  90. package/gsd-core/bin/lib/config.cjs +153 -38
  91. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  92. package/gsd-core/bin/lib/coverage.cjs +1 -1
  93. package/gsd-core/bin/lib/decisions.cjs +343 -28
  94. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  95. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  96. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  97. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  98. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  99. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  100. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  101. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  102. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  103. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +87 -23
  104. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  105. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  106. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  107. package/gsd-core/bin/lib/init.cjs +539 -60
  108. package/gsd-core/bin/lib/install-engine.cjs +199 -14
  109. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  110. package/gsd-core/bin/lib/install-profiles.cjs +36 -14
  111. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  112. package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
  113. package/gsd-core/bin/lib/io.cjs +35 -0
  114. package/gsd-core/bin/lib/loop-resolver.cjs +64 -39
  115. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  116. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  117. package/gsd-core/bin/lib/milestone.cjs +41 -10
  118. package/gsd-core/bin/lib/model-resolver.cjs +101 -10
  119. package/gsd-core/bin/lib/phase-command-router.cjs +20 -7
  120. package/gsd-core/bin/lib/phase-id.cjs +412 -31
  121. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  122. package/gsd-core/bin/lib/phase.cjs +941 -98
  123. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +206 -30
  126. package/gsd-core/bin/lib/planning-workspace.cjs +153 -29
  127. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  128. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  129. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  130. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  131. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  132. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  133. package/gsd-core/bin/lib/research-store.cjs +11 -12
  134. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  135. package/gsd-core/bin/lib/review-lane-invocation.cjs +96 -1
  136. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  137. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  138. package/gsd-core/bin/lib/roadmap-parser.cjs +555 -41
  139. package/gsd-core/bin/lib/roadmap.cjs +292 -69
  140. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +260 -43
  141. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +28 -20
  142. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +294 -108
  143. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -47
  144. package/gsd-core/bin/lib/security.cjs +126 -7
  145. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  146. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  147. package/gsd-core/bin/lib/state-document.cjs +159 -32
  148. package/gsd-core/bin/lib/state-md-schema.cjs +44 -27
  149. package/gsd-core/bin/lib/state-transition.cjs +465 -62
  150. package/gsd-core/bin/lib/state.cjs +906 -151
  151. package/gsd-core/bin/lib/surface.cjs +83 -10
  152. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  153. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  154. package/gsd-core/bin/lib/uat.cjs +1420 -516
  155. package/gsd-core/bin/lib/update-context.cjs +36 -26
  156. package/gsd-core/bin/lib/validate.cjs +230 -12
  157. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  158. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  159. package/gsd-core/bin/lib/verification.cjs +316 -23
  160. package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
  161. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  162. package/gsd-core/bin/lib/verify.cjs +531 -36
  163. package/gsd-core/bin/lib/workstream-inventory.cjs +21 -2
  164. package/gsd-core/bin/lib/worktree-safety.cjs +21 -7
  165. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  166. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  167. package/gsd-core/bin/verify-reapply-patches.cjs +507 -81
  168. package/gsd-core/references/agent-contracts.md +3 -3
  169. package/gsd-core/references/compact-content-gate.md +66 -0
  170. package/gsd-core/references/edge-probe.md +17 -13
  171. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  172. package/gsd-core/references/execute-phase-response-language.md +6 -0
  173. package/gsd-core/references/executor-examples.md +42 -0
  174. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  175. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  176. package/gsd-core/references/model-profiles.md +12 -3
  177. package/gsd-core/references/mvp-concepts.md +2 -2
  178. package/gsd-core/references/plan-checker-examples.md +41 -0
  179. package/gsd-core/references/planner-antipatterns.md +25 -0
  180. package/gsd-core/references/planner-chunked.md +5 -1
  181. package/gsd-core/references/planner-coupling.md +42 -0
  182. package/gsd-core/references/planner-quick-batch.md +71 -0
  183. package/gsd-core/references/planner-reviews.md +47 -0
  184. package/gsd-core/references/planner-revision.md +75 -2
  185. package/gsd-core/references/planning-config.md +5 -1
  186. package/gsd-core/references/response-language-directive.md +9 -0
  187. package/gsd-core/references/revision-loop.md +118 -11
  188. package/gsd-core/references/tdd.md +17 -9
  189. package/gsd-core/references/thinking-models-planning.md +18 -2
  190. package/gsd-core/references/verification-patterns.md +17 -4
  191. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  192. package/gsd-core/references/worktree-path-safety.md +112 -2
  193. package/gsd-core/templates/README.md +7 -1
  194. package/gsd-core/templates/phase-prompt.md +4 -0
  195. package/gsd-core/templates/state.md +6 -3
  196. package/gsd-core/templates/summary.compact.md +212 -0
  197. package/gsd-core/templates/user-setup.compact.md +199 -0
  198. package/gsd-core/templates/user-setup.md +0 -9
  199. package/gsd-core/templates/verification-report.md +5 -0
  200. package/gsd-core/workflows/add-backlog.md +2 -0
  201. package/gsd-core/workflows/add-phase.md +2 -0
  202. package/gsd-core/workflows/add-tests.md +1 -1
  203. package/gsd-core/workflows/add-todo.md +4 -3
  204. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  205. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  206. package/gsd-core/workflows/audit-fix.md +2 -0
  207. package/gsd-core/workflows/audit-milestone.md +2 -0
  208. package/gsd-core/workflows/audit-uat.md +2 -0
  209. package/gsd-core/workflows/autonomous.md +15 -10
  210. package/gsd-core/workflows/check-todos.md +5 -3
  211. package/gsd-core/workflows/cleanup.md +4 -2
  212. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +22 -13
  213. package/gsd-core/workflows/code-review-fix.md +5 -3
  214. package/gsd-core/workflows/code-review.md +211 -43
  215. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  216. package/gsd-core/workflows/complete-milestone.md +40 -254
  217. package/gsd-core/workflows/debug.md +1 -1
  218. package/gsd-core/workflows/diagnose-issues.md +5 -1
  219. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  220. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  221. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  222. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  223. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  224. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  225. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  226. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  227. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  228. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  229. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  230. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  231. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  232. package/gsd-core/workflows/discuss-phase.md +1 -1
  233. package/gsd-core/workflows/do.md +43 -13
  234. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  235. package/gsd-core/workflows/docs-update.md +15 -156
  236. package/gsd-core/workflows/edit-phase.md +2 -0
  237. package/gsd-core/workflows/eval-review.md +1 -1
  238. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  239. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +20 -3
  240. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  241. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +24 -3
  242. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  243. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  245. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  246. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  247. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  248. package/gsd-core/workflows/execute-phase.md +78 -159
  249. package/gsd-core/workflows/execute-plan.md +28 -15
  250. package/gsd-core/workflows/explore.md +2 -0
  251. package/gsd-core/workflows/extract-learnings.md +2 -0
  252. package/gsd-core/workflows/fast.md +6 -0
  253. package/gsd-core/workflows/forensics.md +2 -0
  254. package/gsd-core/workflows/graduation.md +1 -1
  255. package/gsd-core/workflows/health.md +1 -1
  256. package/gsd-core/workflows/help/modes/brief.md +2 -0
  257. package/gsd-core/workflows/help/modes/default.md +2 -0
  258. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  259. package/gsd-core/workflows/help/modes/full.md +12 -0
  260. package/gsd-core/workflows/help/modes/topic.md +2 -0
  261. package/gsd-core/workflows/help.md +3 -1
  262. package/gsd-core/workflows/import.md +3 -3
  263. package/gsd-core/workflows/inbox.md +1 -1
  264. package/gsd-core/workflows/ingest-docs.md +1 -1
  265. package/gsd-core/workflows/insert-phase.md +2 -0
  266. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  267. package/gsd-core/workflows/list-seeds.md +2 -0
  268. package/gsd-core/workflows/list-workspaces.md +2 -0
  269. package/gsd-core/workflows/manager.md +3 -3
  270. package/gsd-core/workflows/map-codebase.md +52 -3
  271. package/gsd-core/workflows/milestone-summary.md +2 -0
  272. package/gsd-core/workflows/mvp-phase.md +1 -1
  273. package/gsd-core/workflows/new-milestone.md +55 -13
  274. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  275. package/gsd-core/workflows/new-project.md +37 -205
  276. package/gsd-core/workflows/new-workspace.md +1 -1
  277. package/gsd-core/workflows/next.md +2 -0
  278. package/gsd-core/workflows/node-repair.md +2 -0
  279. package/gsd-core/workflows/note.md +2 -0
  280. package/gsd-core/workflows/onboard.md +1 -1
  281. package/gsd-core/workflows/pause-work.md +19 -4
  282. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  283. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  284. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  285. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  286. package/gsd-core/workflows/plan-phase.md +144 -185
  287. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  288. package/gsd-core/workflows/plant-seed.md +1 -1
  289. package/gsd-core/workflows/pr-branch.md +30 -10
  290. package/gsd-core/workflows/profile-user.md +1 -1
  291. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  292. package/gsd-core/workflows/progress.md +25 -3
  293. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  294. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  295. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  296. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  297. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  298. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  299. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  300. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  301. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  302. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  303. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  304. package/gsd-core/workflows/quick-batch.md +203 -0
  305. package/gsd-core/workflows/quick.md +21 -4
  306. package/gsd-core/workflows/reapply-patches.md +79 -3
  307. package/gsd-core/workflows/remove-phase.md +2 -0
  308. package/gsd-core/workflows/remove-workspace.md +1 -1
  309. package/gsd-core/workflows/resume-project.md +6 -2
  310. package/gsd-core/workflows/review.md +215 -10
  311. package/gsd-core/workflows/scan.md +2 -0
  312. package/gsd-core/workflows/section-manifest.json +12 -0
  313. package/gsd-core/workflows/secure-phase.md +1 -1
  314. package/gsd-core/workflows/session-report.md +2 -0
  315. package/gsd-core/workflows/settings-advanced.md +2 -0
  316. package/gsd-core/workflows/settings-integrations.md +9 -8
  317. package/gsd-core/workflows/settings.md +19 -6
  318. package/gsd-core/workflows/ship.md +10 -10
  319. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  320. package/gsd-core/workflows/sketch.md +1 -1
  321. package/gsd-core/workflows/smart-entry.md +1 -1
  322. package/gsd-core/workflows/spec-phase.md +24 -19
  323. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  324. package/gsd-core/workflows/spike.md +1 -1
  325. package/gsd-core/workflows/stats.md +2 -0
  326. package/gsd-core/workflows/sync-skills.md +12 -4
  327. package/gsd-core/workflows/thread.md +2 -0
  328. package/gsd-core/workflows/transition.md +2 -0
  329. package/gsd-core/workflows/ui-phase.md +26 -5
  330. package/gsd-core/workflows/ui-review.md +1 -1
  331. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  332. package/gsd-core/workflows/undo.md +1 -1
  333. package/gsd-core/workflows/update.md +48 -43
  334. package/gsd-core/workflows/validate-phase.md +1 -1
  335. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  336. package/gsd-core/workflows/verify-work.md +68 -182
  337. package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
  338. package/hooks/dist/gsd-check-update-worker.js +19 -2
  339. package/hooks/dist/gsd-context-monitor.js +371 -27
  340. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  341. package/hooks/dist/gsd-node-runner.sh +1 -0
  342. package/hooks/dist/gsd-prompt-guard.js +30 -5
  343. package/hooks/dist/gsd-read-guard.js +2 -0
  344. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  345. package/hooks/dist/gsd-secret-read-guard.js +1105 -0
  346. package/hooks/dist/gsd-statusline.js +18 -10
  347. package/hooks/dist/gsd-validate-commit.sh +474 -7
  348. package/hooks/dist/gsd-workflow-guard.js +2 -1
  349. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  350. package/hooks/dist/gsd-write-guard.js +46 -1
  351. package/hooks/dist/lib/dispatch-identity.js +187 -0
  352. package/hooks/dist/lib/filename-classification.js +64 -0
  353. package/hooks/dist/lib/git-cmd.js +210 -1
  354. package/hooks/dist/lib/injection-patterns.js +36 -6
  355. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  356. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  357. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  358. package/hooks/gsd-agent-isolation-guard.js +42 -16
  359. package/hooks/gsd-check-update-worker.js +19 -2
  360. package/hooks/gsd-context-monitor.js +371 -27
  361. package/hooks/gsd-cursor-subagent-start.js +34 -14
  362. package/hooks/gsd-node-runner.sh +1 -0
  363. package/hooks/gsd-prompt-guard.js +30 -5
  364. package/hooks/gsd-read-guard.js +2 -0
  365. package/hooks/gsd-read-injection-scanner.js +5 -5
  366. package/hooks/gsd-secret-read-guard.js +1105 -0
  367. package/hooks/gsd-statusline.js +18 -10
  368. package/hooks/gsd-validate-commit.sh +474 -7
  369. package/hooks/gsd-workflow-guard.js +2 -1
  370. package/hooks/gsd-worktree-path-guard.js +25 -14
  371. package/hooks/gsd-write-guard.js +46 -1
  372. package/hooks/hooks.json +6 -0
  373. package/hooks/lib/dispatch-identity.js +187 -0
  374. package/hooks/lib/filename-classification.js +64 -0
  375. package/hooks/lib/git-cmd.js +210 -1
  376. package/hooks/lib/injection-patterns.js +36 -6
  377. package/hooks/lib/isolation-deny-reason.js +53 -1
  378. package/hooks/lib/isolation-sentinel.js +58 -19
  379. package/hooks/managed-hooks-registry.cjs +1 -0
  380. package/package.json +13 -9
  381. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  382. package/scripts/benchmark-compact-content.cjs +368 -0
  383. package/scripts/build-hooks.js +11 -4
  384. package/scripts/check-contract-drift.cjs +4 -1
  385. package/scripts/check-env.cjs +36 -8
  386. package/scripts/check-glossary-refs.cjs +25 -21
  387. package/scripts/ci-next-health.cjs +271 -0
  388. package/scripts/ci-prepare-test-scope.cjs +7 -7
  389. package/scripts/ci-test-scope.cjs +133 -20
  390. package/scripts/ci-timeout-report.cjs +1 -1
  391. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  392. package/scripts/docs-guard-registry.cjs +17 -2
  393. package/scripts/gen-adr-index.cjs +8 -2
  394. package/scripts/gen-inventory-manifest.cjs +12 -0
  395. package/scripts/gen-loop-host-contract.cjs +67 -15
  396. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  397. package/scripts/lib/drift-scan.cjs +1 -1
  398. package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
  399. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  400. package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
  401. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  402. package/scripts/lib/suite-detection.cjs +32 -0
  403. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  404. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  405. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  406. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  407. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +24 -2
  408. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  409. package/scripts/lint-phase-id-drift.cjs +465 -15
  410. package/scripts/lint-portable-grep.cjs +176 -0
  411. package/scripts/lint-response-language-coverage.cjs +530 -0
  412. package/scripts/lint-source-test-name-collision.cjs +1 -1
  413. package/scripts/lint-test-file-count.allowlist.json +4 -1
  414. package/scripts/lint-vendored-deps.cjs +128 -17
  415. package/scripts/lint-workflow-shellcheck-baseline.json +1112 -0
  416. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  417. package/scripts/npm-audit-baseline.cjs +376 -0
  418. package/scripts/prompt-injection-scan.sh +22 -0
  419. package/scripts/require-issue-link-policy.cjs +16 -1
  420. package/scripts/workflow-size.cjs +139 -0
  421. package/skills/gsd-cleanup/SKILL.md +1 -0
  422. package/skills/gsd-code-review/SKILL.md +2 -1
  423. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  424. package/skills/gsd-config/SKILL.md +1 -0
  425. package/skills/gsd-debug/SKILL.md +1 -0
  426. package/skills/gsd-execute-phase/SKILL.md +1 -1
  427. package/skills/gsd-graphify/SKILL.md +1 -0
  428. package/skills/gsd-health/SKILL.md +1 -0
  429. package/skills/gsd-mempalace-capture/SKILL.md +1 -0
  430. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  431. package/skills/gsd-new-milestone/SKILL.md +1 -0
  432. package/skills/gsd-new-project/SKILL.md +1 -0
  433. package/skills/gsd-next/SKILL.md +1 -0
  434. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  435. package/skills/gsd-pause-work/SKILL.md +1 -0
  436. package/skills/gsd-phase/SKILL.md +2 -1
  437. package/skills/gsd-pr-branch/SKILL.md +1 -0
  438. package/skills/gsd-quick-batch/SKILL.md +105 -0
  439. package/skills/gsd-resume-work/SKILL.md +1 -0
  440. package/skills/gsd-review-backlog/SKILL.md +1 -0
  441. package/skills/gsd-settings/SKILL.md +2 -1
  442. package/skills/gsd-stats/SKILL.md +1 -0
  443. package/skills/gsd-surface/SKILL.md +18 -8
  444. package/skills/gsd-thread/SKILL.md +1 -0
  445. package/skills/gsd-workspace/SKILL.md +1 -0
  446. package/skills/gsd-workstreams/SKILL.md +1 -0
  447. package/vscode/package.json +1 -1
  448. package/gsd-core/templates/claude-md.md +0 -145
  449. package/gsd-core/templates/codebase/concerns.md +0 -310
  450. package/gsd-core/templates/codebase/conventions.md +0 -307
  451. package/gsd-core/templates/codebase/integrations.md +0 -280
  452. package/gsd-core/templates/codebase/structure.md +0 -285
  453. package/gsd-core/templates/codebase/testing.md +0 -480
  454. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  455. package/gsd-core/templates/discovery.md +0 -146
@@ -32,10 +32,25 @@ const { phaseMarkdownRegexSource, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOU
32
32
  roadmapPhaseLookupSources, extractPhaseToken, isSentinelPhaseId,
33
33
  // #3641: the single-owner heading-intro and digit-token grammar sources โ€”
34
34
  // see BRACKET_PHASE_ENTRY_HEADING_RE below.
35
- PHASE_HEADING_PREFIX_SRC, PHASE_NUMBER_TOKEN_SOURCE, } = phaseIdModule;
35
+ PHASE_HEADING_PREFIX_SRC, PHASE_NUMBER_TOKEN_SOURCE, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE,
36
+ // #612: the disk-side milestone filter resolves bracket directories through
37
+ // the owner's gated helpers rather than spelling the grammar a second time.
38
+ phaseTokenMatches,
39
+ // #2761 B1: the version-less bracket milestone boundary (computeSectionEnd /
40
+ // preambleCutoff, below) is built from this single-owner source rather than a
41
+ // re-typed bracket-id literal.
42
+ BRACKET_ID_SRC,
43
+ // #2761 M3: the PINNED bracket milestone intro, owning the pad2 spelling rule
44
+ // as well as the grammar โ€” consumed by the bracket-fallback selector below,
45
+ // which re-typed the project-code class and restated the padding rule.
46
+ bracketMilestoneIntroSrcFor,
47
+ // #2761 B1 (round-2 fix): fold-before-identity for the SAME-MILESTONE
48
+ // continuation check in isBracketMilestoneBoundary, below โ€” the branch's own
49
+ // convention (matches bracketQualifiedKey/isSentinelPhaseId).
50
+ foldBracketId, } = phaseIdModule;
36
51
  // eslint-disable-next-line @typescript-eslint/no-require-imports
37
52
  const planningWorkspace = require("./planning-workspace.cjs");
38
- const { planningDir } = planningWorkspace;
53
+ const { planningDir, resolvePhaseIdConvention } = planningWorkspace;
39
54
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
40
55
  // eslint-disable-next-line @typescript-eslint/no-require-imports
41
56
  const unusableInputMod = require("./unusable-input.cjs");
@@ -63,6 +78,21 @@ function isClosedMilestoneHeading(headingText) {
63
78
  function stripShippedMilestones(content) {
64
79
  return (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(content, 'details');
65
80
  }
81
+ /**
82
+ * #3982: strip only <details> blocks whose <summary> marks a CLOSED milestone
83
+ * (ARCHIVED/SHIPPED/โœ…/โ€ฆ without an active marker) โ€” the narrow form of
84
+ * stripShippedMilestones the current-milestone window needs. A blanket strip
85
+ * would delete the ACTIVE milestone's own collapsed blocks (#1341) and
86
+ * reproduce the phase_count: 0 class of #557/#2947.
87
+ */
88
+ function stripClosedMilestoneDetails(content) {
89
+ return content.replace(/<details\b[^>]*>[\s\S]*?<\/details>/gi, (block) => {
90
+ const summaryMatch = block.match(/<summary[^>]*>([^<]*)<\/summary>/i);
91
+ if (!summaryMatch)
92
+ return block;
93
+ return isClosedMilestoneHeading(summaryMatch[1]) ? '' : block;
94
+ });
95
+ }
66
96
  /**
67
97
  * #2562: is the milestone `version` marked SHIPPED by the ROADMAP itself?
68
98
  *
@@ -99,6 +129,129 @@ function isMilestoneShippedInRoadmap(content, version) {
99
129
  }
100
130
  return false;
101
131
  }
132
+ // #2761 B1: matches a bracket MILESTONE heading's intro (`[GSD.02]`) at the
133
+ // START of a heading's text, capturing the bracket id in group 1. Shared by
134
+ // isBracketMilestoneBoundary below โ€” the ONE recognizer for "is this heading
135
+ // bracket-shaped", so computeSectionEnd and the preambleCutoff scan cannot
136
+ // independently drift on what counts as bracket-shaped (the drift that
137
+ // produced Blocker 3 in the round-2 review).
138
+ const BRACKET_HEADING_INTRO_RE = new RegExp(`^\\[(${BRACKET_ID_SRC})\\]`, 'i');
139
+ // #2761 B2 (round-2 fix, Blocker 2): ADR-612 Decision 1's own discriminator
140
+ // (docs/adr/612-bracket-phase-id-convention.md:56) โ€” "a phase heading is a
141
+ // bracket followed by a digit-then-colon (`[GSD.02] 05:`); a milestone
142
+ // heading is a bracket followed by a name" โ€” is CONTENT, not heading level.
143
+ // The prior `h.level <= 2` level cap broke on a level-3 bracket milestone
144
+ // heading (`### [GSD.02] Foundation`): its own level-3 phase children
145
+ // (`#### [GSD.02] 01: One`) never reached this check at all (excluded
146
+ // upstream by the `h.level > level` sibling-depth filter), but a level-3
147
+ // SIBLING milestone heading (`### [GSD.03] Later`) was ALSO excluded by the
148
+ // level cap, so the section ran to EOF instead of stopping there โ€” trek-e's
149
+ // original #612 defect, reopened on any milestone heading below level 2.
150
+ //
151
+ // Built by interpolating phase-id.cts's single-owner
152
+ // phaseHeadingPrefixSrcFor (the SAME intro grammar getMilestonePhaseFilter's
153
+ // heading counter and extractRetiredPhaseNumbers already compile) plus the
154
+ // digit + optional-tag + colon tail every phase-heading counter in this file
155
+ // already spells (mirrors the `([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:`
156
+ // shape at :nnn below) โ€” not a re-typed grammar. Covers the dotted sub-phase
157
+ // heading form too (`[GSD.02] 05.03:`) via the same `[\w][\w.-]*` token,
158
+ // which admits an embedded `.`.
159
+ const BRACKET_PHASE_TAIL_RE = new RegExp(`^${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.ANY_BRACKET, 'bracket', false)}[\\w][\\w.-]*(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
160
+ /**
161
+ * #2761 B1/B2 (round-2 fixes): is `headingText` (hashes STRIPPED โ€” the
162
+ * `tokenizeHeadings` `HeadingToken.text` shape, and the shape the
163
+ * preambleCutoff scan below is made to match) a BRACKET MILESTONE boundary โ€”
164
+ * as opposed to (a) a bracket PHASE heading (at any level, including the
165
+ * dotted sub-phase form), which must never terminate a milestone's own
166
+ * section, or (b) a heading that names the SAME milestone already selected,
167
+ * which is a CONTINUATION of the current milestone's own section (a
168
+ * version-less split like `## [GSD.02] Foundation (Phase Details)`), not the
169
+ * boundary to a DIFFERENT one?
170
+ *
171
+ * `level` is the CANDIDATE heading's own depth (`h.level`), capped at 3 โ€” a
172
+ * depth-SANITY ceiling, not a phase/milestone discriminator (that job is
173
+ * BRACKET_PHASE_TAIL_RE, below). The cap mirrors the bracket-fallback
174
+ * selector's own `#{1,3}` ceiling (this file's SELECTION branch above) and
175
+ * `isMilestoneBounded`'s (`state.cts`) โ€” a bracket-shaped heading deeper than
176
+ * either of those will ever select as a CURRENT milestone is outside the
177
+ * shape this function needs to discriminate at all.
178
+ *
179
+ * `selectedBracketId` is the SELECTED milestone's own bracket id, already
180
+ * case-folded by the caller โ€” `null` when scoping is not bracket-gated, the
181
+ * selected heading is not itself bracket-shaped, or (preambleCutoff) the
182
+ * same-milestone check does not apply at this call site (see its own comment
183
+ * there) โ€” in which case the same-milestone check below simply never fires.
184
+ */
185
+ function isBracketMilestoneBoundary(headingText, level, selectedBracketId) {
186
+ if (level > 3)
187
+ return false;
188
+ const introMatch = BRACKET_HEADING_INTRO_RE.exec(headingText);
189
+ if (!introMatch)
190
+ return false;
191
+ // #2761 B2: a bracket PHASE heading (`[GSD.02] 05:`, or the dotted
192
+ // sub-phase form `[GSD.02] 05.03:`) is never a milestone boundary,
193
+ // regardless of level.
194
+ if (BRACKET_PHASE_TAIL_RE.test(headingText))
195
+ return false;
196
+ // #2761 B1: fold-before-identity โ€” this branch's own convention
197
+ // (bracketQualifiedKey / isSentinelPhaseId apply the same rule).
198
+ if (selectedBracketId && foldBracketId(introMatch[1]) === selectedBracketId)
199
+ return false;
200
+ return true;
201
+ }
202
+ /**
203
+ * #2761 B1 (round-3 fix, Blocker 1 case D; hardened post-round-3; round-4
204
+ * fix: requires a same-id PHASE child, not merely a same-id child): does
205
+ * `headings[index]`'s SUBTREE โ€” every heading strictly deeper than it, up to
206
+ * (not including) the next heading at or above its own level โ€” contain a
207
+ * bracket-shaped, PHASE-TAIL-shaped heading with the SAME id as
208
+ * `headings[index]`'s own?
209
+ *
210
+ * Used ONLY at the preambleCutoff scan below, to distinguish a genuine prior
211
+ * or later sibling milestone โ€” whose subtree contains a real phase heading
212
+ * carrying its bracket id (`## [GSD.01] Setup` / `### [GSD.01] 01: โ€ฆ`)
213
+ * โ€” from an unrelated bracket-shaped PROSE heading sitting above the current
214
+ * milestone's own content. Same-id-ness alone is insufficient: round-4 F1's
215
+ * `[ADR.612] Heading convention` owns `[ADR.612] Examples`, but that child is
216
+ * milestone-shaped, not a digit-colon phase. Reuse `BRACKET_PHASE_TAIL_RE`
217
+ * for the phase-vs-milestone distinction instead of re-deriving it.
218
+ *
219
+ * Scan the whole subtree, not just the immediate child: a genuine milestone
220
+ * may open with `### Notes` before its first matching-id phase. Stopping at
221
+ * that first non-match leaked the prior milestone's qualified phase into the
222
+ * current filter (3/2/67 instead of 2/1/50 in rv2-amend1).
223
+ *
224
+ * A subtree that closes with no same-id phase hit โ€” including a childless
225
+ * heading or one with only milestone-shaped same-id children โ€” degrades to
226
+ * not-a-boundary. That is deliberately over-inclusive: the unmatched text
227
+ * remains in the preamble rather than silently discarding current content.
228
+ *
229
+ * `headings` is the same fence-aware token list the caller iterates; `index`
230
+ * is the candidate's own position in it.
231
+ */
232
+ function bracketHeadingHasMatchingChild(headings, index) {
233
+ const candidate = headings[index];
234
+ const ownMatch = BRACKET_HEADING_INTRO_RE.exec(candidate.text);
235
+ if (!ownMatch)
236
+ return false;
237
+ const ownId = foldBracketId(ownMatch[1]);
238
+ for (let i = index + 1; i < headings.length; i++) {
239
+ const next = headings[i];
240
+ if (next.level <= candidate.level)
241
+ return false;
242
+ const childMatch = BRACKET_HEADING_INTRO_RE.exec(next.text);
243
+ // #2761 round-4: a same-id child is not enough โ€” it must also be a PHASE
244
+ // (BRACKET_PHASE_TAIL_RE), or an unrelated PROSE heading whose own
245
+ // sub-heading merely happens to share its bracket id (F1: [ADR.612]
246
+ // Heading convention / [ADR.612] Examples) satisfies this rule.
247
+ if (childMatch && foldBracketId(childMatch[1]) === ownId && BRACKET_PHASE_TAIL_RE.test(next.text))
248
+ return true;
249
+ // Not a same-id PHASE match โ€” keep scanning DEEPER into the subtree
250
+ // instead of giving up on this one heading; only a same-or-shallower
251
+ // heading (above) actually closes the subtree.
252
+ }
253
+ return false;
254
+ }
102
255
  /**
103
256
  * #3184 (epic #3180 Phase 2): the sole owner of "where does this milestone
104
257
  * heading's section end". Lifted from `currentMilestoneRawRanges`'s prior
@@ -108,12 +261,12 @@ function isMilestoneShippedInRoadmap(content, version) {
108
261
  * `getMilestonePhaseFilter`'s versionOverride branch all call this instead of
109
262
  * re-deriving it.
110
263
  */
111
- function computeMilestoneSectionEnd(content, headingText, headingStart) {
264
+ function computeMilestoneSectionEnd(content, headingText, headingStart, additionalBoundary, headingTokens) {
112
265
  const level = (headingText.match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
113
266
  const afterHeading = headingStart + headingText.length;
114
267
  // Use tokenizeHeadings (fence-aware, offsets into original content) to find
115
268
  // the next stop boundary without re-implementing fence detection. T4 seam migration.
116
- const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
269
+ const headings = headingTokens ?? (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
117
270
  for (const h of headings) {
118
271
  if (h.offset <= headingStart)
119
272
  continue;
@@ -124,9 +277,10 @@ function computeMilestoneSectionEnd(content, headingText, headingStart) {
124
277
  // Mirrors old stopPattern: level-bounded, not a Phase heading, milestone marker
125
278
  if (/^Phase\s+\S/i.test(h.text))
126
279
  continue;
127
- if (!/v\d+\.\d+|โœ…|๐Ÿ“‹|๐Ÿšง/i.test(h.text))
128
- continue;
129
- return h.offset;
280
+ if (/v\d+\.\d+|โœ…|๐Ÿ“‹|๐Ÿšง/i.test(h.text))
281
+ return h.offset;
282
+ if (additionalBoundary?.(h))
283
+ return h.offset;
130
284
  }
131
285
  return content.length;
132
286
  }
@@ -472,16 +626,17 @@ function hasPhaseEntries(markdown, phaseIdConvention) {
472
626
  return collectTablePhaseRows(markdown).length > 0;
473
627
  }
474
628
  // โ”€โ”€โ”€ #3577: markdown-table phase listings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
475
- // #3577: a GFM table declares phases when its header's FIRST cell is the literal
476
- // `Phase` (optionally `Phase #` / `Phase No.` / `Phase number`) and the header does
477
- // NOT match a known non-listing schema โ€” the canonical RoadmapProgress table
478
- // (`| Phase | Plans Complete | Status | Completed |`) leads with `Phase` too, and
479
- // its rows are progress markers, not declarations. Data rows carry the phase id in
480
- // their first cell (digit-bearing canonical shape โ€” `Phase`-word header cells and
481
- // `---` delimiter rows are digit-free and excluded by construction). Fence-aware
482
- // via stripFencedCode, matching the #3184 lesson: a fenced EXAMPLE of the table
483
- // form is not a declared phase.
629
+ // #3577/#4480: a GFM table declares phases only when its header's FIRST cell is
630
+ // the literal `Phase` (optionally `Phase #` / `Phase No.` / `Phase number`) AND
631
+ // it positively identifies a `Name` or `Phase Name` column. This fails closed:
632
+ // ordinary progress/summary tables such as `| Phase | Status |` cannot mint a
633
+ // phase whose name is whichever value happens to occupy column two. Data rows
634
+ // carry the phase id in their first cell (digit-bearing canonical shape โ€”
635
+ // `Phase`-word header cells and `---` delimiter rows are digit-free and excluded
636
+ // by construction). Fence-aware via stripFencedCode, matching the #3184 lesson:
637
+ // a fenced EXAMPLE of the table form is not a declared phase.
484
638
  const PHASE_LISTING_HEADER_RE = /^\|?\s*phase(?:\s*(?:#|no\.?|number))?\s*\|/i;
639
+ const PHASE_NAME_HEADER_RE = /^(?:phase\s+)?name$/i;
485
640
  const TABLE_PHASE_ID_RE = /^[A-Za-z]?\d[\w.-]*$/;
486
641
  function collectTablePhaseRows(window) {
487
642
  const unfenced = (0, markdown_sectionizer_cjs_1.stripFencedCode)(window).text;
@@ -491,8 +646,9 @@ function collectTablePhaseRows(window) {
491
646
  if (!PHASE_LISTING_HEADER_RE.test(lines[i]))
492
647
  continue;
493
648
  const headerCells = (0, markdown_table_cjs_1.splitTableRow)(lines[i]);
494
- if ((0, markdown_table_cjs_1.matchTableSchema)(headerCells) !== null)
495
- continue; // canonical non-listing schema
649
+ const nameColumn = headerCells.findIndex((cell) => PHASE_NAME_HEADER_RE.test(cell));
650
+ if (nameColumn === -1)
651
+ continue;
496
652
  if (!(0, markdown_table_cjs_1.isDelimiterRow)((0, markdown_table_cjs_1.splitTableRow)(lines[i + 1])))
497
653
  continue;
498
654
  for (let j = i + 2; j < lines.length; j++) {
@@ -508,7 +664,8 @@ function collectTablePhaseRows(window) {
508
664
  if (!TABLE_PHASE_ID_RE.test(first))
509
665
  continue;
510
666
  if (!/^999\b/.test(first)) {
511
- rows.push({ id: first, name: cells[1] && cells[1] !== '' ? cells[1] : null, row: lines[j] });
667
+ const name = cells[nameColumn];
668
+ rows.push({ id: first, name: name && name !== '' ? name : null, row: lines[j] });
512
669
  }
513
670
  }
514
671
  }
@@ -532,18 +689,35 @@ function collectTablePhaseRows(window) {
532
689
  * narrower question โ€” "which phase ids does this window declare" โ€” where only
533
690
  * the 999 icebox range is excluded.
534
691
  */
535
- function scanMilestonePhaseIds(window) {
692
+ function scanMilestonePhaseIdSets(window, convention) {
536
693
  const ids = new Set();
694
+ const qualifiedIds = new Set();
537
695
  // Use tokenizeHeadings (fence-aware) instead of stripFencedLines + regex.
538
696
  // T4 seam migration: phase headings inside fences are excluded automatically.
539
697
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
540
- const phaseHeadingPattern = /^(?:\[[^\]]{1,200}\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/i;
698
+ // #612: select the heading grammar from the resolved convention and retain
699
+ // the bracket id so the disk-side filter can distinguish equal phase tokens
700
+ // belonging to different milestones.
701
+ const capturing = convention === 'bracket';
702
+ const bracketGroups = capturing ? 1 : 0;
703
+ const phaseHeadingPattern = new RegExp(`^${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.ANY_BRACKET, convention, capturing)}([\\w][\\w.-]*)(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
541
704
  for (const h of (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(window)) {
542
705
  if (h.level < 2 || h.level > 4)
543
706
  continue;
544
707
  const pm = phaseHeadingPattern.exec(h.text);
545
- if (pm && !/^999\b/.test(pm[1]))
546
- ids.add(pm[1]);
708
+ if (!pm)
709
+ continue;
710
+ const bracketId = bracketGroups ? pm[1] : undefined;
711
+ const token = pm[1 + bracketGroups];
712
+ if (bracketId && isSentinelPhaseId(`${bracketId}-${token}`, 'bracket'))
713
+ continue;
714
+ // This scan's legacy contract excludes only the 999 icebox range. Keep the
715
+ // narrower rule so real decimal phase 00.1 is not swallowed as milestone 0.
716
+ if (/^999\b/.test(token))
717
+ continue;
718
+ ids.add(token);
719
+ if (bracketId && !token.includes('-'))
720
+ qualifiedIds.add(`${bracketId}-${token}`);
547
721
  }
548
722
  // #2199: also count bullet/checkbox phase entries (`- [ ] **Phase N โ€” name**`)
549
723
  // so a bullet-house-style ROADMAP populates the milestone phase set instead of
@@ -559,7 +733,16 @@ function scanMilestonePhaseIds(window) {
559
733
  // filter must not collapse a table-house-style window to zero-count.
560
734
  for (const tr of collectTablePhaseRows(window))
561
735
  ids.add(tr.id);
562
- return ids;
736
+ return { ids, qualifiedIds };
737
+ }
738
+ /**
739
+ * Public #3262 owner contract: the declared-id Set remains directly iterable.
740
+ * #612's qualified bracket ids are an internal companion used only by the
741
+ * milestone directory filter, so extending that internal read must not break
742
+ * existing consumers of this exported Set (including #3577's table scan).
743
+ */
744
+ function scanMilestonePhaseIds(window, convention) {
745
+ return scanMilestonePhaseIdSets(window, convention).ids;
563
746
  }
564
747
  /**
565
748
  * #3262 (write-time milestone-scope guard): does this free-text value contain
@@ -590,8 +773,26 @@ function scanMilestonePhaseIds(window) {
590
773
  * Fence-aware via `tokenizeHeadings`: a FENCED example of a milestone heading
591
774
  * inside a field value does not terminate the real window, so it must not be
592
775
  * a violation either โ€” the parser and this guard must agree on fences.
776
+ *
777
+ * #612: `convention` is the resolved `phase_id_convention`. This predicate is
778
+ * defined as a MIRROR of the parser's terminator vocabulary, and on this
779
+ * branch that vocabulary is convention-SELECTED: `computeBracketSectionEnd`
780
+ * adds `isBracketMilestoneBoundary` as a terminator arm, so the ADR-canonical
781
+ * `## [GSD.09] Hidden` โ€” no version token, no status emoji, not the word
782
+ * "Milestone" โ€” terminates the window on an opted-in bracket repo while
783
+ * matching NONE of the signals above. Left unmirrored, the guard accepts
784
+ * exactly the description that narrows the window on the one convention this
785
+ * branch teaches the parser to read, which is the failure the guard exists to
786
+ * prevent. A non-bracket (or unresolvable) value takes the pre-existing path
787
+ * byte-identically.
788
+ *
789
+ * REQUIRED, not optional โ€” the same tripwire `scanMilestonePhaseIds` carries,
790
+ * and for the sharper reason: a blind call here fails OPEN (the guard quietly
791
+ * ACCEPTS a window-narrowing description) rather than merely miscounting, so a
792
+ * future call site must fail to COMPILE. The census today is one caller,
793
+ * `assertDescriptionPreservesMilestoneScope`, which pays nothing for it.
593
794
  */
594
- function findMilestoneScopeHeadingLines(text) {
795
+ function findMilestoneScopeHeadingLines(text, convention) {
595
796
  const out = [];
596
797
  for (const h of (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(text)) {
597
798
  if (h.level > 3)
@@ -600,6 +801,22 @@ function findMilestoneScopeHeadingLines(text) {
600
801
  continue;
601
802
  if (MILESTONE_HEADING_SIGNAL_PATTERN.test(h.text) || /๐Ÿ”„/.test(h.text)) {
602
803
  out.push(h.text.trim());
804
+ continue;
805
+ }
806
+ // #612: the bracket arm, routed through the SAME single-owner
807
+ // phase-vs-milestone discriminator `computeBracketSectionEnd` consults โ€”
808
+ // never a second bracket-heading grammar here.
809
+ //
810
+ // `selectedBracketId` is deliberately `null`, so the same-milestone
811
+ // CONTINUATION exemption never fires and a value naming the ACTIVE
812
+ // milestone (`## [GSD.02] Foundation (Phase Details)`) is flagged even
813
+ // though the real parser would treat it as a continuation. That is the
814
+ // third instance of this function's stated conservatism and rests on the
815
+ // same argument as the other two: WHICH milestone is active is a property
816
+ // of the document at write time, not of the text being validated, and
817
+ // over-rejecting is one-directional (reject more, never less).
818
+ if (convention === 'bracket' && isBracketMilestoneBoundary(h.text, h.level, null)) {
819
+ out.push(h.text.trim());
603
820
  }
604
821
  }
605
822
  return out;
@@ -695,7 +912,42 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
695
912
  }
696
913
  const documentHasPhaseEntries = hasPhaseEntries(stripShippedMilestones(content), phaseIdConvention);
697
914
  const summaryPattern = new RegExp(`<summary[^>]*>([^<]*${(0, pattern_cjs_1.escapeRegex)(version)}[^<]*)<\\/summary>`, 'i');
698
- const headingMatches = locateMilestoneHeadings(content, version);
915
+ // #3184 keeps the version-heading locator single-owned. #612 adds a gated
916
+ // bracket candidate source only when that owner finds no version-bearing
917
+ // heading; it never replaces or re-derives the legacy lookup.
918
+ let headingMatches = locateMilestoneHeadings(content, version);
919
+ // #2761 B3: resolve the boundary convention independently of whether the
920
+ // version-heading owner already selected a match. Preserve an explicitly
921
+ // threaded convention; otherwise resolve from the same workstream whose
922
+ // STATE/ROADMAP this call reads (#2761 B1).
923
+ let bracketScopeConvention = phaseIdConvention ?? null;
924
+ if (phaseIdConvention === undefined) {
925
+ try {
926
+ bracketScopeConvention = resolvePhaseIdConvention(cwd, ws);
927
+ }
928
+ catch { /* unresolvable convention โ†’ preserve the legacy fallback */ }
929
+ }
930
+ if (headingMatches.length === 0 && bracketScopeConvention === 'bracket') {
931
+ const vMatch = version.match(/^v(\d+)/i);
932
+ const milestoneInt = vMatch ? parseInt(vMatch[1], 10) : NaN;
933
+ if (Number.isSafeInteger(milestoneInt)) {
934
+ // #2761 M3: the phase-id owner supplies both the bracket intro grammar
935
+ // and canonical pad2 spelling; `[CODE.2]` therefore cannot bound a
936
+ // section whose phase headings the bracket grammar rejects.
937
+ // #612 round-5: HeadingToken.offset is the line start, so require the
938
+ // `#` there to preserve the old line-start anchor's indentation parity.
939
+ // For every survivor, rebuilding [fullLine, fullLine] plus `.index`
940
+ // retains the match shape and first-match order expected downstream.
941
+ const bracketMilestoneHeadingRe = new RegExp(`^${bracketMilestoneIntroSrcFor(milestoneInt)}`, 'i');
942
+ headingMatches = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content)
943
+ .filter((h) => h.level <= 3 && content[h.offset] === '#' && bracketMilestoneHeadingRe.test(h.text))
944
+ .map((h) => {
945
+ const lineEnd = content.indexOf('\n', h.offset);
946
+ const fullLine = content.slice(h.offset, lineEnd === -1 ? content.length : lineEnd);
947
+ return Object.assign([fullLine, fullLine], { index: h.offset });
948
+ });
949
+ }
950
+ }
699
951
  if (headingMatches.length === 0) {
700
952
  const summaryMatch = content.match(summaryPattern);
701
953
  if (summaryMatch) {
@@ -748,16 +1000,174 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
748
1000
  // #3184: selection collapses to the sole owner; `allMatches` is still needed
749
1001
  // below (offsets, detailsMatch search), so only the selection itself routes
750
1002
  // through `selectMilestoneHeading` rather than the whole block.
751
- const selected = selectMilestoneHeading(content, version);
1003
+ // Preserve the canonical legacy selector, then fall back to the gated
1004
+ // bracket candidates above when a name-only bracket milestone carries no
1005
+ // version token for that selector to find.
1006
+ const selected = selectMilestoneHeading(content, version)
1007
+ ?? allMatches.find((m) => !isClosed(m[1]))
1008
+ ?? firstMatch;
752
1009
  const sectionStart = selected.index;
753
- const sectionEnd = computeMilestoneSectionEnd(content, selected[0], sectionStart);
1010
+ // #2761 B1: a bracket heading bearing the selected milestone's own id is
1011
+ // a continuation, not a boundary. Derive the selected id once and compose
1012
+ // the shared discriminator with upstream's centralized section-end walk.
1013
+ const bracketBoundaryActive = bracketScopeConvention === 'bracket';
1014
+ const selectedBracketMatch = bracketBoundaryActive
1015
+ ? selected[0].match(new RegExp(`^#{1,3}\\s+\\[(${BRACKET_ID_SRC})\\]`, 'i'))
1016
+ : null;
1017
+ const selectedBracketId = selectedBracketMatch ? foldBracketId(selectedBracketMatch[1]) : null;
1018
+ // #2761 B3: tokenize once for both the centralized section-end owner and
1019
+ // the bracket preamble scan below. This keeps both boundary decisions
1020
+ // fence-aware without restoring the local section walker retired by #3184.
1021
+ const currentMilestoneHeadings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
1022
+ const bracketBoundary = bracketBoundaryActive
1023
+ ? (heading) => isBracketMilestoneBoundary(heading.text, heading.level, selectedBracketId)
1024
+ : undefined;
1025
+ const sectionEnd = computeMilestoneSectionEnd(content, selected[0], sectionStart, bracketBoundary, currentMilestoneHeadings);
754
1026
  const anyMilestonePattern = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|โœ…|๐Ÿ“‹|๐Ÿšง)/im;
755
- const firstMilestoneMatch = content.match(anyMilestonePattern);
756
- const preambleCutoff = firstMilestoneMatch
757
- ? firstMilestoneMatch.index
1027
+ let earliestMilestoneIndex;
1028
+ if (!bracketBoundaryActive) {
1029
+ // #2761 B3: the LEGACY (non-bracket-shaped) path stays a raw
1030
+ // `content.match` โ€” byte-identical to before this fix, including its
1031
+ // fence-blindness. That hazard is real (a fenced `## Milestone v9.0`
1032
+ // example in the preamble reads identically wrong at base, round-1, and
1033
+ // HEAD โ€” repro12's LEGACY control) but is PRE-EXISTING and shared with
1034
+ // the ORIGINAL (pre-#612) code path, not introduced by this branch โ€”
1035
+ // fixing it is explicitly out of scope (round-2 review's own
1036
+ // minimal-fix note).
1037
+ const versionMilestoneMatch = content.match(anyMilestonePattern);
1038
+ earliestMilestoneIndex = versionMilestoneMatch ? versionMilestoneMatch.index : null;
1039
+ }
1040
+ else {
1041
+ // #2761 Major 1 (round-3 fix): on the BRACKET branch, derive the
1042
+ // version/emoji half of "earliest milestone-shaped heading" from the
1043
+ // SAME fence-aware `currentMilestoneHeadings` token list too, instead of
1044
+ // the raw `content.match` above. Round-2 (ff6bf0a8) fixed the BRACKET
1045
+ // half's fence-blindness but left THIS half a raw regex even on this
1046
+ // branch: a fenced VERSION-BEARING example heading in a bracket repo's
1047
+ // preamble (`` ```markdown\n## Milestone v9.0: Example\n``` ``, ADR-612's
1048
+ // own docs illustrate the LEGACY heading shape exactly this way) was
1049
+ // still textually the earliest match for the raw regex, winning the old
1050
+ // min() and un-suppressing a wrong persisted 75% that base correctly
1051
+ // suppressed (rv-attack3c fixture C1) โ€” B3 fixed only the half of the
1052
+ // asymmetry it introduced, not this pre-existing half once it also
1053
+ // started reaching the bracket branch. The predicate below (`h.text`
1054
+ // against the same `/^Phase\s+\S/i` / `/v\d+\.\d+|โœ…|๐Ÿ“‹|๐Ÿšง/i` pair
1055
+ // `computeSectionEnd` already uses) never sees a fenced heading at all,
1056
+ // because tokenizeHeadings never produces a token for one.
1057
+ //
1058
+ // Hardened post-round-3: the raw `content.match(anyMilestonePattern)`
1059
+ // this replaced was anchored `^#{1,3}\s+โ€ฆ` โ€” a level cap the token loop
1060
+ // dropped entirely. A level-4+ version-bearing heading in the preamble
1061
+ // (`#### v2.0 notes`) would win this scan where the raw pattern on the
1062
+ // legacy path ignores it outright, cutting the preamble at a heading
1063
+ // neither the selector nor `isMilestoneBounded` would ever treat as a
1064
+ // milestone marker. Mirrors the depth-sanity cap
1065
+ // `isBracketMilestoneBoundary` already applies to the bracket half.
1066
+ earliestMilestoneIndex = null;
1067
+ for (const h of currentMilestoneHeadings) {
1068
+ if (h.level > 3)
1069
+ continue;
1070
+ if (/^Phase\s+\S/i.test(h.text))
1071
+ continue;
1072
+ if (/v\d+\.\d+|โœ…|๐Ÿ“‹|๐Ÿšง/i.test(h.text)) {
1073
+ earliestMilestoneIndex = h.offset;
1074
+ break;
1075
+ }
1076
+ }
1077
+ }
1078
+ if (bracketBoundaryActive) {
1079
+ // #2761 B3: scans the SAME fence-aware `currentMilestoneHeadings` token
1080
+ // list computeSectionEnd consumes, instead of a raw `content.matchAll` โ€”
1081
+ // closes the asymmetry between the two halves of one boundary semantic.
1082
+ // Before this fix, a fenced markdown example containing a bracket
1083
+ // heading (ADR-612's own docs do exactly this) was textually the
1084
+ // earliest `#{1,3} [CODE.MM]` match, so `preambleCutoff` landed INSIDE
1085
+ // the fence, `preamble` ended with an unclosed opener, and
1086
+ // `getMilestonePhaseFilter`'s tokenizeHeadings(scope) call then saw an
1087
+ // unbalanced fence and swallowed every real heading, degrading to a
1088
+ // pass-all filter (repro11). tokenizeHeadings already strips fenced
1089
+ // lines before a heading candidate is ever produced, so a heading INSIDE
1090
+ // a fence is never a candidate here at all.
1091
+ //
1092
+ // `h.text` is ALREADY hash-stripped and trimmed (HeadingToken's own
1093
+ // shape) โ€” isBracketMilestoneBoundary is built to consume exactly that,
1094
+ // so no `^#{1,3}\s+` re-derivation is needed (that spelling would not
1095
+ // match `h.text` โ€” it still carries the hashes in a raw regex match).
1096
+ for (let i = 0; i < currentMilestoneHeadings.length; i++) {
1097
+ const h = currentMilestoneHeadings[i];
1098
+ let isBoundary;
1099
+ if (h.offset === sectionStart) {
1100
+ // #2761 B1 (round-3 fix): the SELECTED heading's own occurrence is
1101
+ // ALWAYS a correct earliest answer to "where does milestone content
1102
+ // begin" โ€” bypass BOTH the same-milestone check inside
1103
+ // isBracketMilestoneBoundary (which would otherwise reject this
1104
+ // heading against ITSELF, since `selectedBracketId` is its own id)
1105
+ // and the same-id-child rule below (which would reject a genuinely
1106
+ // childless CURRENT milestone, e.g. one with no phases populated
1107
+ // yet). Without this, a same-milestone heading EARLIER than the
1108
+ // selected one (a version-less checklist/overview split, or the
1109
+ // version-bearing heading landing on the LATER half of such a
1110
+ // split โ€” Blocker 1 round-3 cases A/B) would incorrectly win via the
1111
+ // OLD `null`-everywhere behaviour, or (with the same-milestone
1112
+ // check alone reinstated) the selected heading would incorrectly
1113
+ // reject itself and fall through to a stray, unrelated LATER
1114
+ // heading.
1115
+ isBoundary = true;
1116
+ }
1117
+ else if (isBracketMilestoneBoundary(h.text, h.level, selectedBracketId)) {
1118
+ // #2761 B1 (round-3 fix, Blocker 1 case D): bracket-shaped, not
1119
+ // phase-tail-shaped, and not the SAME id as the selected milestone
1120
+ // is not enough โ€” an unrelated bracket-shaped PROSE heading
1121
+ // (`## [ADR.612] Heading convention used by this roadmap`) reads as
1122
+ // a genuine boundary by those rules alone. Require its own next
1123
+ // DEEPER heading to carry ITS bracket id too โ€” the property every
1124
+ // genuine sibling MILESTONE has (its own phase children) and no
1125
+ // unrelated prose heading does.
1126
+ //
1127
+ // #2761 round-4 Minor 1 (docstring correction โ€” no code change): a
1128
+ // heading rejected here (no same-id PHASE child โ€” see
1129
+ // bracketHeadingHasMatchingChild's own comment) leaves its WHOLE
1130
+ // SUBTREE in the preamble, not merely its own inert heading text.
1131
+ // That subtree can still contain a DIFFERENT-id bracket PHASE
1132
+ // heading, which DOES form a qualified key and CAN admit a foreign
1133
+ // directory (F7: `## [GSD.01] Setup` / `### [GSD.07] 01: Foreign` โ€”
1134
+ // no same-id child, so `[GSD.01] Setup` is not a boundary, and
1135
+ // `GSD.07-01-foreign`'s directory is admitted into the CURRENT
1136
+ // milestone's filter, reading 3/2/67%). This is NOT a regression โ€”
1137
+ // base, round-1 and HEAD all read 3/2/67% on F7 (base via its own
1138
+ // pass-all degrade) โ€” and it remains the declared OVER-inclusive,
1139
+ // never under-inclusive, safe direction; it is a narrower and more
1140
+ // honest claim than "contributes nothing to any phase count", which
1141
+ // is true of the candidate's OWN text but not of what its subtree
1142
+ // can carry.
1143
+ isBoundary = bracketHeadingHasMatchingChild(currentMilestoneHeadings, i);
1144
+ }
1145
+ else {
1146
+ isBoundary = false;
1147
+ }
1148
+ if (!isBoundary)
1149
+ continue;
1150
+ if (earliestMilestoneIndex === null || h.offset < earliestMilestoneIndex) {
1151
+ earliestMilestoneIndex = h.offset;
1152
+ }
1153
+ break;
1154
+ }
1155
+ }
1156
+ const preambleCutoff = earliestMilestoneIndex !== null
1157
+ ? earliestMilestoneIndex
758
1158
  : firstMatch.index;
759
1159
  const beforeMilestones = content.slice(0, preambleCutoff);
760
- const currentSection = content.slice(sectionStart, sectionEnd);
1160
+ // #3982: newest-first roadmaps collapse their archives into <details>
1161
+ // blocks BELOW the active milestone, whose titles live in <summary> tags โ€”
1162
+ // not headings โ€” so no milestone-shaped heading bounds the section walk and
1163
+ // the raw slice runs to end-of-document. Strip CLOSED milestone details
1164
+ // blocks here so the window never feeds archived phases to the
1165
+ // lowest-outstanding scan. Deliberately NOT a blanket strip: the active
1166
+ // milestone may legitimately hold its own collapsed <details> (#1341), and
1167
+ // removing that would trade this bug for the phase_count: 0 class (#557).
1168
+ // Gated on the same isClosedMilestoneHeading the file already applies to
1169
+ // <summary> lines, per the issue's prescribed narrower fix.
1170
+ const currentSection = stripClosedMilestoneDetails(content.slice(sectionStart, sectionEnd));
761
1171
  // Multi-milestone roadmaps split each added milestone across two version-bearing
762
1172
  // headings: a `## Phases` checklist subsection (early) and a dedicated
763
1173
  // `## Milestone โ€ฆ (Phase Details)` section (late) holding the `### Phase N:`
@@ -779,7 +1189,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
779
1189
  (m.index ?? 0) >= sectionEnd);
780
1190
  if (detailsMatch) {
781
1191
  const detailsStart = detailsMatch.index ?? 0;
782
- detailsSection = content.slice(detailsStart, computeMilestoneSectionEnd(content, detailsMatch[0], detailsStart));
1192
+ detailsSection = content.slice(detailsStart, computeMilestoneSectionEnd(content, detailsMatch[0], detailsStart, bracketBoundary, currentMilestoneHeadings));
783
1193
  }
784
1194
  // #2947: the preamble strip removes `### Phase N:` detail headings from the
785
1195
  // pre-milestone region so they don't duplicate the ones inside the selected
@@ -1074,6 +1484,23 @@ function findRoadmapProgressTable(roadmapContent) {
1074
1484
  function stripLeadingDelimiter(s) {
1075
1485
  return s.replace(/^[\sโ€”โ€“:-]+/, '').trim();
1076
1486
  }
1487
+ /**
1488
+ * #4134/#4433 (ยง7.2 rule 6 floor, applied symmetrically): a captured "name"
1489
+ * with no letter or digit anywhere is heading/bullet STRUCTURE, not a curated
1490
+ * name โ€” e.g. the trailing `)` a name-then-version heading leaves after its
1491
+ * version token, or a malformed ๐Ÿšง-bullet whose only content past the version
1492
+ * is punctuation (`---`, `***`, a lone `:`). #4134 fixed this for
1493
+ * `extractMilestoneHeadingName`'s heading path only; #4433 found the sibling
1494
+ * ๐Ÿšง-bullet capture (`getMilestoneInfo`'s `listMatch`) and the no-STATE.md
1495
+ * fallback (`inProgressMatch`) both skipped straight to a bare truthiness
1496
+ * check, so a punctuation-only bullet name passed through as a real one. This
1497
+ * is now the SOLE name-validity predicate โ€” every capture site in this file
1498
+ * calls it instead of re-deriving the character class. A name that merely
1499
+ * CONTAINS punctuation is unaffected; digits alone qualify.
1500
+ */
1501
+ function hasNameableContent(s) {
1502
+ return /[\p{L}\p{N}]/u.test(s);
1503
+ }
1077
1504
  /**
1078
1505
  * #3216 (ADR-3180 ยง7.2's "Name extraction โ€” pinned rule"): the sole "milestone
1079
1506
  * heading text โ†’ version + curated name" rule. Strips everything through the
@@ -1086,6 +1513,13 @@ function stripLeadingDelimiter(s) {
1086
1513
  * one implementation. Returns `null` when `headingText` carries no version
1087
1514
  * token at all (e.g. a non-milestone heading reached this by mistake).
1088
1515
  *
1516
+ * #4134 (ยง7.2 rule 6 floor): the rule's direction assumes version-then-name.
1517
+ * A name-then-version heading (`# Roadmap: Project โ€” Name (v1.13)`) leaves a
1518
+ * punctuation fragment (`)`) after the token; a remainder with no letter or
1519
+ * digit anywhere is heading structure, not a curated name, and is refused as
1520
+ * `name: null` so callers report the honest rule-6 answer instead of
1521
+ * fabricating garbage. Names that merely CONTAIN punctuation are unaffected.
1522
+ *
1089
1523
  * @param expectedVersion - When the caller already knows the exact version it
1090
1524
  * is looking for (the STATE-anchored `getMilestoneInfo` path, which located
1091
1525
  * this heading via `selectMilestoneHeading(roadmap, stateVersion)`), pass it
@@ -1122,7 +1556,18 @@ function extractMilestoneHeadingName(headingText, expectedVersion) {
1122
1556
  // whitespace โ€” the marker is already carried structurally by `closed`, so
1123
1557
  // duplicating it inside `name` (e.g. "Old โœ…") is redundant and wrong. Only
1124
1558
  // these three markers, only at the end; a marker inside a name is untouched.
1125
- const name = stripLeadingDelimiter(afterVersion).replace(/\s*(?:[โœ…๐Ÿ“‹๐Ÿšง]\s*)+$/, '') || null;
1559
+ const candidate = stripLeadingDelimiter(afterVersion).replace(/\s*(?:[โœ…๐Ÿ“‹๐Ÿšง]\s*)+$/, '') || null;
1560
+ // #4134 (ยง7.2 rule 6 floor): a "name" with no letter or digit anywhere is
1561
+ // heading structure, not a curated name. The pinned rule takes everything
1562
+ // AFTER the version token, so a name-then-version heading (`# Roadmap:
1563
+ // Project โ€” Name (v1.13)` โ€” the shape a first-ever ROADMAP.md drifts into)
1564
+ // leaves exactly `)` there, which used to be returned as a COMPLETE-scope
1565
+ // name and propagated into init.* output and STATE.md. Refuse it: callers
1566
+ // already report the honest rule-6 answer (version kept, `name: null`,
1567
+ // scope TRUNCATED) for an unresolvable name. A name that merely CONTAINS
1568
+ // punctuation is untouched โ€” `(` is an ordinary name character (#3171) โ€”
1569
+ // and digits alone qualify (`## v4.0 โ€” 42` is the name `42`).
1570
+ const name = candidate !== null && hasNameableContent(candidate) ? candidate : null;
1126
1571
  return { version, name };
1127
1572
  }
1128
1573
  /**
@@ -1207,7 +1652,7 @@ function getMilestoneInfo(cwd) {
1207
1652
  const listMatch = roadmap.match(new RegExp(`๐Ÿšง\\s*\\*?\\*?${escapedVer}\\*?\\*?\\s+([^*\\n]+)`, 'i'));
1208
1653
  if (listMatch) {
1209
1654
  const name = stripLeadingDelimiter(listMatch[1]);
1210
- if (name)
1655
+ if (name && hasNameableContent(name))
1211
1656
  return scoped({ version: stateVersion, name }, SCOPE.COMPLETE);
1212
1657
  }
1213
1658
  // #3216: heading selection routes through the shared owner
@@ -1241,7 +1686,10 @@ function getMilestoneInfo(cwd) {
1241
1686
  // (unchanged from the pre-#3216 fallback).
1242
1687
  const inProgressMatch = roadmap.match(/๐Ÿšง\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/);
1243
1688
  if (inProgressMatch) {
1244
- return scoped({ version: 'v' + inProgressMatch[1], name: inProgressMatch[2].trim() }, SCOPE.COMPLETE);
1689
+ const inProgressName = inProgressMatch[2].trim();
1690
+ if (hasNameableContent(inProgressName)) {
1691
+ return scoped({ version: 'v' + inProgressMatch[1], name: inProgressName }, SCOPE.COMPLETE);
1692
+ }
1245
1693
  }
1246
1694
  // #3216: enumerate every OPEN (non-shipped) milestone heading via the
1247
1695
  // shared owner and take the first in document order โ€” deletes the
@@ -1305,6 +1753,23 @@ function getMilestoneInfo(cwd) {
1305
1753
  */
1306
1754
  function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
1307
1755
  const milestonePhaseNums = new Set();
1756
+ // #612: the milestone-QUALIFIED form (`{CODE}.{MM}-{PP}`) of each in-scope
1757
+ // bracket heading, kept in its OWN set โ€” deliberately not in
1758
+ // milestonePhaseNums. A qualified id ALWAYS contains a hyphen, so putting one
1759
+ // there would flip `roadmapUsesHyphenedIds` below on EVERY bracket repo, which
1760
+ // swaps `numericRe` to the continuation-segment variant and silently moves the
1761
+ // LEGACY dir path. Separate set; `phaseCount` is unchanged.
1762
+ //
1763
+ // Stated exactly, because the narrower claim is the true one: this keeps
1764
+ // QUALIFIED IDS out of that flag's input, not hyphens in general. A heading
1765
+ // whose TOKEN carries its own hyphen (`### [GSD.02] Phase 02-01:`) still flips
1766
+ // it through milestonePhaseNums โ€” as it also does at base, which matches that
1767
+ // spelling through the un-widened intro. See the token-hyphen guard below.
1768
+ const milestoneQualifiedIds = new Set();
1769
+ // Hoisted out of the try so the DIR side can select the same grammar the
1770
+ // HEADING side selected. The two halves of this one filter reading different
1771
+ // conventions is exactly the defect the bracket branch below closes.
1772
+ let headingConvention;
1308
1773
  let missingExplicitVersion = false;
1309
1774
  let versionScoped = false;
1310
1775
  let versionSectionFound = false;
@@ -1373,12 +1838,21 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
1373
1838
  documentHasPhaseEntries,
1374
1839
  });
1375
1840
  }
1376
- // #3262: the set-building scan now lives in its own named owner
1377
- // (`scanMilestonePhaseIds`) so the new `roadmap milestone-scope` probe
1378
- // reads the SAME derivation this filter does โ€” never a second copy.
1379
- for (const id of scanMilestonePhaseIds(roadmap)) {
1841
+ // Resolve once, then thread the same answer through the shared scan and
1842
+ // directory matcher. Resolve an omitted value from this call's `ws`;
1843
+ // explicit null still means "resolved and non-bracket." A split convention
1844
+ // would widen the ROADMAP side while leaving bracket directories unmatched.
1845
+ headingConvention = phaseIdConvention === undefined
1846
+ ? resolvePhaseIdConvention(cwd, ws)
1847
+ : phaseIdConvention;
1848
+ // #3262 remains the single owner of the phase-set scan. #612 extends its
1849
+ // return with qualified bracket ids rather than restoring the superseded
1850
+ // inline duplicate this commit was originally written against.
1851
+ const scanned = scanMilestonePhaseIdSets(roadmap, headingConvention);
1852
+ for (const id of scanned.ids)
1380
1853
  milestonePhaseNums.add(id);
1381
- }
1854
+ for (const qualified of scanned.qualifiedIds)
1855
+ milestoneQualifiedIds.add(qualified);
1382
1856
  }
1383
1857
  catch {
1384
1858
  /* best-effort (#2245 audit): the real throw source is platformReadSync
@@ -1429,6 +1903,28 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
1429
1903
  // phase-id-owner: the [A-Za-z] letter class does real case handling here โ€” this regex carries NO /i flag; kept literal, not source-byte-equal to the canonical PHASE_NUMBER_TOKEN_SOURCE.
1430
1904
  : /^0*(\d+[A-Za-z]?(?:\.\d+)*)/;
1431
1905
  function isDirInMilestone(dirName) {
1906
+ // #612: the DIR side of this filter, selected by the same convention the
1907
+ // heading side selected. Without it the heading scan's new bracket reach was
1908
+ // half a fix: `milestonePhaseNums` became non-empty, so the pass-all degrade
1909
+ // stopped firing, but no bracket directory could satisfy the three legacy
1910
+ // checks below (numericRe fails on `GSD.02-05-five`, the custom-id match
1911
+ // captures the project code `GSD`, and stripProjectCodePrefix does not strip
1912
+ // a dotted prefix) โ€” so EVERY bracket directory was rejected and
1913
+ // completed_phases / total_plans / completed_plans / percent collapsed to 0
1914
+ // while `state sync` went on writing a percent off the unfiltered disk.
1915
+ //
1916
+ // Matching is delegated to phaseTokenMatches against the milestone-QUALIFIED
1917
+ // id, not the bare token: READING-B puts the milestone in the bracket, so
1918
+ // `GSD.01-01-old-one` and `GSD.02-01-one` share the token `01` and only the
1919
+ // qualified key separates them. That is the scoping this filter exists to do.
1920
+ // ADDITIVE: on a miss we fall through to the three legacy checks, so a
1921
+ // bracket repo carrying legacy-shaped directories reads exactly as before.
1922
+ if (headingConvention === 'bracket') {
1923
+ for (const qualified of milestoneQualifiedIds) {
1924
+ if (phaseTokenMatches(dirName, qualified, 'bracket'))
1925
+ return true;
1926
+ }
1927
+ }
1432
1928
  const m2 = dirName.match(numericRe);
1433
1929
  if (m2 && normalized.has(normalizePhaseIdSegments(m2[1]).toLowerCase()))
1434
1930
  return true;
@@ -1496,6 +1992,24 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
1496
1992
  * `computeMilestoneSectionEnd` owner (#3184), so there is no separate copy to
1497
1993
  * keep in sync. Returns null when there is no versioned active milestone;
1498
1994
  * callers then fall back to whole-content mutation (the prior behaviour).
1995
+ *
1996
+ * #2761 (round-2 review, Minor 3 โ€” latent, currently harmless): this function
1997
+ * consumes #3184's shared owners, but it does not pass the bracket-specific
1998
+ * B1/B2 boundary predicate and still has no bracket-fallback SELECTION branch:
1999
+ * it returns null when the version-string owner finds nothing, unlike
2000
+ * `extractCurrentMilestoneScoped`. The read owner can therefore scope a
2001
+ * bracket ROADMAP this write-range consumer still calls unscoped. Probed and
2002
+ * confirmed harmless TODAY: this
2003
+ * function's single consumer (`mutateMilestonePhase`, src/phase.cts) falls
2004
+ * back to whole-content mutation when it returns null, and every mutation
2005
+ * inside that caller is still `Phase`-labelled-only (not bracket-widened) per
2006
+ * the changeset's own "READ-path opt-in until the migrator and write path
2007
+ * land" โ€” so a bracket ROADMAP's checkbox/heading patterns never match inside
2008
+ * that fallback and nothing is mutated cross-milestone. The moment the write
2009
+ * path is widened (PR-3+), this divergence becomes live: the whole-content
2010
+ * fallback would become a cross-milestone writer, which is exactly what this
2011
+ * note exists to prevent. Bracket-widen this function in lockstep with the
2012
+ * write path landing, not before.
1499
2013
  */
1500
2014
  function currentMilestoneRawRanges(content, cwd) {
1501
2015
  if (!cwd)