@opengsd/gsd-core 1.14.0 → 1.15.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 (283) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-code-fixer.compact.md +7 -6
  8. package/agents/gsd-code-fixer.md +9 -8
  9. package/agents/gsd-debug-session-manager.compact.md +17 -2
  10. package/agents/gsd-debug-session-manager.md +17 -2
  11. package/agents/gsd-debugger.md +2 -2
  12. package/agents/gsd-eval-auditor.compact.md +1 -1
  13. package/agents/gsd-eval-auditor.md +1 -1
  14. package/agents/gsd-executor.md +13 -8
  15. package/agents/gsd-intel-updater.compact.md +1 -1
  16. package/agents/gsd-intel-updater.md +1 -1
  17. package/agents/gsd-phase-researcher.md +19 -11
  18. package/agents/gsd-plan-checker.md +8 -7
  19. package/agents/gsd-planner.md +12 -8
  20. package/agents/gsd-project-researcher.compact.md +1 -1
  21. package/agents/gsd-project-researcher.md +1 -1
  22. package/agents/gsd-research-synthesizer.compact.md +1 -1
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-auditor.md +155 -17
  25. package/agents/gsd-ui-researcher.compact.md +1 -1
  26. package/agents/gsd-ui-researcher.md +1 -1
  27. package/agents/gsd-verifier.md +10 -9
  28. package/bin/install.js +642 -95
  29. package/commands/gsd/autonomous.md +2 -2
  30. package/commands/gsd/capture.md +1 -1
  31. package/commands/gsd/mempalace-capture.md +7 -3
  32. package/commands/gsd/plan-review-convergence.md +6 -6
  33. package/commands/gsd/progress.md +1 -1
  34. package/commands/gsd/quick-batch.md +1 -1
  35. package/commands/gsd/review.md +2 -3
  36. package/gsd-core/bin/gsd-tools.cjs +335 -22
  37. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  38. package/gsd-core/bin/lib/audit.cjs +81 -13
  39. package/gsd-core/bin/lib/capability-registry.cjs +82 -187
  40. package/gsd-core/bin/lib/capability-validator.cjs +0 -1
  41. package/gsd-core/bin/lib/check-command-router.cjs +101 -14
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  43. package/gsd-core/bin/lib/commands.cjs +175 -42
  44. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  45. package/gsd-core/bin/lib/config.cjs +33 -7
  46. package/gsd-core/bin/lib/decisions.cjs +30 -14
  47. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  48. package/gsd-core/bin/lib/graphify.cjs +10 -2
  49. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  50. package/gsd-core/bin/lib/init.cjs +207 -41
  51. package/gsd-core/bin/lib/install-engine.cjs +13 -0
  52. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  53. package/gsd-core/bin/lib/milestone.cjs +18 -5
  54. package/gsd-core/bin/lib/model-resolver.cjs +159 -50
  55. package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
  56. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  57. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  58. package/gsd-core/bin/lib/phase-id.cjs +109 -7
  59. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  60. package/gsd-core/bin/lib/phase.cjs +227 -26
  61. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  62. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  63. package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
  64. package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
  65. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  66. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  67. package/gsd-core/bin/lib/project-root.cjs +41 -2
  68. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  69. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  70. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  71. package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
  72. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  73. package/gsd-core/bin/lib/roadmap.cjs +251 -31
  74. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
  75. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
  76. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  77. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
  78. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  79. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  80. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  81. package/gsd-core/bin/lib/state.cjs +42 -0
  82. package/gsd-core/bin/lib/surface.cjs +17 -1
  83. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  84. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  85. package/gsd-core/bin/lib/uat.cjs +8 -0
  86. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  87. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  88. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  89. package/gsd-core/bin/lib/verification.cjs +268 -15
  90. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  91. package/gsd-core/bin/lib/verify.cjs +132 -25
  92. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  93. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  94. package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
  95. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  96. package/gsd-core/references/checkpoints.md +5 -3
  97. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  98. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  99. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  100. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  101. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  102. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  103. package/gsd-core/references/edge-probe.md +195 -21
  104. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  105. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  106. package/gsd-core/references/gsd-run-resolver.md +1 -1
  107. package/gsd-core/references/model-profiles.md +1 -1
  108. package/gsd-core/references/phase-argument-parsing.md +9 -7
  109. package/gsd-core/references/phase-id-convention.md +28 -0
  110. package/gsd-core/references/planner-gap-closure.md +2 -0
  111. package/gsd-core/references/planner-load-graph-context.md +24 -13
  112. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  113. package/gsd-core/references/planning-config.md +11 -2
  114. package/gsd-core/references/tdd.md +27 -4
  115. package/gsd-core/references/ui-consideration-probe.md +10 -5
  116. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  117. package/gsd-core/references/worktree-path-safety.md +321 -0
  118. package/gsd-core/templates/verification-report.md +1 -1
  119. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  120. package/gsd-core/workflows/add-backlog.md +1 -1
  121. package/gsd-core/workflows/add-phase.md +1 -1
  122. package/gsd-core/workflows/add-tests.md +2 -2
  123. package/gsd-core/workflows/add-todo.md +3 -3
  124. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  125. package/gsd-core/workflows/audit-fix.md +1 -1
  126. package/gsd-core/workflows/audit-milestone.md +1 -1
  127. package/gsd-core/workflows/audit-uat.md +1 -1
  128. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  129. package/gsd-core/workflows/autonomous.md +16 -6
  130. package/gsd-core/workflows/check-todos.md +2 -2
  131. package/gsd-core/workflows/cleanup.md +2 -2
  132. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  133. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  134. package/gsd-core/workflows/code-review-fix.md +108 -22
  135. package/gsd-core/workflows/code-review.md +63 -46
  136. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
  137. package/gsd-core/workflows/complete-milestone.md +2 -2
  138. package/gsd-core/workflows/debug.md +3 -3
  139. package/gsd-core/workflows/diagnose-issues.md +1 -1
  140. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  141. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  142. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  143. package/gsd-core/workflows/discuss-phase.md +1 -1
  144. package/gsd-core/workflows/do.md +2 -2
  145. package/gsd-core/workflows/docs-update.md +3 -3
  146. package/gsd-core/workflows/edit-phase.md +1 -1
  147. package/gsd-core/workflows/eval-review.md +10 -3
  148. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  149. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  150. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  151. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
  152. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  153. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  154. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  155. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  156. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  157. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  158. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  159. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  160. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  161. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  162. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  163. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  164. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  165. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  166. package/gsd-core/workflows/execute-phase.md +36 -26
  167. package/gsd-core/workflows/execute-plan.md +5 -4
  168. package/gsd-core/workflows/explore.md +3 -3
  169. package/gsd-core/workflows/extract-learnings.md +2 -1
  170. package/gsd-core/workflows/fast.md +1 -1
  171. package/gsd-core/workflows/forensics.md +1 -1
  172. package/gsd-core/workflows/graduation.md +1 -1
  173. package/gsd-core/workflows/health.md +2 -2
  174. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  175. package/gsd-core/workflows/help/modes/full.md +5 -5
  176. package/gsd-core/workflows/help/modes/topic.md +15 -5
  177. package/gsd-core/workflows/import.md +2 -2
  178. package/gsd-core/workflows/inbox.md +2 -2
  179. package/gsd-core/workflows/ingest-docs.md +3 -3
  180. package/gsd-core/workflows/insert-phase.md +1 -1
  181. package/gsd-core/workflows/list-seeds.md +1 -1
  182. package/gsd-core/workflows/list-workspaces.md +1 -1
  183. package/gsd-core/workflows/manager.md +2 -2
  184. package/gsd-core/workflows/map-codebase.md +2 -2
  185. package/gsd-core/workflows/milestone-summary.md +1 -1
  186. package/gsd-core/workflows/mvp-phase.md +1 -1
  187. package/gsd-core/workflows/new-milestone.md +2 -2
  188. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  189. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  190. package/gsd-core/workflows/new-project.md +7 -7
  191. package/gsd-core/workflows/new-workspace.md +2 -2
  192. package/gsd-core/workflows/next.md +1 -1
  193. package/gsd-core/workflows/note.md +1 -1
  194. package/gsd-core/workflows/onboard.md +1 -1
  195. package/gsd-core/workflows/pause-work.md +1 -1
  196. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  197. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  198. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  199. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  200. package/gsd-core/workflows/plan-phase.md +24 -7
  201. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  202. package/gsd-core/workflows/plant-seed.md +62 -20
  203. package/gsd-core/workflows/pr-branch.md +113 -13
  204. package/gsd-core/workflows/profile-user.md +2 -2
  205. package/gsd-core/workflows/progress.md +1 -1
  206. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  207. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  208. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  209. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  210. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  211. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  212. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  213. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  214. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  215. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  216. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  217. package/gsd-core/workflows/quick-batch.md +1 -1
  218. package/gsd-core/workflows/quick.md +21 -9
  219. package/gsd-core/workflows/reapply-patches.md +9 -3
  220. package/gsd-core/workflows/remove-phase.md +1 -1
  221. package/gsd-core/workflows/remove-workspace.md +2 -2
  222. package/gsd-core/workflows/resume-project.md +1 -1
  223. package/gsd-core/workflows/review.md +31 -16
  224. package/gsd-core/workflows/scan.md +1 -1
  225. package/gsd-core/workflows/secure-phase.md +3 -2
  226. package/gsd-core/workflows/settings-advanced.md +30 -10
  227. package/gsd-core/workflows/settings-integrations.md +2 -3
  228. package/gsd-core/workflows/settings.md +4 -4
  229. package/gsd-core/workflows/ship.md +3 -2
  230. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  231. package/gsd-core/workflows/sketch.md +1 -1
  232. package/gsd-core/workflows/smart-entry.md +2 -2
  233. package/gsd-core/workflows/spec-phase.md +15 -5
  234. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  235. package/gsd-core/workflows/spike.md +1 -1
  236. package/gsd-core/workflows/stats.md +1 -1
  237. package/gsd-core/workflows/sync-skills.md +5 -5
  238. package/gsd-core/workflows/thread.md +1 -1
  239. package/gsd-core/workflows/transition.md +1 -1
  240. package/gsd-core/workflows/ui-phase.md +44 -8
  241. package/gsd-core/workflows/ui-review.md +18 -4
  242. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  243. package/gsd-core/workflows/undo.md +339 -20
  244. package/gsd-core/workflows/update.md +7 -7
  245. package/gsd-core/workflows/validate-phase.md +3 -2
  246. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  247. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  248. package/gsd-core/workflows/verify-work.md +81 -16
  249. package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
  250. package/hooks/dist/gsd-secret-read-guard.js +27 -1
  251. package/hooks/dist/gsd-statusline.js +70 -13
  252. package/hooks/dist/gsd-validate-commit.sh +63 -4
  253. package/hooks/gsd-agent-isolation-guard.js +24 -0
  254. package/hooks/gsd-secret-read-guard.js +27 -1
  255. package/hooks/gsd-statusline.js +70 -13
  256. package/hooks/gsd-validate-commit.sh +63 -4
  257. package/package.json +3 -2
  258. package/scripts/build-hooks.js +15 -6
  259. package/scripts/check-contract-drift.cjs +127 -11
  260. package/scripts/command-contract-helpers.cjs +3 -0
  261. package/scripts/docs-guard-registry.cjs +28 -0
  262. package/scripts/gen-loop-host-contract.cjs +69 -0
  263. package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
  264. package/scripts/lib/ndjson-reporter.cjs +3 -2
  265. package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
  266. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
  267. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  268. package/scripts/lint-phase-id-drift.cjs +290 -5
  269. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  270. package/scripts/lint-retired-runtime-name.cjs +619 -0
  271. package/scripts/lint-state-write-path-drift.cjs +93 -0
  272. package/scripts/lint-test-file-count.allowlist.json +28 -9
  273. package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
  274. package/scripts/prompt-injection-scan.sh +4 -0
  275. package/scripts/release-tarball-smoke.cjs +194 -1
  276. package/skills/gsd-autonomous/SKILL.md +2 -2
  277. package/skills/gsd-capture/SKILL.md +1 -1
  278. package/skills/gsd-mempalace-capture/SKILL.md +7 -3
  279. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  280. package/skills/gsd-progress/SKILL.md +1 -1
  281. package/skills/gsd-quick-batch/SKILL.md +1 -1
  282. package/skills/gsd-review/SKILL.md +2 -3
  283. package/vscode/package.json +1 -1
