@opengsd/gsd-core 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (544) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +12 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-doc-synthesizer.md +2 -4
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +22 -14
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +5 -3
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +23 -2
  15. package/agents/gsd-plan-checker.md +50 -53
  16. package/agents/gsd-planner.md +50 -50
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +63 -4
  21. package/agents/gsd-ui-researcher.md +41 -3
  22. package/agents/gsd-user-profiler.md +3 -0
  23. package/agents/gsd-verifier.md +13 -4
  24. package/bin/install.js +1448 -1103
  25. package/commands/gsd/code-review.md +1 -1
  26. package/commands/gsd/discuss-phase.md +1 -1
  27. package/commands/gsd/execute-phase.md +1 -1
  28. package/commands/gsd/import.md +1 -1
  29. package/commands/gsd/map-codebase.md +1 -1
  30. package/commands/gsd/mempalace-capture.md +1 -1
  31. package/commands/gsd/mempalace-recall.md +1 -1
  32. package/commands/gsd/new-milestone.md +1 -1
  33. package/commands/gsd/quick.md +9 -5
  34. package/commands/gsd/review-backlog.md +2 -1
  35. package/commands/gsd/verify-work.md +1 -1
  36. package/gsd-core/bin/gsd-tools.cjs +1035 -138
  37. package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
  38. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  39. package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
  40. package/gsd-core/bin/lib/api-coverage.cjs +33 -14
  41. package/gsd-core/bin/lib/artifacts.cjs +5 -0
  42. package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
  43. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  44. package/gsd-core/bin/lib/audit.cjs +1026 -268
  45. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  46. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  47. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  48. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  49. package/gsd-core/bin/lib/capability-registry.cjs +845 -130
  50. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  51. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  52. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  53. package/gsd-core/bin/lib/capability-validator.cjs +700 -40
  54. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  55. package/gsd-core/bin/lib/check-command-router.cjs +216 -42
  56. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  57. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  58. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  59. package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
  60. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  61. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  62. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  63. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  64. package/gsd-core/bin/lib/commands.cjs +1172 -108
  65. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  66. package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
  67. package/gsd-core/bin/lib/config-loader.cjs +187 -23
  68. package/gsd-core/bin/lib/config.cjs +102 -3
  69. package/gsd-core/bin/lib/configuration.cjs +129 -37
  70. package/gsd-core/bin/lib/core-utils.cjs +208 -33
  71. package/gsd-core/bin/lib/decisions.cjs +23 -0
  72. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  73. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  74. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  75. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  76. package/gsd-core/bin/lib/frontmatter.cjs +899 -229
  77. package/gsd-core/bin/lib/gap-checker.cjs +95 -10
  78. package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
  79. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  80. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  81. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  82. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
  83. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  84. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  85. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  86. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
  87. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  88. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  89. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
  90. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  91. package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
  92. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  93. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  94. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  95. package/gsd-core/bin/lib/init.cjs +439 -168
  96. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  97. package/gsd-core/bin/lib/install-engine.cjs +811 -259
  98. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  99. package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
  100. package/gsd-core/bin/lib/install-profiles.cjs +212 -61
  101. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  102. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  103. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  104. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  105. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  106. package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
  107. package/gsd-core/bin/lib/intel.cjs +101 -26
  108. package/gsd-core/bin/lib/io.cjs +170 -15
  109. package/gsd-core/bin/lib/learnings.cjs +85 -14
  110. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  111. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  112. package/gsd-core/bin/lib/markdown-table.cjs +183 -22
  113. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  114. package/gsd-core/bin/lib/milestone.cjs +842 -73
  115. package/gsd-core/bin/lib/model-catalog.cjs +232 -16
  116. package/gsd-core/bin/lib/model-resolver.cjs +193 -68
  117. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  118. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  119. package/gsd-core/bin/lib/pattern.cjs +122 -0
  120. package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
  121. package/gsd-core/bin/lib/phase-id.cjs +514 -40
  122. package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
  123. package/gsd-core/bin/lib/phase-locator.cjs +262 -34
  124. package/gsd-core/bin/lib/phase.cjs +1038 -214
  125. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  126. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  127. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  128. package/gsd-core/bin/lib/plan-scan.cjs +98 -3
  129. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  130. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  131. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  132. package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
  133. package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
  134. package/gsd-core/bin/lib/probe-core.cjs +5 -2
  135. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  136. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  137. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  138. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  139. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
  140. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  141. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
  142. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  143. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  144. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  145. package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
  146. package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
  147. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  148. package/gsd-core/bin/lib/roadmap.cjs +442 -96
  149. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
  150. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  151. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
  152. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  153. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  154. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
  155. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  156. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  157. package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
  158. package/gsd-core/bin/lib/security.cjs +104 -5
  159. package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
  160. package/gsd-core/bin/lib/smart-entry.cjs +133 -23
  161. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  162. package/gsd-core/bin/lib/state-command-router.cjs +52 -19
  163. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  164. package/gsd-core/bin/lib/state-document.cjs +338 -8
  165. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  166. package/gsd-core/bin/lib/state-transition.cjs +846 -176
  167. package/gsd-core/bin/lib/state.cjs +2589 -369
  168. package/gsd-core/bin/lib/surface.cjs +33 -11
  169. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  170. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  171. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  172. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  173. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  174. package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
  175. package/gsd-core/bin/lib/uat.cjs +1761 -167
  176. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  177. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  178. package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
  179. package/gsd-core/bin/lib/unusable-input.cjs +37 -0
  180. package/gsd-core/bin/lib/update-context.cjs +8 -2
  181. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  182. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  183. package/gsd-core/bin/lib/validate.cjs +20 -6
  184. package/gsd-core/bin/lib/vendor/README.md +75 -0
  185. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  186. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  187. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  188. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  189. package/gsd-core/bin/lib/verification.cjs +272 -9
  190. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  191. package/gsd-core/bin/lib/verify.cjs +453 -918
  192. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  193. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  194. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  195. package/gsd-core/bin/lib/workstream.cjs +2 -2
  196. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  197. package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
  198. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
  199. package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
  200. package/gsd-core/bin/shared/exit-codes.json +8 -0
  201. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  202. package/gsd-core/bin/shared/model-catalog.json +8 -1
  203. package/gsd-core/references/agent-contracts.md +44 -26
  204. package/gsd-core/references/api-coverage.md +24 -2
  205. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  206. package/gsd-core/references/checkpoints.md +39 -21
  207. package/gsd-core/references/context-budget.md +1 -1
  208. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  209. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  210. package/gsd-core/references/doc-conflict-engine.md +1 -1
  211. package/gsd-core/references/edge-probe.md +8 -0
  212. package/gsd-core/references/execute-mvp-tdd.md +4 -6
  213. package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
  214. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  215. package/gsd-core/references/execute-phase-response-language.md +1 -1
  216. package/gsd-core/references/execute-phase-wave-guard.md +17 -11
  217. package/gsd-core/references/failing-direction.md +78 -0
  218. package/gsd-core/references/gate-prompts.md +1 -1
  219. package/gsd-core/references/git-integration.md +5 -5
  220. package/gsd-core/references/git-planning-commit.md +5 -4
  221. package/gsd-core/references/gsd-run-resolver.md +1 -1
  222. package/gsd-core/references/loop-hook-dispatch.md +61 -2
  223. package/gsd-core/references/model-profiles.md +12 -4
  224. package/gsd-core/references/mvp-concepts.md +9 -9
  225. package/gsd-core/references/nyquist-compliance.md +74 -0
  226. package/gsd-core/references/offer-next.md +3 -5
  227. package/gsd-core/references/phase-argument-parsing.md +3 -3
  228. package/gsd-core/references/planner-failing-direction.md +53 -0
  229. package/gsd-core/references/planner-guidance.md +3 -9
  230. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  231. package/gsd-core/references/planner-preconditions.md +1 -1
  232. package/gsd-core/references/planner-reviews.md +1 -1
  233. package/gsd-core/references/planner-revision.md +1 -1
  234. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  235. package/gsd-core/references/planning-config.md +44 -13
  236. package/gsd-core/references/reviewer-instances.md +31 -0
  237. package/gsd-core/references/revision-loop.md +1 -1
  238. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  239. package/gsd-core/references/specless-probe-fallback.md +1 -1
  240. package/gsd-core/references/tdd.md +1 -3
  241. package/gsd-core/references/ui-brand.md +65 -21
  242. package/gsd-core/references/ui-consideration-probe.md +1 -1
  243. package/gsd-core/references/universal-anti-patterns.md +5 -5
  244. package/gsd-core/references/verifier-phase-gates.md +192 -0
  245. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  246. package/gsd-core/references/verify-mvp-mode.md +2 -2
  247. package/gsd-core/references/workstream-flag.md +33 -17
  248. package/gsd-core/templates/README.md +1 -1
  249. package/gsd-core/templates/SECURITY.md +3 -3
  250. package/gsd-core/templates/UI-SPEC.md +25 -3
  251. package/gsd-core/templates/VALIDATION.md +3 -3
  252. package/gsd-core/templates/discussion-log.md +1 -1
  253. package/gsd-core/templates/phase-prompt.md +5 -4
  254. package/gsd-core/templates/state.md +11 -4
  255. package/gsd-core/templates/verification-report.md +9 -1
  256. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  257. package/gsd-core/workflows/add-backlog.md +1 -1
  258. package/gsd-core/workflows/add-phase.md +3 -3
  259. package/gsd-core/workflows/add-tests.md +3 -8
  260. package/gsd-core/workflows/add-todo.md +1 -1
  261. package/gsd-core/workflows/ai-integration-phase.md +13 -20
  262. package/gsd-core/workflows/audit-fix.md +12 -3
  263. package/gsd-core/workflows/audit-milestone.md +9 -9
  264. package/gsd-core/workflows/audit-uat.md +17 -2
  265. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  266. package/gsd-core/workflows/autonomous.md +11 -27
  267. package/gsd-core/workflows/check-todos.md +1 -1
  268. package/gsd-core/workflows/cleanup.md +64 -5
  269. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
  270. package/gsd-core/workflows/code-review-fix.md +38 -11
  271. package/gsd-core/workflows/code-review.md +159 -52
  272. package/gsd-core/workflows/complete-milestone.md +151 -23
  273. package/gsd-core/workflows/debug.md +12 -8
  274. package/gsd-core/workflows/diagnose-issues.md +47 -15
  275. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  276. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
  277. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  278. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  279. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  280. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  281. package/gsd-core/workflows/discuss-phase.md +1 -1
  282. package/gsd-core/workflows/do.md +3 -6
  283. package/gsd-core/workflows/docs-update.md +5 -4
  284. package/gsd-core/workflows/edit-phase.md +27 -2
  285. package/gsd-core/workflows/eval-review.md +7 -14
  286. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  287. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
  288. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  289. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  290. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  291. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -4
  292. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  293. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  294. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  295. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  296. package/gsd-core/workflows/execute-phase.md +72 -100
  297. package/gsd-core/workflows/execute-plan.md +52 -15
  298. package/gsd-core/workflows/explore.md +131 -4
  299. package/gsd-core/workflows/extract-learnings.md +1 -1
  300. package/gsd-core/workflows/fast.md +10 -2
  301. package/gsd-core/workflows/forensics.md +1 -1
  302. package/gsd-core/workflows/graduation.md +5 -5
  303. package/gsd-core/workflows/health.md +76 -10
  304. package/gsd-core/workflows/import.md +18 -15
  305. package/gsd-core/workflows/inbox.md +4 -5
  306. package/gsd-core/workflows/ingest-docs.md +49 -16
  307. package/gsd-core/workflows/insert-phase.md +5 -5
  308. package/gsd-core/workflows/list-seeds.md +5 -3
  309. package/gsd-core/workflows/list-workspaces.md +1 -1
  310. package/gsd-core/workflows/manager.md +12 -23
  311. package/gsd-core/workflows/map-codebase.md +1 -1
  312. package/gsd-core/workflows/milestone-summary.md +1 -1
  313. package/gsd-core/workflows/mvp-phase.md +8 -5
  314. package/gsd-core/workflows/new-milestone.md +22 -29
  315. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  316. package/gsd-core/workflows/new-project.md +26 -40
  317. package/gsd-core/workflows/new-workspace.md +1 -1
  318. package/gsd-core/workflows/next.md +14 -2
  319. package/gsd-core/workflows/pause-work.md +1 -1
  320. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  321. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  322. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  323. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  324. package/gsd-core/workflows/plan-phase.md +162 -59
  325. package/gsd-core/workflows/plan-review-convergence.md +96 -11
  326. package/gsd-core/workflows/plant-seed.md +2 -2
  327. package/gsd-core/workflows/pr-branch.md +187 -51
  328. package/gsd-core/workflows/profile-user.md +16 -14
  329. package/gsd-core/workflows/progress.md +61 -18
  330. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  331. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
  332. package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
  333. package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
  334. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  335. package/gsd-core/workflows/quick.md +55 -44
  336. package/gsd-core/workflows/remove-phase.md +4 -4
  337. package/gsd-core/workflows/remove-workspace.md +2 -2
  338. package/gsd-core/workflows/resume-project.md +8 -12
  339. package/gsd-core/workflows/review.md +219 -20
  340. package/gsd-core/workflows/scan.md +1 -1
  341. package/gsd-core/workflows/secure-phase.md +3 -3
  342. package/gsd-core/workflows/session-report.md +2 -1
  343. package/gsd-core/workflows/settings-advanced.md +7 -9
  344. package/gsd-core/workflows/settings-integrations.md +64 -31
  345. package/gsd-core/workflows/settings.md +69 -7
  346. package/gsd-core/workflows/ship.md +116 -50
  347. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  348. package/gsd-core/workflows/sketch.md +12 -18
  349. package/gsd-core/workflows/smart-entry.md +3 -5
  350. package/gsd-core/workflows/spec-phase.md +53 -13
  351. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  352. package/gsd-core/workflows/spike.md +20 -31
  353. package/gsd-core/workflows/stats.md +2 -2
  354. package/gsd-core/workflows/sync-skills.md +64 -9
  355. package/gsd-core/workflows/thread.md +11 -7
  356. package/gsd-core/workflows/transition.md +49 -14
  357. package/gsd-core/workflows/ui-phase.md +15 -21
  358. package/gsd-core/workflows/ui-review.md +8 -12
  359. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  360. package/gsd-core/workflows/undo.md +8 -16
  361. package/gsd-core/workflows/update.md +7 -11
  362. package/gsd-core/workflows/validate-phase.md +3 -3
  363. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  364. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  365. package/gsd-core/workflows/verify-work.md +66 -25
  366. package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
  367. package/hooks/dist/gsd-check-update-worker.js +56 -13
  368. package/hooks/dist/gsd-check-update.js +19 -1
  369. package/hooks/dist/gsd-config-reload.js +18 -12
  370. package/hooks/dist/gsd-context-monitor.js +19 -10
  371. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  372. package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
  373. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  374. package/hooks/dist/gsd-cursor-stop.js +2 -1
  375. package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
  376. package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
  377. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  378. package/hooks/dist/gsd-graphify-update.sh +22 -18
  379. package/hooks/dist/gsd-node-runner.sh +76 -0
  380. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  381. package/hooks/dist/gsd-prompt-guard.js +37 -27
  382. package/hooks/dist/gsd-read-guard.js +16 -7
  383. package/hooks/dist/gsd-read-injection-scanner.js +55 -32
  384. package/hooks/dist/gsd-session-state.sh +1 -0
  385. package/hooks/dist/gsd-statusline.js +231 -24
  386. package/hooks/dist/gsd-update-banner.js +22 -1
  387. package/hooks/dist/gsd-validate-commit.sh +80 -6
  388. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  389. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  390. package/hooks/dist/gsd-workflow-guard.js +162 -46
  391. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  392. package/hooks/dist/gsd-write-guard.js +35 -25
  393. package/hooks/dist/lib/cli-exit.js +560 -0
  394. package/hooks/dist/lib/exit-code-registry.js +98 -0
  395. package/hooks/dist/lib/git-cmd.js +92 -59
  396. package/hooks/dist/lib/git-probe.js +84 -0
  397. package/hooks/dist/lib/hook-exit.js +81 -0
  398. package/hooks/dist/lib/injection-patterns.js +45 -0
  399. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  400. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  401. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  402. package/hooks/gsd-agent-isolation-guard.js +158 -30
  403. package/hooks/gsd-check-update-worker.js +56 -13
  404. package/hooks/gsd-check-update.js +19 -1
  405. package/hooks/gsd-config-reload.js +18 -12
  406. package/hooks/gsd-context-monitor.js +19 -10
  407. package/hooks/gsd-cursor-post-tool.js +3 -1
  408. package/hooks/gsd-cursor-pre-tool.js +2 -3
  409. package/hooks/gsd-cursor-session-start.js +2 -1
  410. package/hooks/gsd-cursor-stop.js +2 -1
  411. package/hooks/gsd-cursor-subagent-start.js +83 -3
  412. package/hooks/gsd-cursor-subagent-stop.js +6 -3
  413. package/hooks/gsd-ensure-canonical-path.js +2 -1
  414. package/hooks/gsd-graphify-update.sh +22 -18
  415. package/hooks/gsd-node-runner.sh +76 -0
  416. package/hooks/gsd-phase-boundary.sh +1 -0
  417. package/hooks/gsd-prompt-guard.js +37 -27
  418. package/hooks/gsd-read-guard.js +16 -7
  419. package/hooks/gsd-read-injection-scanner.js +55 -32
  420. package/hooks/gsd-session-state.sh +1 -0
  421. package/hooks/gsd-statusline.js +231 -24
  422. package/hooks/gsd-update-banner.js +22 -1
  423. package/hooks/gsd-validate-commit.sh +80 -6
  424. package/hooks/gsd-windsurf-pre-command.js +16 -11
  425. package/hooks/gsd-windsurf-pre-write.js +22 -13
  426. package/hooks/gsd-workflow-guard.js +162 -46
  427. package/hooks/gsd-worktree-path-guard.js +36 -21
  428. package/hooks/gsd-write-guard.js +35 -25
  429. package/hooks/lib/cli-exit.js +560 -0
  430. package/hooks/lib/exit-code-registry.js +98 -0
  431. package/hooks/lib/git-cmd.js +92 -59
  432. package/hooks/lib/git-probe.js +84 -0
  433. package/hooks/lib/hook-exit.js +81 -0
  434. package/hooks/lib/injection-patterns.js +45 -0
  435. package/hooks/lib/isolation-deny-reason.js +39 -0
  436. package/hooks/lib/isolation-sentinel.js +9 -0
  437. package/hooks/managed-hooks-registry.cjs +3 -0
  438. package/package.json +28 -11
  439. package/pi/gsd.cjs +19 -5
  440. package/scripts/base64-scan.sh +74 -12
  441. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  442. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  443. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  444. package/scripts/build-hooks.js +5 -0
  445. package/scripts/changeset/lint.cjs +60 -5
  446. package/scripts/check-alias-drift.cjs +7 -43
  447. package/scripts/check-contract-drift.cjs +297 -0
  448. package/scripts/check-glossary-refs.cjs +77 -15
  449. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  450. package/scripts/ci-check-job-near-cap.cjs +49 -0
  451. package/scripts/ci-pr-mergeability.cjs +262 -0
  452. package/scripts/ci-test-scope.cjs +64 -14
  453. package/scripts/ci-timeout-report.cjs +230 -0
  454. package/scripts/command-contract-helpers.cjs +903 -1
  455. package/scripts/docs-guard-registry.cjs +396 -0
  456. package/scripts/gen-adr-index.cjs +728 -38
  457. package/scripts/gen-capability-registry.cjs +11 -21
  458. package/scripts/gen-context-index.cjs +2 -11
  459. package/scripts/gen-exit-code-docs.cjs +318 -0
  460. package/scripts/gen-exit-code-registry.cjs +891 -0
  461. package/scripts/gen-features.cjs +836 -0
  462. package/scripts/gen-health-docs.cjs +390 -0
  463. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  464. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  465. package/scripts/gen-inventory-manifest.cjs +50 -4
  466. package/scripts/gen-loop-host-contract.cjs +138 -25
  467. package/scripts/gen-registry.cjs +3 -14
  468. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  469. package/scripts/gen-state-md-docs.cjs +727 -0
  470. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  471. package/scripts/lib/alias-drift-families.cjs +46 -0
  472. package/scripts/lib/ci-job-timing.cjs +72 -0
  473. package/scripts/lib/cli-exit.cjs +546 -44
  474. package/scripts/lib/drift-scan.cjs +308 -0
  475. package/scripts/lib/exit-code-registry.cjs +98 -0
  476. package/scripts/lib/ndjson-reporter.cjs +119 -0
  477. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  478. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  479. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  480. package/scripts/lint-canary-version-leak.cjs +73 -0
  481. package/scripts/lint-command-contract.cjs +96 -13
  482. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  483. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  484. package/scripts/lint-default-flip-documentation.cjs +193 -0
  485. package/scripts/lint-docs-guard-registration.cjs +495 -0
  486. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  487. package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
  488. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  489. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  490. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  491. package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
  492. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  493. package/scripts/lint-milestone-window-drift.cjs +468 -0
  494. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  495. package/scripts/lint-phase-enumeration-drift.cjs +492 -0
  496. package/scripts/lint-plan-count-drift.cjs +318 -0
  497. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  498. package/scripts/lint-planning-prompt-drift.cjs +471 -0
  499. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  500. package/scripts/lint-regression-test-names.cjs +15 -13
  501. package/scripts/lint-removed-but-needed.cjs +488 -0
  502. package/scripts/lint-seam-enforcement.cjs +182 -0
  503. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  504. package/scripts/lint-source-test-name-collision.cjs +241 -0
  505. package/scripts/lint-state-field-drift.cjs +805 -0
  506. package/scripts/lint-state-write-path-drift.cjs +950 -0
  507. package/scripts/lint-test-file-count.allowlist.json +137 -8
  508. package/scripts/lint-test-file-count.cjs +25 -3
  509. package/scripts/lint-unreachable-guard-drift.cjs +830 -0
  510. package/scripts/lint-vendored-deps.cjs +297 -0
  511. package/scripts/mutation-matrix.cjs +599 -50
  512. package/scripts/pr-changed-files.cjs +63 -0
  513. package/scripts/pr-template-policy.cjs +14 -4
  514. package/scripts/prompt-injection-scan.sh +100 -14
  515. package/scripts/require-issue-link-policy.cjs +192 -0
  516. package/scripts/secret-scan.sh +75 -13
  517. package/scripts/select-docs-guards.cjs +56 -0
  518. package/scripts/sync-runtime-launcher.cjs +24 -7
  519. package/skills/gsd-autonomous/SKILL.md +0 -1
  520. package/skills/gsd-code-review/SKILL.md +1 -1
  521. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  522. package/skills/gsd-execute-phase/SKILL.md +1 -2
  523. package/skills/gsd-import/SKILL.md +1 -1
  524. package/skills/gsd-map-codebase/SKILL.md +1 -1
  525. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  526. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  527. package/skills/gsd-new-milestone/SKILL.md +1 -1
  528. package/skills/gsd-next/SKILL.md +0 -1
  529. package/skills/gsd-plan-phase/SKILL.md +0 -1
  530. package/skills/gsd-progress/SKILL.md +0 -1
  531. package/skills/gsd-quick/SKILL.md +9 -5
  532. package/skills/gsd-review-backlog/SKILL.md +2 -1
  533. package/skills/gsd-stats/SKILL.md +0 -1
  534. package/skills/gsd-verify-work/SKILL.md +1 -1
  535. package/vscode/package.json +1 -1
  536. package/bin/lib/ui-safety-gate.cjs +0 -107
  537. package/gsd-core/workflows/discovery-phase.md +0 -298
  538. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  539. package/gsd-core/workflows/verify-phase.md +0 -574
  540. package/scripts/affected-tests-lib.cjs +0 -554
  541. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  542. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  543. package/scripts/run-affected-tests.cjs +0 -7
  544. package/scripts/run-tests.cjs +0 -1051
@@ -8,12 +8,23 @@
8
8
  * boundary moved. The core.cjs re-export spine was retired in epic #1267;
9
9
  * callers import phase-id helpers from phase-id.cjs directly.
10
10
  *
11
- * Dependencies: none (pure string/regex, no Node built-ins required).
11
+ * Dependencies:
12
+ * - ./pattern.cjs (escapeRegex — #3212 Phase 1 seam; this module is no
13
+ * longer the owner of pattern-escaping, only a consumer)
14
+ * - ./core-utils.cjs (generateSlugInternal — #3883/ADR-3473 §8.3: the
15
+ * canonical slug formula). core-utils.cjs also requires THIS module
16
+ * (comparePhaseNum, scopeToPhase), so a top-level require here would be
17
+ * circular and — per this codebase's compiled-.cjs convention of a
18
+ * single `module.exports = {...}` reassignment at the bottom of each
19
+ * file — a top-level circular require captures a stale, still-empty
20
+ * exports object forever (verified live: it throws
21
+ * "generateSlugInternal is not a function" when core-utils.cjs happens
22
+ * to load first). The require is deferred (lazy, inside each function
23
+ * body) instead, mirroring the same cycle-break already used by
24
+ * core-utils.cts's own getPhaseFileStats/plan-scan.cjs seam.
12
25
  */