@@ -35,7 +35,7 @@ const frontmatterModule = require("./frontmatter.cjs");
35
35
  const { extractFrontmatter } = frontmatterModule;
36
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
37
37
  const planDependencyGraphModule = require("./plan-dependency-graph.cjs");
38
- const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted } = planDependencyGraphModule;
38
+ const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSummaryFileBlocked } = planDependencyGraphModule;
39
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
40
  const roadmapParserModule = require("./roadmap-parser.cjs");
41
41
  const { getMilestonePhaseFilter } = roadmapParserModule;
@@ -146,13 +146,15 @@ function listArchiveVersionDirs(cwd, wsOverride) {
146
146
  }
147
147
  return out;
148
148
  }
149
- function searchPhaseInDir(baseDir, relBase, normalized) {
149
+ function searchPhaseInDir(baseDir, relBase, normalized, convention) {
150
150
  try {
151
151
  const dirs = readSubdirectories(baseDir, true);
152
152
  // #2528: canonical two-pass selection (exact token match, then the
153
153
  // bare-integer leading-digit-run fallback) shared with the find-phase and
154
154
  // phase-plan-index scans — see phase-id.cts::matchPhaseDirs.
155
- const { matches, usedBareFallback } = matchPhaseDirs(dirs, normalized);
155
+ // #4801: the convention is threaded (optional) so bracket-convention
156
+ // consumers get exact token matching here too.
157
+ const { matches, usedBareFallback } = matchPhaseDirs(dirs, normalized, convention);
156
158
  if (matches.length === 0)
157
159
  return null;
158
160
  // #2237: fail loud when multiple directories match the same bare phase
@@ -176,6 +178,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
176
178
  halted_plans: [],
177
179
  blocked_by: {},
178
180
  runnable_plans: [],
181
+ ready_plans: [],
179
182
  };
180
183
  }
181
184
  const match = matches[0];
@@ -208,12 +211,19 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
208
211
  const planIds = plans.map(p => p.replace('-PLAN.md', '').replace('PLAN.md', ''));
209
212
  const planIdByLower = new Map(planIds.map(id => [id.toLowerCase(), id]));
210
213
  const canonicalToPlanId = new Map(plans.map((p, i) => [extractCanonicalPlanId(p).toLowerCase(), planIds[i]]));
214
+ // #4628: raw depends_on per plan, so readiness can fail closed on a
215
+ // DROPPED edge (a dep token that resolves to nothing carries no evidence)
216
+ // instead of the resolution silently shrinking the dependency list.
217
+ const rawDeps = plans.map((p) => parsePlanDependsOn(phaseDir, p));
218
+ // #4628: completion evidence excludes status:blocked summaries (#3345) —
219
+ // a failure record is not completion, matching cmdPhasePlanIndex's count.
220
+ const completionEvidence = buildSummaryFileIndex(summaries.filter((f) => !isSummaryFileBlocked(node_path_1.default.join(phaseDir, f))), extractCanonicalPlanId);
211
221
  const haltNodes = plans.map((p, i) => {
212
222
  const planId = planIds[i];
213
223
  const canonical = extractCanonicalPlanId(p);
214
224
  const summaryFile = summaryFileByPlanId.get(planId) ?? summaryFileByPlanId.get(canonical);
215
225
  const halted = summaryFile !== undefined && isSummaryFileHalted(node_path_1.default.join(phaseDir, summaryFile));
216
- const resolvedDependsOn = parsePlanDependsOn(phaseDir, p)
226
+ const resolvedDependsOn = rawDeps[i]
217
227
  .map((dep) => {
218
228
  const lower = dep.toLowerCase();
219
229
  return planIdByLower.get(lower) ?? canonicalToPlanId.get(lower) ?? null;
@@ -226,6 +236,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
226
236
  const incompletePlanSet = new Set(incompletePlans);
227
237
  const blockedByFiles = {};
228
238
  const runnablePlans = [];
239
+ const readyPlans = [];
229
240
  for (let i = 0; i < plans.length; i++) {
230
241
  const p = plans[i];
231
242
  if (!incompletePlanSet.has(p))
@@ -233,10 +244,16 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
233
244
  const causes = blockedBy.get(planIds[i]) ?? [];
234
245
  if (causes.length > 0) {
235
246
  blockedByFiles[p] = causes;
247
+ continue;
236
248
  }
237
- else {
238
- runnablePlans.push(p);
239
- }
249
+ runnablePlans.push(p);
250
+ // #4628: DAG-ready on top of runnable — every dependency must have
251
+ // completion evidence (a matching, non-blocked SUMMARY) and no dropped
252
+ // edge: both readers of this contract fail closed identically.
253
+ const depsComplete = rawDeps[i].length === haltNodes[i].resolvedDependsOn.length &&
254
+ haltNodes[i].resolvedDependsOn.every((dep) => completionEvidence.has(dep));
255
+ if (depsComplete)
256
+ readyPlans.push(p);
240
257
  }
241
258
  return {
242
259
  found: true,
@@ -259,19 +276,21 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
259
276
  halted_plans: haltedPlans,
260
277
  blocked_by: blockedByFiles,
261
278
  runnable_plans: runnablePlans,
279
+ ready_plans: readyPlans,
262
280
  };
263
281
  }
264
282
  catch {
265
283
  return null;
266
284
  }
267
285
  }
268
- function findPhaseInternal(cwd, phase) {
286
+ function findPhaseInternal(cwd, phase, convention) {
269
287
  if (!phase)
270
288
  return null;
271
289
  const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
272
290
  const normalized = normalizePhaseName(phase);
273
291
  const relPhasesDir = toPosixPath(node_path_1.default.relative(cwd, phasesDir));
274
- const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized);
292
+ // #4801: convention threaded through to the matcher (see searchPhaseInDir).
293
+ const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized, convention);
275
294
  if (current)
276
295
  return current;
277
296
  // #2855: scope the archived-milestone fallback to the SAME workstream as the
@@ -285,7 +304,7 @@ function findPhaseInternal(cwd, phase) {
285
304
  // getArchivedPhaseDirs via listArchiveVersionDirs (see its doc comment).
286
305
  for (const { version, archivePath } of listArchiveVersionDirs(cwd)) {
287
306
  const relBase = toPosixPath(node_path_1.default.relative(cwd, archivePath));
288
- const result = searchPhaseInDir(archivePath, relBase, normalized);
307
+ const result = searchPhaseInDir(archivePath, relBase, normalized, convention);
289
308
  if (result) {
290
309
  result.archived = version;
291
310
  return result;
@@ -65,6 +65,7 @@ const frontmatterMod = require("./frontmatter.cjs");
65
65
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- state.cjs is an export= CommonJS module
66
66
  const stateMod = require("./state.cjs");
67
67
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
68
+ const planning_document_cjs_1 = require("./planning-document.cjs");
68
69
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
69
70
  const clock_cjs_1 = require("./clock.cjs");
70
71
  const state_transition_cjs_1 = require("./state-transition.cjs");
@@ -86,7 +87,7 @@ const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
86
87
  const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSummaryFileBlocked } = planDependencyGraphMod;
87
88
  // #612: `resolvePhaseIdConvention` selects the write-time milestone-scope
88
89
  // guard's terminator vocabulary (see assertDescriptionPreservesMilestoneScope).
89
- const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolvePhaseIdConvention, } = planningWorkspace;
90
+ const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolveEnvWorkstream, resolvePhaseIdConvention, } = planningWorkspace;
90
91
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- milestone-lock.cjs is an export= CommonJS module
91
92
  const milestoneLockMod = require("./milestone-lock.cjs");
92
93
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -348,6 +349,7 @@ function getRoadmapModeForPhase(cwd, phaseNum) {
348
349
  const milestoneContent = extractCurrentMilestone(rawContent, cwd);
349
350
  const fullContent = stripShippedMilestones(rawContent);
350
351
  const escapedPhase = phaseMarkdownRegexSource(phaseNum);
352
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
351
353
  const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'i');
352
354
  for (const content of [milestoneContent, fullContent]) {
353
355
  const headerMatch = content.match(phaseHeader);
@@ -592,8 +594,16 @@ function cmdFindPhase(cwd, phase, raw) {
592
594
  * none left in this file) degrades to the pre-#3897 two-tier behavior rather
593
595
  * than throwing on a missing argument.
594
596
  */
597
+ /**
598
+ * The dependency resolver's one comparison normalization. Callers that must
599
+ * predict whether a token names a plan reuse this seam instead of copying its
600
+ * case-folding rule.
601
+ */
602
+ function normalizeDependencyToken(token) {
603
+ return String(token).toLowerCase();
604
+ }
595
605
  function resolveDependencyId(dep, planMap, canonicalToId, shortFormToId) {
596
- const lower = dep.toLowerCase();
606
+ const lower = normalizeDependencyToken(dep);
597
607
  if (planMap.has(lower))
598
608
  return planMap.get(lower).id;
599
609
  if (canonicalToId.has(lower))
@@ -634,7 +644,7 @@ function buildShortFormToId(rawPlans) {
634
644
  const canonical = extractCanonicalPlanId(p.id);
635
645
  const lastDash = canonical.lastIndexOf('-');
636
646
  if (lastDash > 0 && lastDash < canonical.length - 1) {
637
- const shortForm = canonical.slice(lastDash + 1).toLowerCase();
647
+ const shortForm = normalizeDependencyToken(canonical.slice(lastDash + 1));
638
648
  if (/^\d+$/.test(shortForm) && !shortFormToId.has(shortForm)) {
639
649
  shortFormToId.set(shortForm, p.id);
640
650
  }
@@ -744,12 +754,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
744
754
  phase: normalized,
745
755
  error: `Phase ${normalized} is ambiguous: ${ambiguousMatches.length} directories match (${ambiguousMatches.map((m) => `"${m}"`).join(', ')}).`,
746
756
  ambiguous_matches: ambiguousMatches,
747
- plans: [], waves: {}, incomplete: [], has_checkpoints: false,
757
+ plans: [], waves: {}, incomplete: [], runnable: [], ready_plans: [], has_checkpoints: false,
748
758
  }, raw);
749
759
  return;
750
760
  }
751
761
  if (!phaseDir) {
752
- output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], has_checkpoints: false }, raw);
762
+ output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], ready_plans: [], has_checkpoints: false }, raw);
753
763
  return;