26
+ const pattern_cjs_1 = require("./pattern.cjs");
13
27
  // ─── Phase-id helpers ─────────────────────────────────────────────────────────
14
- function escapeRegex(value) {
15
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
- }
17
28
  // project_code values start with an uppercase letter (e.g. PROJ, APP_CODE);
18
29
  // leading underscores are not valid project codes per .planning/config.json.
19
30
  const PROJECT_CODE_PREFIX_STRIP_RE = /^[A-Z][A-Z0-9_]*-(?=\d)/;
@@ -48,6 +59,22 @@ const OPTIONAL_PHASE_TAG_SOURCE = '(?:\\s*\\([^)\\n]{0,200}\\))?';
48
59
  // (scripts/lint-phase-id-drift.cjs) fails CI if a literal re-derivation is
49
60
  // introduced outside this module without a `// phase-id-owner:` justification.
50
61
  const PHASE_NUMBER_TOKEN_SOURCE = '\\d+[A-Z]?(?:\\.\\d+)*';
62
+ // #2528 review: the CASE-FLEXIBLE renderings of the two sources above, for call
63
+ // sites that scan directory names (where a project code or a variant suffix may
64
+ // legitimately be lowercase) and therefore cannot use a case-sensitive class.
65
+ //
66
+ // They live HERE, beside the sources they widen, because the alternative in use
67
+ // was `SOURCE.replaceAll('A-Z', 'A-Za-z')` at the consuming site — a derivation
68
+ // that depends on the owner rendering that exact literal. It passes
69
+ // lint-phase-id-drift.cjs (no literal copy of the grammar), but the day this
70
+ // module expresses the same class any other way (`[[:upper:]]`, a named
71
+ // fragment, an escaped range) the replaceAll silently no-ops and the consumer
72
+ // quietly narrows to uppercase-only — the failure is a NON-match, so nothing
73
+ // throws and no test that only feeds uppercase input notices. Deriving it once,
74
+ // where the source is defined, makes that impossible: a rename here is a
75
+ // compile-visible change, not a silent behavior change three modules away.
76
+ const CASE_FLEXIBLE_PROJECT_CODE_PREFIX_SOURCE = OPTIONAL_PROJECT_CODE_PREFIX_SOURCE.replaceAll('A-Z', 'A-Za-z');
77
+ const CASE_FLEXIBLE_PHASE_NUMBER_TOKEN_SOURCE = PHASE_NUMBER_TOKEN_SOURCE.replaceAll('A-Z', 'A-Za-z');
51
78
  // #2232: the canonical CONTINUATION-segment grammar — a dash-separated segment