754
764
  }
755
765
  void phaseDirName; // used only to set phaseDir above
@@ -844,7 +854,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
844
854
  // ── Pass 2: topological level assignment via depends_on DAG ──────────────
845
855
  const seenLower = new Map();
846
856
  for (const p of rawPlans) {
847
- const lower = p.id.toLowerCase();
857
+ const lower = normalizeDependencyToken(p.id);
848
858
  const existing = seenLower.get(lower);
849
859
  if (existing !== undefined) {
850
860
  error(`depends_on index collision in phase ${normalized}: plan IDs '${existing}' and '${p.id}' are identical when case-folded. Rename one file to avoid ambiguous dependency resolution.`);
@@ -852,8 +862,8 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
852
862
  }
853
863
  seenLower.set(lower, p.id);
854
864
  }
855
- const planMap = new Map(rawPlans.map((p) => [p.id.toLowerCase(), p]));
856
- const canonicalToId = new Map(rawPlans.map((p) => [extractCanonicalPlanId(p.id).toLowerCase(), p.id]));
865
+ const planMap = new Map(rawPlans.map((p) => [normalizeDependencyToken(p.id), p]));
866
+ const canonicalToId = new Map(rawPlans.map((p) => [normalizeDependencyToken(extractCanonicalPlanId(p.id)), p.id]));
857
867
  // #3897 rung 4 (ADR-3473 §8.9) — the third depends_on resolution tier.
858
868
  // Resolves a bare in-phase plan-number short form (e.g. "01") to its owning
859
869
  // plan id. In-phase only by construction (T49): the map is built from THIS
@@ -888,6 +898,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
888
898
  const waves = {};
889
899
  const incomplete = [];
890
900
  const runnable = [];
901
+ // #4628: DAG-ready view — see the per-plan emission below. Keyed by id so
902
+ // the per-plan pass can resolve each plan's dependency edges.
903
+ const resolvedDepsByPlan = new Map(haltNodes.map((n) => [n.id, n.resolvedDependsOn]));
904
+ const readyPlans = [];
891
905
  let hasCheckpoints = false;
892
906
  const warnings = [];
893
907
  // #3427 / ADR-3473 §8.5: name every dropped depends_on edge (plan AND
@@ -897,8 +911,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
897
911
  // plan with no dropped edges and a genuinely wrong `wave:` still warns
898
912
  // (N3, D6, T25).
899
913
  const plansWithUnresolvedTokens = new Set();
914
+ const unresolvedTokensByPlan = new Map();
900
915
  for (const { plan, token } of unresolved) {
901
916
  plansWithUnresolvedTokens.add(plan);
917
+ const tokens = unresolvedTokensByPlan.get(plan) ?? [];
918
+ tokens.push(formatDiagnosticToken(token));
919
+ unresolvedTokensByPlan.set(plan, tokens);
902
920
  warnings.push(`Plan ${plan}: depends_on token ${formatDiagnosticToken(token)} does not resolve to any plan in this phase — edge dropped, wave placement for this plan may be unreliable`);
903
921
  }