52
79
  // that extends a phase token (a zero-padded sub-phase or plan number, e.g. the
53
80
  // "01" in "02-01-setup"). getPhaseDirFromPhaseId writes these zero-padded to
@@ -59,7 +86,8 @@ const PHASE_NUMBER_TOKEN_SOURCE = '\\d+[A-Z]?(?:\\.\\d+)*';
59
86
  // trailing grammar (letter suffixes, dotted sub-phases, segment boundaries).
60
87
  // POLICY (locked by boundary tests): sub-phase/plan numbers ≥100 are out of the
61
88
  // dir-token grammar — the LEADING phase number stays unbounded (`\d+`), only
62
- // continuation segments are width-capped. Shared from here so the five #2043
89
+ // continuation segments begin with a two-digit run; consuming sites retain
90
+ // their established suffix and boundary grammar. Shared from here so the five #2043
63
91
  // call sites cannot drift independently (see scripts/lint-phase-id-drift.cjs).
64
92
  const PHASE_CONTINUATION_SEGMENT_SOURCE = '\\d{2}(?!\\d)';
65
93
  const PHASE_CONTINUATION_SEGMENT_PREFIX_RE = new RegExp(`^${PHASE_CONTINUATION_SEGMENT_SOURCE}`);
@@ -124,7 +152,8 @@ const BRACKET_CANONICAL_NUMERIC_SOURCE = '(?:[1-9]\\d{2,}|\\d{2})';
124
152
  const BRACKET_PHASE_TOKEN_SOURCE = `\\d+[A-Z]?` +
125
153
  `(?:-${BRACKET_CANONICAL_NUMERIC_SOURCE}(?!\\d))?` +
126
154
  `(?:\\.${BRACKET_CANONICAL_NUMERIC_SOURCE}(?!\\d))?` +
127
- `(?:-${PHASE_CONTINUATION_SEGMENT_SOURCE})?`;
155
+ `(?:-${PHASE_CONTINUATION_SEGMENT_SOURCE})?` +
156
+ `(?=-|$)`;
128
157
  // A phase HEADING intro under bracket is either a `[...]` bracket (optionally
129
158
  // followed by a `Phase ` label) or a bare `Phase ` label; a bare number is NOT
130
159
  // a phase-heading intro. The `[^\]]{1,200}` bound mirrors the existing