904
922
  for (const rawPlan of rawPlans) {
@@ -906,14 +924,34 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
906
924
  hasCheckpoints = true;
907
925
  }
908
926
  const blockedByIds = blockedBy.get(rawPlan.id) ?? [];
927
+ // #4628: readiness fails closed — every resolved dependency must have
928
+ // completion evidence (has_summary), and a depends_on edge that never
929
+ // resolved (#3427 — a dropped edge) carries no evidence to check. A
930
+ // single evaluation feeds both the ready_plans list and the per-plan
931
+ // `ready` / `unresolved_dependencies` fields below.
932
+ // Case-fold the dep before the planMap lookup: planMap is lowercase-keyed
933
+ // (#2237) while resolveDependencyId returns the raw id — a mixed-case id
934
+ // must not read as 'no evidence' (over-blocking a ready plan).
935
+ const missingEvidence = (resolvedDepsByPlan.get(rawPlan.id) ?? [])
936
+ .filter((dep) => planMap.get(dep.toLowerCase())?.hasSummary !== true);
937
+ if (plansWithUnresolvedTokens.has(rawPlan.id)) {
938
+ missingEvidence.push(...(unresolvedTokensByPlan.get(rawPlan.id) ?? []));
939
+ }
940
+ const isReady = blockedByIds.length === 0 && missingEvidence.length === 0;
909
941
  if (!rawPlan.hasSummary) {
910
942
  incomplete.push(rawPlan.id);
911
943
  // #2830: the runnable-only view — incomplete AND not transitively
912
944
  // blocked by a halted upstream plan. Additive alongside `incomplete`,
913
945
  // which keeps its existing "no SUMMARY yet" meaning unchanged.
946
+ // #4628: runnable says NOTHING about completion evidence — a runnable
947
+ // plan whose dependencies lack a SUMMARY is not DAG-ready. Consumers
948
+ // dispatch from `ready_plans`.
914
949
  if (blockedByIds.length === 0) {
915
950
  runnable.push(rawPlan.id);
916
951
  }
952
+ if (isReady) {
953
+ readyPlans.push(rawPlan.id);
954
+ }
917
955
  }
918
956
  const computedWave = (level.get(rawPlan.id) ?? 0) + levelOffset;
919
957
  const effectiveWave = computedWave;
@@ -955,6 +993,15 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
955
993
  halted: rawPlan.halted,
956
994
  blocked_by: blockedByIds,
957
995
  };
996
+ if (!rawPlan.hasSummary) {
997
+ // #4628: readiness is a property of INCOMPLETE plans (a summarized plan
998
+ // is filtered by has_summary before readiness is ever consulted). The
999
+ // unresolved_dependencies list names exactly which predecessors lack
1000
+ // completion evidence, for the named-skip report.
1001
+ plan['ready'] = isReady;
1002
+ if (missingEvidence.length > 0)
1003
+ plan['unresolved_dependencies'] = missingEvidence;
1004
+ }
958
1005
  plans.push(plan);
959
1006
  const waveKey = String(effectiveWave);
960
1007
  if (!waves[waveKey]) {
@@ -968,6 +1015,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
968
1015
  waves,
969
1016
  incomplete,
970
1017
  runnable,
1018
+ ready_plans: readyPlans,
971
1019
  has_checkpoints: hasCheckpoints,
972
1020
  };
973
1021
  if (planNamingWarning)
@@ -1115,6 +1163,7 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
1115
1163
  const siblingPlanningDir = (wt) => planningDir(wt, ws);
1116
1164
  const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
1117
1165
  // Same header shape the allocators scan locally (#1729 tag tolerance).
1166
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1118
1167
  const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
1119
1168
  for (const line of porcelain.split('\n')) {
1120
1169
  if (!line.startsWith('worktree '))
@@ -1180,6 +1229,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
1180
1229
  // (section header, roadmap bullet, or on-disk directory) is counted:
1181
1230
  // 1) Section headers: ### Phase N: / ## Phase N: / #### Phase N:
1182
1231
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1232
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1183
1233
  const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
1184
1234
  // 2) Roadmap bullet entries: - [ ] **Phase N: ...** (all checkbox variants)
1185
1235
  // The lookahead accepts colon, decimal-dot, whitespace, bold-close asterisk,
@@ -1289,6 +1339,7 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
1289
1339
  // bullets, on-disk dirs. The bullet scan was missing here — a bullet-only
1290
1340
  // `Phase N` row was invisible to batch allocation (#3849 secondary).
1291
1341
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1342
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1292
1343
  const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
1293
1344
  const bulletPattern = /^[ \t]*-[ \t]*\[[^\]]{0,200}\][ \t]*\*{0,2}Phase[ \t]+(\d+)(?=[:.\s*]|$)/gim;
1294
1345
  let m;
@@ -1416,7 +1467,9 @@ function scanExistingDecimalPhaseNumbers(phasesDir, rawContent, base) {
1416
1467
  decimalSet.add(parseInt(dm[1], 10));
1417
1468
  }
1418
1469
  }
1419
- const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
1470
+ const rmPhasePattern = new RegExp(
1471
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1472
+ `#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
1420
1473
  let rmMatch;
1421
1474
  while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
1422
1475
  decimalSet.add(parseInt(rmMatch[1], 10));
@@ -1443,9 +1496,11 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
1443
1496
  const content = extractCurrentMilestone(rawContent, cwd);
1444
1497
  const normalizedAfter = normalizePhaseName(afterPhase);
1445
1498
  const afterPhaseEscaped = phaseMarkdownRegexSource(normalizedAfter);
1499
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1446
1500
  const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
1447
1501
  const headingMatch = targetPattern.test(content);
1448
1502
  const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
1503
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1449
1504
  const anyHeadingPattern = /#{2,4}\s*Phase\s+\d/i;
1450
1505
  const roadmapHasHeadingPhases = anyHeadingPattern.test(content);
1451
1506
  const isBulletStyle = !headingMatch && bulletPattern.test(content) && !roadmapHasHeadingPhases;
@@ -1522,7 +1577,9 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
1522
1577
  }
1523
1578
  else {
1524
1579
  const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_decimalPhase} to break down)\n`;
1525
- const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
1580
+ const headerPattern = new RegExp(
1581
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1582
+ `(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
1526
1583
  const headerMatch = rawContent.match(headerPattern);
1527
1584
  if (!headerMatch) {
1528
1585
  error(`Could not find Phase ${afterPhase} header`);
@@ -1844,7 +1901,9 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
1844
1901
  if (!isDecimal) {
1845
1902
  // #1729: fold an optional pre-colon ( ) tag into the suffix capture so it
1846
1903
  // is re-emitted verbatim — a tagged later phase still gets renumbered.
1847
- content = content.replace(/(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)((?:\s*\([^)\r\n]{0,200}\))?\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
1904
+ content = content.replace(
1905
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1906
+ /(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)((?:\s*\([^)\r\n]{0,200}\))?\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
1848
1907
  content = content.replace(/(-\s*\[[ x]\]\s*.*?Phase\s+)(\d+)(\s*:|\s+)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
1849
1908
  // ORDINAL-RENUMBER — CELL EDIT (not row-deletion) — migrated onto
1850
1909
  // updateTableCell (ADR-2143 §7, sibling of the deleteTableRow scoping
@@ -2177,6 +2236,7 @@ function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
2177
2236
  if (!roadmapContent || !phaseNum)
2178
2237
  return null;
2179
2238
  const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
2239
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
2180
2240
  const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:\\s*([^\\n]+)`, 'im'));
2181
2241
  if (!heading)
2182
2242
  return null;
@@ -2838,7 +2898,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
2838
2898
  // non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
2839
2899
  // here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
2840
2900
  // which would misreport a present-but-bad marker as no marker at all.
2841
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
2901
+ const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
2842
2902
  if (availableWorkstreams.length > 0 && !resolvedWorkstream) {
2843
2903
  // #3579: getActiveWorkstream now inherits a pointer-less session's read
2844
2904
  // from the shared .planning/active-workstream marker, so reaching this
@@ -3130,7 +3190,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3130
3190
  // whole-slice `.replace()` onto the seam. Applied per single physical
3131
3191
  // line by updateBullet, so the pattern no longer needs the `m` flag
3132
3192
  // (it never sees more than one line at a time); see
3133
- // planCountBodyPattern below for the sites that were migrated onto
3193
+ // writePlansField below for the sites that were migrated onto
3134
3194
  // withPhaseSection instead.
3135
3195
  //
3136
3196
  // #2245 review Fix 6: this is behaviour-preserving for GSD-GENERATED
@@ -3183,12 +3243,105 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3183
3243
  };
3184
3244
  // ADR-2143 §4: the plan-count write is now routed through
3185
3245
  // withPhaseSection (see mutateMilestonePhase below), which hands this