@@ -196,9 +225,15 @@ function getPhaseDirFromPhaseId(phaseId, phaseName, projectCode) {
196
225
  const milestone = String(parseInt(m[1], 10)).padStart(2, '0');
197
226
  const subParts = m[2].split('-').map(p => String(parseInt(p, 10)).padStart(2, '0'));
198
227
  const sub = subParts.join('-');
199
- const slug = phaseName
200
- ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
201
- : '';
228
+ // #3883 (ADR-3473 §8.3): delegate to the canonical slug formula
229
+ // (generateSlugInternal, core-utils.cts) rather than re-implementing it.
230
+ // `maxLen: null` preserves this site's pre-migration untruncated contract —
231
+ // the 60-char default would silently shadow one on-disk phase dir's
232
+ // reported phase_slug behind another distinct >60-char phase name's.
233
+ // Lazy require to break the core-utils.cjs <-> phase-id.cjs cycle (see the
234
+ // module dependency doc comment above).
235
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
236
+ const slug = phaseName ? (require('./core-utils.cjs').generateSlugInternal(phaseName, null) ?? '') : '';
202
237
  const parts = [milestone, sub, slug].filter(Boolean);
203
238
  const base = parts.join('-');
204
239
  return projectCode ? `${projectCode}-${base}` : base;
@@ -306,7 +341,22 @@ function toDir(id, slug) {
306
341
  const sub = id.subphase ? `.${id.subphase}` : '';
307
342
  // Slug guard: the slug becomes an on-disk path segment, so collapse it to a
308
343
  // safe lowercase token — never a path separator or `..` traversal.
309
- const safeSlug = slug.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
344
+ // #3883 (ADR-3473 §8.3): delegate the sanitize formula itself to the
345
+ // canonical (generateSlugInternal, core-utils.cts) — this fixes the
346
+ // Cyrillic-collapses-to-empty defect (#2848-class) that toDir carried
347
+ // before (it never transliterated). The empty-sanitize and all-digit
348
+ // throw guards below stay: they are a DECLARED DIFFERENCE from every
349
+ // other slug call site, not a bug — a slug here becomes a real directory
350
+ // name, and toDir protects the parsePhaseId dir↔identity bijection
351
+ // (see the toDir docstring above) by refusing to emit an unusable name,
352
+ // where every other site silently accepts "" or a re-truncated value.
353
+ // `maxLen: null` preserves toDir's pre-migration untruncated contract — the
354
+ // 60-char default let two distinct >60-char phase names collapse onto the
355
+ // identical directory name, one silently shadowing the other on disk.
356
+ // Lazy require to break the core-utils.cjs <-> phase-id.cjs cycle (see the
357
+ // module dependency doc comment above).
358
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
359
+ const safeSlug = require('./core-utils.cjs').generateSlugInternal(slug, null) ?? '';
310
360
  // A slug that sanitizes to nothing (e.g. '!!!') would otherwise emit a
311
361
  // dangling trailing hyphen.
312
362
  if (!safeSlug) {
@@ -342,6 +392,39 @@ function isSentinelPhaseId(phaseId, convention) {
342
392
  return false;
343
393
  return SENTINEL_RANGES.includes(parseInt(legacy[1], 10));
344
394
  }
395
+ /**
396
+ * Disk-side sentinel recognizer (#3639): is this on-disk PHASE DIRECTORY a
397
+ * sentinel (never-on-roadmap by convention)?
398
+ *
399
+ * The disk-side guards (C001 gap numbering, W007 orphan dirs) see raw
400
+ * directory names and do not know the repo's naming convention — and neither
401
+ * convention-blind route could recognize a bracket sentinel: `isSentinelPhaseId`
402
+ * without the convention argument reads only the legacy leading int, while
403
+ * `extractPhaseToken(dirName)` (convention-aware or not) strips the MILESTONE
404
+ * and returns the bare phase token — bracket sentinel-ness lives in the
405
+ * milestone portion (`GSD.999-07-icebox` is icebox because of the 999, not
406
+ * the 07). This helper reads the milestone directly off the dir name.
407
+ *
408
+ * The bracket branch requires the FULL bracket dir shape — code prefix, dot,
409
+ * milestone digits, hyphen, PHASE DIGITS — so a #1324 letter-prefixed real
410
+ * dir with a LETTER slug (`P0.0-foundation`) never matches it (ADR-2121
411
+ * indistinguishability, same gate as extractPhaseToken below). DISCLOSED
412
+ * RESIDUAL (#3639 review): the #1324 family also has digit continuations
413
+ * (`P0.0-1-foundation`, `P0.3-2` are real shapes per derivePhaseTokenSegments),
414
+ * and `{code}.{0|999}-{digit}...` is string-indistinguishable from a bracket
415
+ * sentinel dir — no convention-free discriminator exists (ADR-2121). Such a
416
+ * dir reads as sentinel here, which at the disk-guard call sites suppresses
417
+ * a warning (conservative for a linter) rather than deleting data. The
418
+ * digit-continuation family with NON-sentinel first decimals (`P0.3-2`)
419
+ * reads milestone 3 — ordinary — exactly as the convention-gated id
420
+ * predicate does. Everything else falls to the legacy leading-int rule.
421
+ */
422
+ function isSentinelPhaseDir(dirName) {
423
+ const bracketDir = dirName.match(/^[A-Z][A-Z0-9_]*\.(\d+)-\d/); // milestone digits + hyphen + phase DIGITS
424
+ if (bracketDir)
425
+ return SENTINEL_RANGES.includes(parseInt(bracketDir[1], 10));
426
+ return isSentinelPhaseId(dirName);
427
+ }
345
428
  /**
346
429
  * Render a regex source fragment matching a phase number against ROADMAP/STATE
347
430
  * prose regardless of zero-padding on either side.
@@ -355,20 +438,24 @@ function phaseMarkdownRegexSource(phaseNum) {
355
438
  const subParts = milestoneSegments[2].slice(1).split('-');
356
439
  const subFragments = subParts.map(s => {
357
440
  const unpadded = s.replace(/^0+/, '') || '0';
358
- return `0*${escapeRegex(unpadded)}`;
441
+ return `0*${(0, pattern_cjs_1.escapeRegex)(unpadded)}`;
359
442
  });
360
443
  const suffix = milestoneSegments[3] || '';
361
- const suffixFragment = suffix ? escapeRegex(suffix) : '';
362
- return `0*${escapeRegex(majorUnpadded)}-${subFragments.join('-')}${suffixFragment}`;
444
+ const suffixFragment = suffix ? (0, pattern_cjs_1.escapeRegex)(suffix) : '';
445
+ return `0*${(0, pattern_cjs_1.escapeRegex)(majorUnpadded)}-${subFragments.join('-')}${suffixFragment}`;
363
446
  }
364
447
  // Plain numeric phase: 1, 01, 12A, 12.1
365
448
  const match = stripped.match(/^0*(\d+)([A-Z])?((?:\.\d+)*)$/i);
449
+ // #3212: escapeRegex now requires a string (the seam owns coercion policy,
450
+ // not this module) — String(...) here preserves this function's own
451
+ // pre-existing `unknown` acceptance for callers that pass a non-string
452
+ // phaseNum through to this fallback branch.
366
453
  if (!match)
367
- return escapeRegex(phaseNum);
454
+ return (0, pattern_cjs_1.escapeRegex)(String(phaseNum));
368
455
  const integer = match[1].replace(/^0+/, '') || '0';
369
- const letter = match[2] ? escapeRegex(match[2]) : '';
370
- const decimal = match[3] ? escapeRegex(match[3]) : '';
371
- return `0*${escapeRegex(integer)}${letter}${decimal}`;
456
+ const letter = match[2] ? (0, pattern_cjs_1.escapeRegex)(match[2]) : '';
457
+ const decimal = match[3] ? (0, pattern_cjs_1.escapeRegex)(match[3]) : '';
458
+ return `0*${(0, pattern_cjs_1.escapeRegex)(integer)}${letter}${decimal}`;
372
459
  }
373
460
  /**
374
461
  * #3599: when the caller passed a project-code-prefixed ID like `PROJ-42`,
@@ -378,7 +465,7 @@ function phaseMarkdownRegexSourceExact(phaseNum) {
378
465
  const raw = String(phaseNum);
379
466
  if (!hasProjectCodePrefix(raw))
380
467
  return null;
381
- return escapeRegex(raw);
468
+ return (0, pattern_cjs_1.escapeRegex)(raw);
382
469
  }
383
470
  function comparePhaseNum(a, b) {
384
471
  // Strip optional project_code prefix before comparing
@@ -436,27 +523,18 @@ function comparePhaseNum(a, b) {
436
523
  return 0;
437
524
  }
438
525
  /**
439
- * Extract the phase token from a directory name.
526
+ * Segmentation core shared by `extractPhaseToken` (the token VALUE) and
527
+ * `isPhaseArtifact` (the DERIVABILITY check — #3511). Factored out so the two
528
+ * questions ("what is this dir's token" and "did a real token exist at all")
529
+ * can never diverge — see CLAUDE.md's "Generative Fix Divergence" note: this
530
+ * is exactly a shared parser between two parallel surfaces.
531
+ *
532
+ * Returns `tokenSegments.length === 0` iff dirName's own leading segment
533
+ * carries no phase-number token (the `extractPhaseToken` dirName-unchanged
534
+ * fallback) — i.e. the directory name itself does not start with a digit or a
535
+ * short letter+digit prefix, so no reliable phase token can be read from it.
440
536
  */
441
- function extractPhaseToken(dirName, convention) {
442
- // #612 bracket dir form `{CODE}.{MM}-{PP}[.{SS}]-slug` → phase token `PP[.SS]`.
443
- // GATED on convention === 'bracket' (mirrors getMilestoneFromPhaseId's READING-B
444
- // decision above). A bracket dir `{CODE}.{MM}-{PP}` is string-INDISTINGUISHABLE
445
- // from the legacy #2043/#1324 letter-prefixed-decimal family (`P0.3-2`,
446
- // `P0.12-34`) whenever the project code ends in a digit, so NO string-only
447
- // discriminator can separate the two conventions — auto-detecting here silently
448
- // reinterpreted `P0.3-2` → `2` (was `P0.3-2`), a byte-identical-read regression
449
- // on this CRITICAL 6-caller helper (ADR-2121). Requiring an explicit convention
450
- // signal keeps every existing (convention-less) call site byte-identical to
451
- // prior behaviour — see the #2043 numeric-tail characterization in
452
- // tests/phase-id.test.cjs — while keeping the helper pure (optional param, no
453
- // config read). The captured token is dot-only (`PP[.SS]`); the milestone↔phase
454
- // hyphen and any trailing plan/slug are excluded.
455
- if (convention === 'bracket') {
456
- const bracketDir = dirName.match(/^[A-Z][A-Z0-9_]*\.\d+-(\d+(?:\.\d+)?)/);
457
- if (bracketDir)
458
- return bracketDir[1];
459
- }
537
+ function derivePhaseTokenSegments(dirName) {
460
538
  const codePrefixMatch = dirName.match(PROJECT_CODE_PREFIX_CAPTURE_RE_I);
461
539
  let prefix = '';
462
540
  let rest = dirName;
@@ -493,18 +571,295 @@ function extractPhaseToken(dirName, convention) {
493
571
  break;
494
572
  }
495
573
  }
496
- else if (isPhaseContinuationSegment(seg) || (firstLetterPrefixed && /^\d/.test(seg))) {
574
+ else if ((firstLetterPrefixed && /^\d/.test(seg)) ||
575
+ (!firstLetterPrefixed && isPhaseContinuationSegment(seg))) {
497
576
  tokenSegments.push(seg);
498
577
  }
499
578
  else {
500
579
  break;
501
580
  }
502
581
  }
582
+ return { prefix, tokenSegments, firstLetterPrefixed };
583
+ }
584
+ /**
585
+ * Extract the phase token from a directory name.
586
+ */
587
+ function extractPhaseToken(dirName, convention) {
588
+ // #612 bracket dir form `{CODE}.{MM}-{PP}[.{SS}]-slug` → phase token `PP[.SS]`.
589
+ // GATED on convention === 'bracket' (mirrors getMilestoneFromPhaseId's READING-B
590
+ // decision above). A bracket dir `{CODE}.{MM}-{PP}` is string-INDISTINGUISHABLE
591
+ // from the legacy #2043/#1324 letter-prefixed-decimal family (`P0.3-2`,
592
+ // `P0.12-34`) whenever the project code ends in a digit, so NO string-only
593
+ // discriminator can separate the two conventions — auto-detecting here silently
594
+ // reinterpreted `P0.3-2` → `2` (was `P0.3-2`), a byte-identical-read regression
595
+ // on this CRITICAL 6-caller helper (ADR-2121). Requiring an explicit convention
596
+ // signal keeps every existing (convention-less) call site byte-identical to
597
+ // prior behaviour — see the #2043 numeric-tail characterization in
598
+ // tests/phase-id.test.cjs — while keeping the helper pure (optional param, no
599
+ // config read). The captured token is dot-only (`PP[.SS]`); the milestone↔phase
600
+ // hyphen and any trailing plan/slug are excluded.
601
+ if (convention === 'bracket') {
602
+ const bracketDir = dirName.match(/^[A-Z][A-Z0-9_]*\.\d+-(\d+(?:\.\d+)?)/);
603
+ if (bracketDir)
604
+ return bracketDir[1];
605
+ }
606
+ const { prefix, tokenSegments, firstLetterPrefixed } = derivePhaseTokenSegments(dirName);
503
607
  if (tokenSegments.length === 0) {
504
608
  return dirName;
505
609
  }
610
+ // #2528 (re-review): the tokenizer deliberately does NOT try to tell a 2-digit
611
+ // slug word ("24" of "24/7 Autonomy") from a genuine zero-padded continuation
612
+ // ("24" of sub-phase 10.24) — by width alone they are the same string, the gap
613
+ // between #2043's 1-digit and #2232's ≥3-digit guards, and no LOCAL signal
614
+ // separates them. An earlier revision of this fix rewound the token when the
615
+ // segment that stopped the scan was a 1-digit word, which reads
616
+ // "10-24-7-autonomy" correctly but silently re-tokenizes the equally real
617
+ // "10-24-7-zip" (sub-phase 10.24 named "7-Zip …") from "10-24" to "10" — it
618
+ // trades the reported ambiguity for the symmetric one a level down, on a
619
+ // CRITICAL 15-caller chokepoint whose output feeds query-less derivations
620
+ // (STATE.md phase counts, W007, the #2562 key surface).
621
+ //
622
+ // So the token stays the LITERAL reading of the name, and disambiguation lives
623
+ // ONE layer up, in matchPhaseDirs, where a QUERY exists to disambiguate
624
+ // against: a bare-integer lookup falls back to the directory's leading digit
625
+ // run and resolves "10-24-7-autonomy" for "10" without touching what the
626
+ // directory's own token is. That is the same bounded mechanism the
627
+ // "05-80-20-cleanup" shape already uses — one rule for the whole
628
+ // digit-leading-slug family instead of two overlapping ones.
629
+ //
630
+ // A generated slug is lowercase. If the owner admitted a two-digit prefix
631
+ // from a digit+letter slug segment ("10x", "25abc"), remove only that final
632
+ // segment. Uppercase suffixes remain available to the established plan-ID
633
+ // grammar, and dotted continuations remain intact.
634
+ if (!firstLetterPrefixed &&
635
+ tokenSegments.length > 1 &&
636
+ /^\d{2}[a-z][a-z0-9]*$/.test(tokenSegments[tokenSegments.length - 1])) {
637
+ tokenSegments.pop();
638
+ }
506
639
  return prefix + tokenSegments.join('-');
507
640
  }
641
+ /**
642
+ * #3511 (reworked — adversarial review found the membership rule wrong in
643
+ * approach, not just detail): predicate for AGGREGATE phase-directory scans
644
+ * (every matching file contributes, e.g.
645
+ * uat-predicate/phase.cts/state.cts/uat.cts/audit.cts's `*-UAT.md` /
646
+ * `*-VERIFICATION.md` scans) — answers "does fileName belong to THIS phase"
647
+ * so a stray, cross-phase, or ad-hoc file (`04-VERIFICATION.md` sitting in
648
+ * phase 03's directory) cannot contribute its status to phase 03. This is
649
+ * deliberately NOT `resolveVerificationFile` (`src/verification.cts`,
650
+ * #3357/#3492) — that resolver answers a SINGLE-PICK question ("which one
651
+ * candidate is THE report") for a phase dir already known to hold one; this
652
+ * answers a per-file membership question for a scan that must fold in EVERY
653
+ * match. See "Reconciliation" below — the two do NOT fully agree.
654
+ *
655
+ * THE ORIGINAL BUG: files are named by `normalizePhaseName`
656
+ * (`cmdScaffold`, `src/commands.cts` — PADDED, project-code-STRIPPED), while
657
+ * this predicate read the directory's OWN token via the literal, unpadded,
658
+ * project-code-CARRYING `extractPhaseToken(phaseDirName)`. Two different
659
+ * normalizations of the same phase number, so a literal
660
+ * `startsWith(token + '-')` excluded a phase's own artifacts whenever they
661
+ * disagreed: `CK-01-foundation` (token `CK-01`, file `01-VERIFICATION.md`),
662
+ * `1-unpadded` (token `1`, file `01-VERIFICATION.md`), and the #2528
663
+ * digit-leading-slug family `05-80-20-cleanup` / `10-24-7-autonomy` (token
664
+ * over-absorbs past the digit run `cmdScaffold` actually writes: `05-80-20`
665
+ * vs the real `05-UAT.md`).
666
+ *
667
+ * THE FIX: build the set of every phase-number READING this directory could
668
+ * plausibly resolve to elsewhere in the module, then check fileName against
669
+ * ALL of them — reusing the exact readings `matchPhaseDirs` /
670
+ * `phaseNumberForMatch` (#2528, below) already carry for directory
671
+ * RESOLUTION, so this membership check can never diverge from what "the
672
+ * directory for phase N" means elsewhere in the module (no third
673
+ * normalization — CLAUDE.md's Generative Fix Divergence class):
674
+ * 1. the literal token (`extractPhaseToken(phaseDirName)` — still correct
675
+ * for the common case and for genuine decimal / letter-suffixed
676
+ * sub-phase dirs);
677
+ * 2. the same token read off the project-code-STRIPPED name
678
+ * (`stripProjectCodePrefix` — the exact fallback `phaseTokenMatches`
679
+ * already applies, #612/#1324);
680
+ * 3. the directory's own LEADING DIGIT RUN on the stripped name
681
+ * (`LEADING_DIGIT_RUN_RE` — the #2528 bare-integer-fallback reading,
682
+ * the one that actually matches what `cmdScaffold` writes for the
683
+ * digit-leading-slug family); and
684
+ * 4. each of (1)-(3) additionally passed through `normalizePhaseName`,
685
+ * since files always carry the PADDED form and directories often do
686
+ * not (`1-unpadded` vs `01-...`).
687
+ * A file belongs when it starts with any candidate + `-` OR any candidate +
688
+ * `.`, compared case-insensitively (matching `phaseTokenMatches`' own rule —
689
+ * review item 8: `03A-VERIFICATION.md` vs `03a-foo`). This is a PREFIX check,
690
+ * not a full-token equality — `03-01-SUMMARY.md` (phase 03, plan 01) must
691
+ * still match dir `03-foo` on candidate `03`, even though
692
+ * `extractPhaseToken('03-01-SUMMARY.md')` would (wrongly, for this purpose)
693
+ * read `03-01` as a mis-absorbed 2-digit continuation.
694
+ *
695
+ * DOTTED SUB-PHASE CONTINUATION: the dot arm of the check exists because this
696
+ * module's own token grammar (`PHASE_NUMBER_TOKEN_SOURCE`) admits a dotted
697
+ * sub-phase continuation (`(?:\.\d+)*`) alongside dash-continuations — a
698
+ * sub-phase artifact `01.1-CONTEXT.md` is `01`'s own file, written into `01`'s
699
+ * directory, not a stray from a different phase. A dash-only prefix check
700
+ * excluded it (`01.1-` does not start with `01-`), which is over-exclusion:
701
+ * the dangerous direction for an aggregate scan whose fail-safes above all
702
+ * default to inclusion when membership is unclear. Widening dash-only to
703
+ * dash-OR-dot only ever ADDS a match a candidate already earned; it cannot
704
+ * newly admit a file whose leading digits differ from `candidate`, so it
705
+ * cannot resolve a genuinely different phase's artifact (`02.1-...` still
706
+ * fails every `01`-rooted candidate).
707
+ *
708
+ * BRACKET CONVENTION (review item 7): a letter-prefixed-decimal dir
709
+ * (`P0.3-2-slug`) is string-INDISTINGUISHABLE from a bracket-dir token
710
+ * (`extractPhaseToken` above, gated on `convention === 'bracket'`) without an
711
+ * explicit convention signal — and this predicate is never given one: none
712
+ * of its 9 call sites thread `convention`/config through today. Rather than
713
+ * guess a reading it cannot know is active and risk excluding the phase's OWN
714
+ * artifact (the exact defect class this rework exists to fix), this family
715
+ * (`firstLetterPrefixed` dirs) falls into the same include-everything
716
+ * fail-safe as the zero-segment case below — a documented, deliberate
717
+ * widening (it also stops excluding a genuine stray from a DIFFERENT
718
+ * letter-prefixed-decimal phase, narrowly) accepted in trade for never
719
+ * dropping the phase's own report. Convention-aware scoping for this family
720
+ * is deferred to whenever a call site actually threads `convention` through.
721
+ *
722
+ * FAIL-SAFE (#3511, unchanged): when dirName's own leading segment carries no
723
+ * phase-number token at all (`derivePhaseTokenSegments` finds zero segments —
724
+ * the same condition `extractPhaseToken` treats as "return dirName
725
+ * unchanged"), no reliable token exists to scope against. Excluding on an
726
+ * unreliable token would make an aggregate gate silently PERMISSIVE in the
727
+ * wrong direction — dropping the phase's own real blockers — which is worse
728
+ * than the cross-phase-contamination bug this predicate exists to fix.
729
+ * Instead every file is treated as belonging to the phase (returns `true`
730
+ * unconditionally), matching pre-fix (unscoped) behaviour for that directory.
731
+ *
732
+ * FIX 2 — bare `VERIFICATION.md` / `UAT.md` (no dash, no token of its own):
733
+ * `derivePhaseTokenSegments(fileName)` also finds zero segments for these —
734
+ * the file carries no phase number to compare against anything. Directory
735
+ * containment is the only signal available for a token-less file, and it is
736
+ * sufficient: every call site passes `fs.readdirSync` results for ONE
737
+ * specific phase dir, so a token-less candidate already reaching this
738
+ * predicate (past each call site's own verification/UAT suffix pre-filter)
739
+ * is, by construction, that phase's own
740
+ * listing. Returns `true` unconditionally, same as the dir-side fail-safe.
741
+ *
742
+ * RECONCILIATION WITH resolveVerificationFile (#3357/#3492/#3511) — the two
743
+ * surfaces now AGREE. `resolveVerificationFile`'s fallback (`verification.cts`,
744
+ * "Fallback" step in its own docblock) filters its dashed candidates through
745
+ * THIS predicate — `isPhaseArtifact(f, phaseDirName)` — before picking
746
+ * alphabetically-first, via a new `phaseDirName` option every call site
747
+ * threads in (the same basename each already derives for `phaseToken`). So a
748
+ * stray cross-phase file can no longer win the single-pick fallback either:
749
+ * it is excluded there for the identical reason it is excluded from the
750
+ * aggregate scans here — membership, not canonical shape. The fail-safes stay
751
+ * aligned too: when this predicate cannot determine membership for a
752
+ * directory (returns `true` unconditionally — see FAIL-SAFE above),
753
+ * `resolveVerificationFile`'s filter is a no-op and its fallback degrades to
754
+ * the original pre-#3357 "alphabetically first of ALL dashed candidates"
755
+ * behavior, exactly as it always did for that directory shape.
756
+ */
757
+ function isPhaseArtifact(fileName, phaseDirName) {
758
+ const { tokenSegments, firstLetterPrefixed } = derivePhaseTokenSegments(phaseDirName);
759
+ if (tokenSegments.length === 0)
760
+ return true;
761
+ const literalToken = extractPhaseToken(phaseDirName);
762
+ const strippedDir = stripProjectCodePrefix(phaseDirName);
763
+ const strippedToken = strippedDir !== phaseDirName ? extractPhaseToken(strippedDir) : literalToken;
764
+ const leadingRunMatch = strippedDir.match(LEADING_DIGIT_RUN_RE);
765
+ const rawCandidates = [literalToken, strippedToken, leadingRunMatch?.[1]].filter((t) => Boolean(t));
766
+ // Each reading is compared in BOTH its padded and de-padded form: files are
767
+ // written padded by `normalizePhaseName` (`cmdScaffold`) while directories
768
+ // are often not (`1-unpadded`), and legacy trees carry the reverse pairing.
769
+ // De-padding is numeric-only — a token with a letter suffix or a dotted
770
+ // sub-phase (`03A`, `03.1`) has no meaningful de-padded form and is left
771
+ // alone, so this only ever ADDS a reading and can never drop one.
772
+ const depad = (t) => (/^\d+$/.test(t) ? String(Number(t)) : t);
773
+ const candidates = new Set(rawCandidates
774
+ .flatMap(t => [t, normalizePhaseName(t), depad(t)])
775
+ .map(t => t.toUpperCase()));
776
+ const fileUpper = fileName.toUpperCase();
777
+ for (const candidate of candidates) {
778
+ // A dotted sub-phase segment (e.g. `01.1-CONTEXT.md`) is a legitimate
779
+ // continuation of `candidate` per this module's own token grammar
780
+ // (PHASE_NUMBER_TOKEN_SOURCE admits `(?:\.\d+)*`), so it belongs to
781
+ // `candidate`'s own directory just as a dash-continuation does. Inclusion
782
+ // is the safe direction for these aggregate scans (see FAIL-SAFE above) —
783
+ // widening a dash-only check to dash-OR-dot never drops a genuine match,
784
+ // it only stops wrongly excluding one.
785
+ //
786
+ // Accepted separator class after a matched candidate: `-`, `.`, or `_`.
787
+ // The underscore was added for state.cts's `cmdStateValidate` S006/S007
788
+ // scan, whose own pre-filter is deliberately broader than the dashed
789
+ // grammar every other call site uses (`.includes('VERIFICATION')`, no
790
+ // dash required — see the WARNING-4 comment there), so it admits names
791
+ // like `03_VERIFICATION.md`. Before this predicate accepted `_` as a
792
+ // boundary, such a file failed the `-`/`.`-only check here even though
793
+ // its digits matched `candidate` exactly, and `scopeToPhase` dropped it —
794
+ // a real same-phase verification report reported as absent. Widening the
795
+ // separator class only ever EXTENDS a candidate whose digits already
796
+ // match exactly; it cannot admit a genuinely different phase's file,
797
+ // since the candidate comparison itself is unchanged.
798
+ if (fileUpper.startsWith(`${candidate}-`) ||
799
+ fileUpper.startsWith(`${candidate}.`) ||
800
+ fileUpper.startsWith(`${candidate}_`))
801
+ return true;
802
+ }
803
+ // FIX 2: token-less filename (bare "VERIFICATION.md"/"UAT.md") — containment
804
+ // in this phase's own directory listing is sufficient.
805
+ if (derivePhaseTokenSegments(fileName).tokenSegments.length === 0)
806
+ return true;
807
+ // Bracket-convention ambiguity fail-safe — see docblock above.
808
+ if (firstLetterPrefixed)
809
+ return true;
810
+ return false;
811
+ }
812
+ /**
813
+ * #3511: scope `fileNames` to the subset that passes
814
+ * `isPhaseArtifact(fileName, phaseDirName)`. The single seam every
815
+ * phase-directory scan routes through, so the membership rule has ONE owner.
816
+ *
817
+ * AN EMPTY RESULT IS A REAL ANSWER — deliberately, and this is the hard-won
818
+ * part. An earlier revision of this helper carried an extra rule ("scoping
819
+ * must never turn a non-empty set into an empty one": if the filter removed
820
+ * every file, return the unfiltered input). It was added to rescue a
821
+ * directory whose basename merely PARSES phase-shaped —
822
+ * `gsd-651-broad-grep-a1b2`, an `mkdtemp`-style fixture name that
823
+ * `extractPhaseToken` reads as project code `gsd` + phase `651` (the capture
824
+ * regex is case-INSENSITIVE) — holding only `01-bg-VERIFICATION.md`, which
825
+ * the filter then dropped, yielding an empty set indistinguishable from "no
826
+ * report exists".
827
+ *
828
+ * That rescue was wrong, and no local rule can make it right: a directory
829
+ * whose own name says phase 651 holding only a file that says phase 01 is
830
+ * STRING-INDISTINGUISHABLE from `03-foo/` holding only `04-VERIFICATION.md`
831
+ * — the exact cross-phase stray #3511 exists to exclude. Keeping the rule
832
+ * meant a real phase directory holding only a MISFILED report would resolve
833
+ * to it and publish another phase's `passed` as its own: the reported bug, in
834
+ * its single most damaging form. `missing` is the correct answer when a
835
+ * phase's own report is genuinely absent, and every caller already has a
836
+ * `missing`/`null` branch for it.
837
+ *
838
+ * The over-exclusion that rule was reaching for is instead handled where it
839
+ * is actually determinable, inside `isPhaseArtifact`: the zero-token dir
840
+ * fail-safe, the `firstLetterPrefixed` bracket-ambiguity fail-safe, the
841
+ * token-less-filename rule, and the multi-reading candidate set (literal /
842
+ * project-code-stripped / leading-digit-run, each also padded AND de-padded)
843
+ * that covers every normalization a phase directory and its files can
844
+ * legitimately disagree on. A file excluded after all of those genuinely
845
+ * names a different phase.
846
+ *
847
+ * SITE DISCIPLINE: every aggregate-scan call site (`uat.cts`,
848
+ * `uat-predicate.cts`, `phase.cts`, `audit.cts`, `state.cts`,
849
+ * `core-utils.cts`'s `getPhaseFileStats` — #3511 BLOCKER-2 — and
850
+ * `init.cts`'s two phase-info-projection sites — #3511 BLOCKER-3, both of
851
+ * which scope the raw listing once up front and reuse it for every bare
852
+ * `.find()`/`.some()` artifact predicate: context/research/UAT/reviews/
853
+ * patterns) and `resolveVerificationFile`'s single-pick fallback
854
+ * (`verification.cts`) MUST route through this helper rather than calling
855
+ * `isPhaseArtifact` in a filter position directly, so the rule cannot be
856
+ * re-derived per site (CLAUDE.md's Generative Fix Divergence class).
857
+ * `isPhaseArtifact` stays exported for single-item membership questions and
858
+ * its own unit tests.
859
+ */
860
+ function scopeToPhase(fileNames, phaseDirName) {
861
+ return fileNames.filter((f) => isPhaseArtifact(f, phaseDirName));
862
+ }
508
863
  /**
509
864
  * Check if a directory name's phase token matches the normalized phase exactly.
510
865
  */
@@ -520,6 +875,119 @@ function phaseTokenMatches(dirName, normalized) {
520
875
  }
521
876
  return false;
522
877
  }
878
+ /**
879
+ * #2528: the LEADING DIGIT RUN of a directory name — the fragment the
880
+ * bare-integer fallback selects on, and the one `phaseNumberForMatch` then
881
+ * displays. Named (per this module's convention of naming grammar fragments
882
+ * rather than inlining them) because the two sites must not drift: selecting on
883
+ * one run and displaying another would resolve a directory and then label it
884
+ * with a number that never matched.
885
+ *
886
+ * `LEADING_DIGIT_RUN_RE` anchors a trailing `-`-or-end so the run is a whole
887
+ * segment; `_PREFIX` is the same run without that boundary, for reading the run
888
+ * back off a name already known to match.
889
+ */
890
+ const LEADING_DIGIT_RUN_SOURCE = '\\d+';
891
+ const LEADING_DIGIT_RUN_RE = new RegExp(`^(${LEADING_DIGIT_RUN_SOURCE})(?:-|$)`);
892
+ const LEADING_DIGIT_RUN_PREFIX_RE = new RegExp(`^${LEADING_DIGIT_RUN_SOURCE}`);
893
+ const BARE_INTEGER_RE = new RegExp(`^${LEADING_DIGIT_RUN_SOURCE}$`);
894
+ /** Strip leading zeros for numeric-equality compare, keeping a lone "0". */
895
+ const unpad = (digits) => digits.replace(/^0+(?=\d)/, '');
896
+ /**
897
+ * #2528: the CANONICAL phase-directory match selection — the one rule every
898
+ * directory-resolution path (the shared locator plus the `find-phase` and
899
+ * `phase-plan-index` command scans) applies to a candidate dir list. Extracted
900
+ * here because the surrounding scan/ambiguity/shaping code exists per site and
901
+ * had already diverged; the selection itself must not.
902
+ *
903
+ * Two passes:
904
+ * 1. PRIMARY — exact token match (`phaseTokenMatches`), unchanged behavior.
905
+ * 2. BARE-INTEGER FALLBACK — only when the primary pass matched NOTHING and
906
+ * the query is a bare integer, re-filter by each directory's own LEADING
907
+ * digit run (zero-padded compare). This catches digit-leading slug shapes
908
+ * the tokenizer cannot disambiguate from genuine sub-phase segments
909
+ * (e.g. "05-80-20-cleanup", phase 5 named "80/20 Cleanup", whose token
910
+ * "05-80-20" is byte-identical in shape to a real deep-decomposition dir).
911
+ * The fallback can only turn a silent not-found into a resolution or into
912
+ * a surfaced ambiguity (callers keep their #2237 multi-match guards) —
913
+ * never override a primary match.
914
+ *
915
+ * SCOPE, precisely (#2528 re-review). Non-bare QUERIES ("46-6", "12A",
916
+ * "PROJ-42") never enter the fallback, so nothing changes about how a
917
+ * deep-decomposition or letter-suffix lookup is asked. What DOES change is the
918
+ * DIRECTORY side: a bare query now reaches directories the tokenizer classified
919
+ * as multi-segment, and a genuine sub-phase directory has exactly that shape.
920
+ * So `5` against a lone `05-01-auth` resolves (phase_number "05", phase_name
921
+ * "01-auth") where it previously found nothing.
922
+ *
923
+ * That widening is DELIBERATE and it is irreducible from directory names alone.
924
+ * `05-01-auth` (sub-phase 5.1) and `30-12-factor-refactor` (phase 30 named
925
+ * "12-Factor Refactor") are the same string shape — `NN-NN-<slug>` — and the
926
+ * discriminator that would separate them, "is the second segment a valid decimal
927
+ * sub-phase", accepts both (`5.1` and `30.12` are equally well-formed). Any rule
928
+ * strong enough to exclude `05-01-auth` also excludes `30-12-factor-refactor`,
929
+ * which is the defect #2528 exists to fix. The tie is therefore broken in favour
930
+ * of resolving, and the consequence is bounded on the side that matters: when
931
+ * BOTH readings have a directory (`05-01-auth` + `05-02-api`) the result is two
932
+ * matches. `tests/phase-resolution-parity.test.cjs` pins both directions: the
933
+ * lone-directory resolution and the two-directory refusal.
934
+ *
935
+ * WHAT IS SHARED IS SELECTION, NOT AMBIGUITY POLICY. This function is the one
936
+ * owner of "which directories does this query name". What a caller does with
937
+ * two of them stays the caller's own decision, and the callers split in two
938
+ * tiers on purpose:
939
+ *
940
+ * REFUSE on `matches.length > 1` — `searchPhaseInDir`, `cmdFindPhase`,
941
+ * `cmdPhasePlanIndex`, `cmdPhaseRemove`. These either act destructively or
942
+ * answer "which phase is this", so guessing is worse than reporting the
943
+ * candidates (#2237).
944
+ *
945
+ * TAKE `matches[0]` — `cmdPhasesList`, `cmdInitManager`, `cmdRoadmapAnalyze`,
946
+ * `cmdVerifySchemaDrift`, `detectVerifyFailed`. Each read a directory to
947
+ * DECORATE a row they are already emitting; each used `.find()` before this
948
+ * PR, so first-match is their prior behavior preserved verbatim, and each is
949
+ * order-stable because the directory list is sorted and this function filters
950
+ * without reordering.
951
+ *
952
+ * The honest caveat on that second tier: the bare-number fallback makes
953
+ * multi-match newly REACHABLE for inputs that previously found nothing, so those
954
+ * five can now silently pick one of several candidates where they used to report
955
+ * not-found. That is a widening of an existing first-match rule, not a new rule
956
+ * — but it is a widening, and promoting any of them to refusal is a UX decision
957
+ * about their own output, not a change to selection, so it does not belong here.
958
+ *
959
+ * `usedBareFallback` tells callers to derive the displayed phase number from
960
+ * the directory's leading digit run instead of `extractPhaseToken` (whose
961
+ * token for these dirs is the mis-absorbed multi-segment form).
962
+ */
963
+ function matchPhaseDirs(dirs, normalized) {
964
+ const primary = dirs.filter(d => phaseTokenMatches(d, normalized));
965
+ if (primary.length > 0)
966
+ return { matches: primary, usedBareFallback: false };
967
+ const bare = String(normalized);
968
+ if (!BARE_INTEGER_RE.test(bare))
969
+ return { matches: primary, usedBareFallback: false };
970
+ const want = unpad(bare);
971
+ const fallback = dirs.filter(d => {
972
+ const m = stripProjectCodePrefix(d).match(LEADING_DIGIT_RUN_RE);
973
+ return m !== null && unpad(m[1]) === want;
974
+ });
975
+ return { matches: fallback, usedBareFallback: fallback.length > 0 };
976
+ }
977
+ /**
978
+ * #2528: the display phase number for a directory selected by matchPhaseDirs.
979
+ * Primary matches keep the extracted token; bare-fallback matches use the
980
+ * directory's leading digit run (the whole point of the fallback is that the
981
+ * extracted token is wrong for these dirs).
982
+ */
983
+ function phaseNumberForMatch(dirName, usedBareFallback) {
984
+ if (!usedBareFallback)
985
+ return extractPhaseToken(dirName);
986
+ const stripped = stripProjectCodePrefix(dirName);
987
+ const prefix = dirName.slice(0, dirName.length - stripped.length);
988
+ const m = stripped.match(LEADING_DIGIT_RUN_PREFIX_RE);
989
+ return m ? prefix + m[0] : extractPhaseToken(dirName);
990
+ }
523
991
  // ─── Canonical phase KEY surface (#2562) ─────────────────────────────────────
524
992
  //
525
993
  // A phase "key" is the padding-, case- and project-code-insensitive identity of
@@ -699,10 +1167,11 @@ function roadmapPhaseLookupSources(phaseNum) {
699
1167
  return [...new Set(sources)];
700
1168
  }
701
1169
  module.exports = {
702
- escapeRegex,
703
1170
  OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
704
1171
  OPTIONAL_PHASE_TAG_SOURCE,
705
1172
  PHASE_NUMBER_TOKEN_SOURCE,
1173
+ CASE_FLEXIBLE_PROJECT_CODE_PREFIX_SOURCE,
1174
+ CASE_FLEXIBLE_PHASE_NUMBER_TOKEN_SOURCE,
706
1175
  PHASE_CONTINUATION_SEGMENT_SOURCE,
707
1176
  isPhaseContinuationSegment,
708
1177
  BRACKET_PHASE_TOKEN_SOURCE,
@@ -716,11 +1185,16 @@ module.exports = {
716
1185
  toDir,
717
1186
  SENTINEL_RANGES,
718
1187
  isSentinelPhaseId,
1188
+ isSentinelPhaseDir,
719
1189
  phaseMarkdownRegexSource,
720
1190
  phaseMarkdownRegexSourceExact,
721
1191
  comparePhaseNum,
722
1192
  extractPhaseToken,
1193
+ isPhaseArtifact,
1194
+ scopeToPhase,
723
1195
  phaseTokenMatches,
1196
+ matchPhaseDirs,
1197
+ phaseNumberForMatch,
724
1198
  phaseKeyFromToken,
725
1199
  phaseKeyFromDir,
726
1200
  phaseKeyFromProse,