3186
- // pattern ONLY phase N's own detail-section body — so the pattern no
3187
- // longer needs its own `#{2,4}\s*Phase\s+N` anchor + skip-ahead-past-
3188
- // interior-headings lookahead; the section boundary itself confines
3189
- // the match (the #2067/#2200 boundary-crossing class is now
3190
- // structurally impossible for this site rather than regex-enforced).
3191
- const planCountBodyPattern = /(\*\*Plans:\*\*\s*)[^\n]+/i;
3246
+ // seam call ONLY phase N's own detail-section body — the section
3247
+ // boundary itself confines the write (the #2067/#2200 boundary-
3248
+ // crossing class is structurally impossible for this site).
3249
+ //
3250
+ // #4906 Phase 2 (#4917/ADR-4910): migrated off the one-capture-group
3251
+ // regex that replaced to end of line, dropping any hand-written
3252
+ // trailing prose after the count (#4852) — onto the PlanningDoc
3253
+ // `boldField` write seam, whose `valueSpan`/`trailingSpan` split
3254
+ // never touches the trailing annotation.
3255
+ const writePlansField = (body) => {
3256
+ const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(body, 'ROADMAP.md');
3257
+ if (!parsed.ok) {
3258
+ preservationWarnings.push({ field: 'Plans', reason: parsed.reason });
3259
+ return body;
3260
+ }
3261
+ const fieldId = (0, planning_document_cjs_1.findField)(parsed.value, 'Plans');
3262
+ if (!fieldId) {
3263
+ // #4906 regression (#1163 parity, caught by gsd-test against
3264
+ // roadmap.cts's sibling site): a hand-edited or pre-template
3265
+ // ROADMAP.md may carry a PLAIN (non-bold) `Plans:` line rather
3266
+ // than the canonical `**Plans**:`/`**Plans:**` bold field.
3267
+ // BOLD_FIELD_RE stays bold-only (widening it would register
3268
+ // ordinary prose as a spurious field seam-wide) — this fallback
3269
+ // mirrors roadmap.cts's identical one, kept in parity per
3270
+ // Decision 2 rather than letting the two sites diverge on which
3271
+ // legacy shapes they tolerate.
3272
+ const plainMatch = body.match(/^([ \t]*)Plans:([ \t]*)([^\r\n]*)$/m);
3273
+ if (!plainMatch) {
3274
+ // No `**Plans:**`/`**Plans**:`/plain `Plans:` line in this
3275
+ // phase's section — nothing to write; not a failure (mirrors
3276
+ // the old regex's silent no-match no-op).
3277
+ return body;
3278
+ }
3279
+ const [whole, indent, spacing, plainValue] = plainMatch;
3280
+ const plainCountPrefixMatch = plainValue.match(/^(?:\d+\s*\/\s*\d+\s+plans(?:\s+(?:complete|executed))?|\d+\s+plans?)/i);
3281
+ const plainIsTemplatePlaceholder = /^\[\s*Number of plans\b[\s\S]*\]$/i.test(plainValue.trim());
3282
+ if (!plainCountPrefixMatch && !plainIsTemplatePlaceholder) {
3283
+ // Arm 3: freeform prose, TBD, a bracketed human annotation, or
3284
+ // an empty value — leave the field exactly as it was.
3285
+ return body;
3286
+ }
3287
+ const plainNewCountText = `${summaryCount}/${planCount} plans complete`;
3288
+ const plainSuffix = plainCountPrefixMatch ? plainValue.slice(plainCountPrefixMatch[0].length) : '';
3289
+ const newPlainLine = `${indent}Plans:${spacing}${plainNewCountText}${plainSuffix}`;
3290
+ const start = plainMatch.index ?? body.indexOf(whole);
3291
+ return body.slice(0, start) + newPlainLine + body.slice(start + whole.length);
3292
+ }
3293
+ // #4906 regression fix: PREFIX-match the existing value's count
3294
+ // token and re-glue whatever follows it VERBATIM — a glued-on
3295
+ // annotation with no ` — ` separator (e.g. a parenthetical like
3296
+ // `0/1 plans executed (11-16 are gap closure from VERIFICATION)`)
3297
+ // lives entirely inside `value` (`TRAILING_SEPARATOR_RE` in
3298
+ // planning-document.cts only splits on ` — `, unchanged/correct),
3299
+ // so overwriting `value` outright previously destroyed it.
3300
+ //
3301
+ // #4906 review finding (isolated adversarial pass): the prior
3302
+ // version of this migration preserved this site's OLD
3303
+ // unconditional-overwrite behavior for the no-count-prefix case,
3304
+ // which clobbers arm 3 (freeform prose / TBD / a bracketed human
3305
+ // annotation like `[Deferred pending re-scope]`) — a real
3306
+ // regression against the design doc's own Behavior table row 4,
3307
+ // not an accepted trade-off. Fixed here by adopting the SAME
3308
+ // template-placeholder / arm-3-untouched classification
3309
+ // roadmap.cts's sibling site already uses (isTemplatePlaceholder +
3310
+ // "no count prefix and not a placeholder => leave untouched"),
3311
+ // rather than letting the two migrated sites diverge on this.
3312
+ const newCountText = `${summaryCount}/${planCount} plans complete`;
3313
+ const current = (0, planning_document_cjs_1.readNode)(parsed.value, fieldId);
3314
+ if (!current.ok) {
3315
+ return body;
3316
+ }
3317
+ const currentValue = current.value;
3318
+ const countPrefixMatch = currentValue.match(/^(?:\d+\s*\/\s*\d+\s+plans(?:\s+(?:complete|executed))?|\d+\s+plans?)/i);
3319
+ const isTemplatePlaceholder = /^\[\s*Number of plans\b[\s\S]*\]$/i.test(currentValue.trim());
3320
+ if (!countPrefixMatch && !isTemplatePlaceholder) {
3321
+ // Arm 3: freeform prose, TBD, a bracketed human annotation, or an
3322
+ // empty value — leave the field exactly as it was.
3323
+ return body;
3324
+ }
3325
+ const newValueToWrite = countPrefixMatch
3326
+ ? newCountText + currentValue.slice(countPrefixMatch[0].length)
3327
+ : newCountText;
3328
+ const staged = (0, planning_document_cjs_1.setFieldValue)(parsed.value, fieldId, newValueToWrite);
3329
+ if (!staged.ok) {
3330
+ preservationWarnings.push({ field: 'Plans', reason: staged.reason });
3331
+ return body;
3332
+ }
3333
+ const out = (0, planning_document_cjs_1.serialize)(staged.value);
3334
+ if (!out.ok) {
3335
+ // `hasUnreadableNodes` refusal (ADR-4910 amendment) — a ragged
3336
+ // SIBLING node elsewhere in this same section refuses the whole
3337
+ // splice. Never throw / crash the phase-complete transaction over
3338
+ // a node unrelated to this write; surface it and leave `body`
3339
+ // unchanged, same as any other preservation warning.
3340
+ preservationWarnings.push({ field: 'Plans', reason: out.reason });
3341
+ return body;
3342
+ }
3343
+ return out.value;
3344
+ };
3192
3345
  const phaseInfoSummaries = phaseInfo['summaries'];
3193
3346
  // #2200: apply the phase-checkbox flip, the plan-count write, and the
3194
3347
  // per-plan checkbox flips ONLY within the current milestone's region(s)
@@ -3225,7 +3378,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3225
3378
  // section's body, so neither regex can escape into a sibling
3226
3379
  // phase's section, a shipped milestone, or a Backlog entry.
3227
3380
  s = withPhaseSection(s, phaseNum, (body) => {
3228
- let b = body.replace(planCountBodyPattern, `$1${summaryCount}/${planCount} plans complete`);
3381
+ let b = writePlansField(body);
3229
3382
  for (const summaryFile of phaseInfoSummaries) {
3230
3383
  const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
3231
3384
  if (!planId)
@@ -3270,14 +3423,24 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3270
3423
  if (node_fs_1.default.existsSync(reqPath)) {
3271
3424
  const phaseEsc = phaseMarkdownRegexSource(phaseNum);
3272
3425
  const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
3273
- const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
3426
+ const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(
3427
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
3428
+ `(#{2,4}\\s*Phase\\s+${phaseEsc}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
3274
3429
  const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
3275
- const reqMatch = sectionText.match(/\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]+)/i);
3430
+ // #4731: multiline-aware — hard-wrapped Requirements read past the
3431
+ // line break before the ID scan. The shared extractor also stops at
3432
+ // headings and table rows, so a Requirements field followed by the
3433
+ // Traceability table cannot bleed other phases' REQ-IDs into the
3434
+ // citation scan (isolated-review MEDIUM on the inline lookahead,
3435
+ // whose lazy capture swallowed everything to section end).
3436
+ const reqLine = sectionText
3437
+ ? roadmapParserMod.extractPhaseFieldMultiline(sectionText, 'Requirements')
3438
+ : null;
3276
3439
  const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
3277
3440
  let reqContent = originalReqContent;
3278
3441
  // #2316: `citedReqIds` — the REQ-IDs ROADMAP's own **Requirements:**
3279
3442
  // line for this phase actually cites — is hoisted out of the
3280
- // `if (reqMatch)` block (previously scoped only inside it) so the
3443
+ // `if (reqLine)` block (previously scoped only inside it) so the
3281
3444
  // ghost-ID cross-check below (~#2316-1) can consult it. `TBD` is the
3282
3445
  // literal placeholder `phase.add`/`-batch`/`-insert` seed
3283
3446
  // (`**Requirements**: TBD`, src/phase.cts:833,920,1078) — never a
@@ -3289,16 +3452,16 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3289
3452
  // otherwise) — the `if (reqUpdate.ok)` below previously had no
3290
3453
  // `else`, discarding this fact silently instead of surfacing it.
3291
3454
  const traceabilityWriteMisses = [];
3292
- if (reqMatch) {
3455
+ if (reqLine) {
3293
3456
  // #2334 HIGH 3 + #3697: selection and under-selection detection both
3294
3457
  // live in `analyzeRequirementsLine` (module scope, above), extracted in
3295
3458
  // round 3 so the parser is directly testable — a closure in here is
3296
3459
  // reachable only by spawning the CLI, which no fast-check property test
3297
3460
  // can do. `citedReqIds` is byte-identical to the expression that stood
3298
3461
  // here; nothing about what phase-complete MARKS has changed.
3299
- const reqLineAnalysis = analyzeRequirementsLine(reqMatch[1]);
3462
+ const reqLineAnalysis = analyzeRequirementsLine(reqLine);
3300
3463
  citedReqIds = reqLineAnalysis.citedReqIds;
3301
- const reqLineWarning = formatRequirementsLineWarning(phaseNum, reqMatch[1], reqLineAnalysis);
3464
+ const reqLineWarning = formatRequirementsLineWarning(phaseNum, reqLine, reqLineAnalysis);
3302
3465
  if (reqLineWarning) {
3303
3466
  warnings.push(reqLineWarning.message);
3304
3467
  // Carried out to the JSON result as its own field — see
@@ -3584,6 +3747,34 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3584
3747
  let diskNextName = null;
3585
3748
  let roadmapNextNum = null;
3586
3749
  let roadmapNextName = null;
3750
+ // #4699: a phase whose roadmap checkbox is `[x]` is already complete and
3751
+ // must never be selected as next_phase — out-of-order completion (a
3752
+ // reopened phase finished after later phases shipped) otherwise persists
3753
+ // the already-done phase as STATE.md current_phase. Collected from the
3754
+ // same milestone-scoped text the roadmap scan walks; membership is
3755
+ // comparePhaseNum-based so `02` and `2` dedupe. With no ROADMAP.md (or no
3756
+ // parseable rows) the set is empty and the scans behave exactly as
3757
+ // before.
3758
+ const roadmapCompleteNums = [];
3759
+ if (roadmapContent !== null) {
3760
+ try {
3761
+ const milestoneForComplete = extractCurrentMilestone(roadmapContent, cwd);
3762
+ const completePattern = new RegExp(`-\\s*\\[[xX]\\]\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'gi');
3763
+ let cm;
3764
+ while ((cm = completePattern.exec(milestoneForComplete)) !== null) {
3765
+ if (isSentinelPhaseId(cm[1]))
3766
+ continue;
3767
+ if (!roadmapCompleteNums.some((n) => comparePhaseNum(cm[1], n) === 0)) {
3768
+ roadmapCompleteNums.push(cm[1]);
3769
+ }
3770
+ }
3771
+ }
3772
+ catch {
3773
+ /* best-effort: an unreadable milestone section leaves the complete
3774
+ * set empty — the scans then behave exactly as they did pre-#4699. */
3775
+ }
3776
+ }
3777
+ const isCompletePhaseNum = (num) => roadmapCompleteNums.some((n) => comparePhaseNum(num, n) === 0);
3587
3778
  try {
3588
3779
  // #3185 (ADR-3180 Decision 1): "which phase directories belong to
3589
3780
  // the CURRENT milestone" — routed through the canonical owner
@@ -3599,6 +3790,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3599
3790
  // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
3600
3791
  if (isSentinelPhaseId(dm[1]))
3601
3792
  continue;
3793
+ // #4699: an already-complete phase (roadmap checkbox [x]) is never
3794
+ // a next_phase candidate — out-of-order completion must skip it.
3795
+ if (roadmapContent !== null && isCompletePhaseNum(dm[1]))
3796
+ continue;
3602
3797
  // Numeric MINIMUM above N, not "first encountered". `listMilestonePhaseDirs`
3603
3798
  // does sort by `comparePhaseNum`, so a `break` on the first hit happens to be
3604
3799
  // correct today — but that makes this scan's correctness depend on an
@@ -3666,6 +3861,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3666
3861
  // stage 2's heading scan must not advance into backlog headings either.
3667
3862
  if (isSentinelPhaseId(pmNum))
3668
3863
  continue;
3864
+ // #4699: skip complete phases — a `[x]` checkbox row and the
3865
+ // `## Phase Details` heading of an already-done phase both name a
3866
+ // phase that must never be next_phase.
3867
+ if (roadmapContent !== null && isCompletePhaseNum(pmNum))
3868
+ continue;
3669
3869
  // #3701 review: the numeric MINIMUM above N, not the first row above N in
3670
3870
  // DOCUMENT order. This scan walks raw roadmap text, and one global regex
3671
3871
  // sweeps both the `## Phases` checklist and the `## Phase Details`
@@ -4057,4 +4257,5 @@ module.exports = {
4057
4257
  cmdPhaseListPlans,
4058
4258
  computeDependencyLevels,
4059
4259
  buildShortFormToId,
4260
+ normalizeDependencyToken,
4060
4261
  };