@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
@@ -17,12 +17,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.setExplicitProjectRoot = setExplicitProjectRoot;
21
+ exports.resolveProjectRoot = resolveProjectRoot;
20
22
  exports.findProjectRoot = findProjectRoot;
21
23
  exports.consentProjectRoot = consentProjectRoot;
22
24
  const node_fs_1 = __importDefault(require("node:fs"));
23
25
  const node_path_1 = __importDefault(require("node:path"));
24
26
  const node_os_1 = __importDefault(require("node:os"));
25
27
  const FIND_PROJECT_ROOT_MAX_DEPTH = 10;
28
+ // #4894: an operator-supplied `--project-dir` IS the project root — the
29
+ // dispatcher validates it and skips the ancestor walk-up for `cwd`, but code
30
+ // that derives a root from a PHASE DIRECTORY (verification) never sees `cwd`.
31
+ // The dispatcher records the validated root here, once per process, and those
32
+ // callers resolve through `resolveProjectRoot`. `null` (the default, and what
33
+ // the dispatcher sets when the flag is absent) leaves `findProjectRoot`'s
34
+ // walk-up as the only source of truth, so no-flag behavior is unchanged.
35
+ let explicitProjectRoot = null;
36
+ function setExplicitProjectRoot(root) {
37
+ explicitProjectRoot = root;
38
+ }
39
+ function resolveProjectRoot(startDir) {
40
+ return explicitProjectRoot ?? findProjectRoot(startDir);
41
+ }
26
42
  function findProjectRoot(startDir) {
27
43
  let resolvedStart;
28
44
  try {
@@ -43,12 +59,35 @@ function findProjectRoot(startDir) {
43
59
  catch {
44
60
  // fall through
45
61
  }
62
+ // A `.planning` that is itself a symlink is a deliberate hop into a separate
63
+ // tree (the documented "keep planning content out of the tracked repo"
64
+ // convention: `.planning -> ~/.gsd-external-planning/<project>/`, its own git
65
+ // repo). `fs.existsSync(d + '/.git')` DEREFERENCES `d`, so at `d ===
66
+ // <the .planning symlink path>` it finds the EXTERNAL store's `.git`, and
67
+ // both isInsideGitRepo/nearestGitRoot misread it as a crossed nested-child-
68
+ // repo boundary (#2843's guard). `fs.lstatSync` does not dereference its
69
+ // final path component, so it answers "is `d` ITSELF a symlink" (#4815).
70
+ //
71
+ // Deliberately scoped to a symlink named `.planning`, NOT any symlinked
72
+ // directory: a nested child repo reached through some other symlinked
73
+ // ancestor is still a real repo boundary, and skipping it would reopen
74
+ // #2843 through a different trigger.
75
+ function isSymlinkedPlanningDir(d) {
76
+ if (node_path_1.default.basename(d) !== '.planning')
77
+ return false;
78
+ try {
79
+ return node_fs_1.default.lstatSync(d).isSymbolicLink();
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
46
85
  // Walk upward, mirroring isInsideGitRepo from the CJS reference.
47
86
  function isInsideGitRepo(candidateParent) {
48
87
  let d = resolvedStart;
49
88
  while (d !== fsRoot) {
50
89
  try {
51
- if (node_fs_1.default.existsSync(d + node_path_1.default.sep + '.git'))
90
+ if (!isSymlinkedPlanningDir(d) && node_fs_1.default.existsSync(d + node_path_1.default.sep + '.git'))
52
91
  return true;
53
92
  }
54
93
  catch {
@@ -77,7 +116,7 @@ function findProjectRoot(startDir) {
77
116
  if (d === upTo)
78
117
  break;
79
118
  try {
80
- if (node_fs_1.default.existsSync(d + node_path_1.default.sep + '.git'))
119
+ if (!isSymlinkedPlanningDir(d) && node_fs_1.default.existsSync(d + node_path_1.default.sep + '.git'))
81
120
  return d;
82
121
  }
83
122
  catch {
@@ -64,7 +64,7 @@ exports.checkReviewerDocsParity = checkReviewerDocsParity;
64
64
  * `args` is an argv TEMPLATE, not a prefix. The injected pieces — model, effort, output file,
65
65
  * argv-borne prompt — do not all go in the same place, and no positional rule expresses that:
66
66
  * `codex` injects the model in the MIDDLE (after the `exec --ephemeral` subcommand) and the output
67
- * file later still, while `gemini` injects the model first and five lanes end with a bare `-` that
67
+ * file later still, while `kimi-code` injects the model first and five lanes end with a bare `-` that
68
68
  * must stay last. Splicing by position silently produced
69
69
  * `codex --model M -o F exec --ephemeral …`, which is not a valid codex invocation.
70
70
  *
@@ -94,36 +94,16 @@ const SPAWN_STDIN_STDOUT = {
94
94
  outputChannel: 'stdout',
95
95
  };
96
96
  /**
97
- * The twelve declared lanes, in `write_reviews` order.
97
+ * The eleven declared lanes, in `write_reviews` order.
98
+ *
99
+ * The `gemini` lane was retired by #4709: Google sunset Gemini CLI on 2026-06-18 (the same
100
+ * sunset that removed the gemini RUNTIME in #1928/1.8.0), so the lane spawned a binary that no
101
+ * longer serves the free/Pro/Ultra tiers that are GSD's audience.
98
102
  *
99
103
  * `kimi-code` joined in Phase 5b (#2799, closes #2718) — ADR-2782's phase table lands it here
100
104
  * rather than in 5a precisely so it arrives together with the iteration that can invoke it.
101
105
  */
102
106
  exports.REVIEWER_LANES = Object.freeze([
103
- {
104
- slug: 'gemini',
105
- flags: ['--gemini'],
106
- transport: 'spawn',
107
- probe: { kind: 'command-exists', binary: 'gemini' },
108
- invoke: {
109
- binary: 'gemini',
110
- args: ['{{model}}', '-p', '-'],
111
- ...SPAWN_STDIN_STDOUT,
112
- modelArg: '-m',
113
- effortChannel: 'none',
114
- },
115
- timeoutFloorMs: 900_000,
116
- timeoutConfigKey: 'review.timeouts.gemini',
117
- emptyOutput: 'stub-with-stderr',
118
- reviewsSection: 'Gemini',
119
- evidenceClass: 'source-grounded',
120
- requiresBinaries: [],
121
- promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.gemini',
122
- modelConfigKey: 'review.models.gemini',
123
- effortConfigKey: null,
124
- defaultEffort: null,
125
- handler: null,
126
- },
127
107
  {
128
108
  // 1_200_000 rather than the 900_000 floor: headless Claude measured ~525 s
129
109
  // on a large plan set (review.md:304).
@@ -139,8 +119,8 @@ exports.REVIEWER_LANES = Object.freeze([
139
119
  effortChannel: 'argv',
140
120
  // #2483: without these the claude leg is the only reviewer that additionally inherits the
141
121
  // invoking user's global CLAUDE.md, the project CLAUDE.md, and Claude Code auto-memory —
142
- // a context asymmetry against the independent-review premise (gemini sees only the
143
- // assembled prompt; codex runs --ephemeral). Both flags, not just the first: CLAUDE.md
122
+ // a context asymmetry against the independent-review premise (codex sees only the
123
+ // assembled prompt and runs --ephemeral). Both flags, not just the first: CLAUDE.md
144
124
  // loading and auto-memory are independently-toggled mechanisms, and an environment
145
125
  // exporting CLAUDE_CODE_DISABLE_AUTO_MEMORY=0 forces auto-memory back ON — the explicit
146
126
  // pair is robust against that. Applies when /gsd:review runs from a non-Claude-Code host;
@@ -796,7 +776,7 @@ const NON_LANE_SIGNATURE_FLAGS = new Set(['--all']);
796
776
  /**
797
777
  * Is `flag` documented in `text` under one of the two structural shapes docs actually use?
798
778
  *
799
- * Backticked is the `COMMANDS.md` table-cell shape; bracketed (`[--gemini]`) is the
779
+ * Backticked is the `COMMANDS.md` table-cell shape; bracketed (`[--codex]`) is the
800
780
  * `FEATURES.md` signature shape. Requiring one of those two delimiters — rather than a bare
801
781
  * substring — is what keeps prose and fenced examples from satisfying the gate, and it bounds the
802
782
  * token for free: a backticked `--claude` demands its closing backtick, so a backticked
@@ -889,7 +869,7 @@ const BRACKETED_FLAG_RE = /\[(--[a-z0-9][a-z0-9-]*)\]/g;
889
869
  * cell 3. A file-scoped check is therefore satisfied by the forwarding row alone, so deleting a
890
870
  * lane's actual table row — the exact #2781 regression — passes. Keying on cell 1 separates the
891
871
  * two: `| `--agy` / `--antigravity` | … |` is one lane row declaring two flags, while
892
- * `| Reviewer flags | No | … `--gemini`, `--claude` … |` is not a lane row at all.
872
+ * `| Reviewer flags | No | … `--codex`, `--claude` … |` is not a lane row at all.
893
873
  */
894
874
  function flagsInFirstTableCell(lines, declared) {
895
875
  const found = new Set();
@@ -20,7 +20,7 @@
20
20
  * `reviewer` bodies in the capability registry, not a hand-maintained tail.
21
21
  * A capability of EITHER `role: "runtime"` (the six dual-purpose hosts —
22
22
  * antigravity/claude/codex/cursor/opencode/qwen) or the lane-only
23
- * `role: "reviewer"` (gemini/coderabbit/ollama/lm_studio/llama_cpp) may carry
23
+ * `role: "reviewer"` (coderabbit/ollama/lm_studio/llama_cpp) may carry
24
24
  * a `reviewer` body, and it is the body's `reviewer.slug` — NOT the capability
25
25
  * id — that becomes the roster entry: the two differ for `lm-studio` (id) /
26
26
  * `lm_studio` (slug) and `llama-cpp` (id) / `llama_cpp` (slug), ADR-2782's
@@ -229,7 +229,7 @@ function resolveReviewerSelection(input) {
229
229
  // ADR-2782 D4: absent-safe governs DISCOVERY, never explicit selection.
230
230
  // Not finding a lane nobody asked for is normal; failing to run a lane
231
231
  // somebody asked for is an error. Every miss used to be an `info`, so a
232
- // PARTIAL miss (`--gemini --qwen` with qwen absent) ran the review with a
232
+ // PARTIAL miss (`--codex --qwen` with qwen absent) ran the review with a
233
233
  // thinner reviewer set while present_results reported success — "a cross-AI
234
234
  // review that silently drops a lane is blind in one eye" (review.md).
235
235
  // A total miss already errored, but only as a side effect of the selected
@@ -19,6 +19,9 @@ const { routeCjsCommandFamily } = cjsCommandRouterAdapter;
19
19
  // eslint-disable-next-line @typescript-eslint/no-require-imports
20
20
  const roadmapUpgrade = require("./roadmap-upgrade.cjs");
21
21
  // eslint-disable-next-line @typescript-eslint/no-require-imports
22
+ const phaseIdCardMod = require("./phase-id-card.cjs");
23
+ const { phaseIdCard } = phaseIdCardMod;
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
22
25
  const planningWorkspace = require("./planning-workspace.cjs");
23
26
  const { planningDir } = planningWorkspace;
24
27
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -234,7 +237,7 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) {
234
237
  'upgrade': () => {
235
238
  const dryRun = !args.includes('--apply');
236
239
  // Parse `--convention <value>` and `--convention=<value>`. When the flag is
237
- // absent entirely, default to the only supported convention; when present
240
+ // absent entirely, default to the legacy supported convention; when present
238
241
  // with a missing/unsupported value, fall through to the rejection below
239
242
  // (fail-closed — never silently run a migration the user did not request).
240
243
  let convention = 'milestone-prefixed';
@@ -245,13 +248,18 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) {
245
248
  ? token.slice(token.indexOf('=') + 1)
246
249
  : (args[conventionFlagIdx + 1] ?? '');
247
250
  }
248
- if (convention !== 'milestone-prefixed') {
251
+ if (convention !== 'milestone-prefixed' && convention !== 'bracket') {
249
252
  // No-throw hub contract (ADR-0012): a hub-dispatched handler must not call
250
253
  // process.exit. Throw instead — the hub converts this to HandlerFailure and
251
254
  // the adapter routes it through the injected error() boundary.
252
- throw new Error('Only --convention milestone-prefixed is supported');
255
+ throw new Error('Only --convention milestone-prefixed or bracket is supported');
256
+ }
257
+ if (convention === 'bracket') {
258
+ // Keep stdout machine-readable for the dry-run JSON plan. The card is
259
+ // human guidance emitted at command start for both dry-run and apply.
260
+ process.stderr.write(`${phaseIdCard({ title: 'Bracket phase-ID convention' })}\n`);
253
261
  }
254
- const plan = roadmapUpgrade.computeMigrationPlan(cwd);
262
+ const plan = roadmapUpgrade.computeMigrationPlan(cwd, { convention });
255
263
  roadmapUpgrade.applyMigration(cwd, plan, { dryRun });
256
264
  },
257
265
  },
@@ -571,6 +571,55 @@ function sliceMilestoneWindow(content, version) {
571
571
  return null;
572
572
  return content.slice(selected.index, computeMilestoneSectionEnd(content, selected[0], selected.index));
573
573
  }
574
+ /**
575
+ * Enumerate the milestone section ranges recognized by the ROADMAP readers.
576
+ * Markdown boundaries reuse the reader's heading tokenizer, milestone-signal
577
+ * vocabulary, name/version extractor, and section-end owner. Collapsed
578
+ * archives reuse the same `<details><summary>` shape handled by
579
+ * `extractCurrentMilestoneScoped`.
580
+ */
581
+ function milestoneSections(content) {
582
+ const sections = [];
583
+ const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
584
+ for (const heading of headings) {
585
+ if (heading.level < 1 || heading.level > 3)
586
+ continue;
587
+ if (/^Phase\s+\S/i.test(heading.text))
588
+ continue;
589
+ if (!MILESTONE_HEADING_SIGNAL_PATTERN.test(heading.text))
590
+ continue;
591
+ const extracted = extractMilestoneHeadingName(heading.text);
592
+ const version = extracted?.version ?? null;
593
+ const versionMajor = version?.match(/^v(\d+)/i);
594
+ const milestoneInt = versionMajor ? parseInt(versionMajor[1], 10) : null;
595
+ const lineEnd = content.indexOf('\n', heading.offset);
596
+ const fullLine = content.slice(heading.offset, lineEnd === -1 ? content.length : lineEnd);
597
+ sections.push({
598
+ start: heading.offset,
599
+ end: computeMilestoneSectionEnd(content, fullLine, heading.offset, undefined, headings),
600
+ version,
601
+ milestoneInt: milestoneInt !== null && Number.isSafeInteger(milestoneInt) ? milestoneInt : null,
602
+ });
603
+ }
604
+ const detailsPattern = /<details\b[^>]*>[\s\S]*?<\/details>/gi;
605
+ let detailsMatch;
606
+ while ((detailsMatch = detailsPattern.exec(content)) !== null) {
607
+ const summary = detailsMatch[0].match(/<summary[^>]*>([^<]*)<\/summary>/i)?.[1]?.trim() ?? '';
608
+ if (!MILESTONE_HEADING_SIGNAL_PATTERN.test(summary))
609
+ continue;
610
+ const extracted = extractMilestoneHeadingName(summary);
611
+ const version = extracted?.version ?? null;
612
+ const versionMajor = version?.match(/^v(\d+)/i);
613
+ const milestoneInt = versionMajor ? parseInt(versionMajor[1], 10) : null;
614
+ sections.push({
615
+ start: detailsMatch.index,
616
+ end: detailsMatch.index + detailsMatch[0].length,
617
+ version,
618
+ milestoneInt: milestoneInt !== null && Number.isSafeInteger(milestoneInt) ? milestoneInt : null,
619
+ });
620
+ }
621
+ return sections.sort((a, b) => a.start - b.start || a.end - b.end);
622
+ }
574
623
  /**
575
624
  * #3184: counts RAW phase references — a `#{2,4} Phase <id>:` heading
576
625
  * (fence-aware via `tokenizeHeadings`) or a `#2199` bullet entry — BEFORE any
@@ -600,17 +649,37 @@ function sliceMilestoneWindow(content, version) {
600
649
  // decoy `### [GSD.04] Notes:` outside the window manufactured a false V005
601
650
  // while suppressing the correct V004. The digit anchor forecloses both.
602
651
  const BRACKET_PHASE_ENTRY_HEADING_RE = new RegExp(`^${PHASE_HEADING_PREFIX_SRC}${PHASE_NUMBER_TOKEN_SOURCE}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'i');
652
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal
653
+ // mirror of OPTIONAL_PHASE_TAG_SOURCE). Hoisted out of hasPhaseEntries (#4144
654
+ // round 5 Blocker 1) so a consumer OUTSIDE this file's own phase-entry count —
655
+ // the bracket roadmap migrator's "phase-like but unparsed" refusal
656
+ // (src/roadmap-upgrade.cts) — can ask "is this heading text a phase heading
657
+ // by the readers' own rules" without re-deriving the pattern. A heading with
658
+ // no phase-number token and no trailing colon (`## Phase Details`, `##
659
+ // Phase Lifecycle`, `### Phase Notes`) is NOT a phase heading by this
660
+ // grammar — it is an ordinary section heading that happens to start with the
661
+ // word "Phase".
662
+ const PHASE_HEADING_TEXT_RE = /^(?:\[[^\]]{1,200}\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/i;
663
+ /**
664
+ * #4144 round 5 Blocker 1: whether `headingText` — a heading's text with its
665
+ * leading `#{1,6}` markers and surrounding whitespace already stripped
666
+ * (`tokenizeHeadings`'s own `h.text`, or the equivalent for a line a caller
667
+ * already knows is a `#{2,4}` heading) — is a phase heading by the readers'
668
+ * own grammar. The single owner of this test; `hasPhaseEntries` below is
669
+ * itself just the first caller.
670
+ */
671
+ function isPhaseHeadingText(headingText) {
672
+ return PHASE_HEADING_TEXT_RE.test(headingText);
673
+ }
603
674
  function hasPhaseEntries(markdown, phaseIdConvention) {
604
- // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
605
675
  // #3641: the widened grammar engages ONLY when the resolved convention is
606
676
  // 'bracket' — a project that has not opted in runs the legacy pattern
607
677
  // alone, byte-identically.
608
- const phaseHeadingPattern = /^(?:\[[^\]]{1,200}\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/i;
609
678
  const bracketMode = phaseIdConvention === 'bracket';
610
679
  for (const h of (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(markdown)) {
611
680
  if (h.level < 2 || h.level > 4)
612
681
  continue;
613
- if (phaseHeadingPattern.test(h.text))
682
+ if (isPhaseHeadingText(h.text))
614
683
  return true;
615
684
  if (bracketMode && BRACKET_PHASE_ENTRY_HEADING_RE.test(h.text))
616
685
  return true;
@@ -671,6 +740,23 @@ function collectTablePhaseRows(window) {
671
740
  }
672
741
  return rows;
673
742
  }
743
+ /**
744
+ * True when `content` contains a GFM table whose header row is phase-listing
745
+ * SHAPED (`| Phase | ... |`), regardless of whether it also declares a `Name`
746
+ * column. `collectTablePhaseRows` correctly refuses to mint a phase from a
747
+ * `| Phase | Status |`-shaped table (no name column, so no declaration) —
748
+ * but that table's mere presence still signals this roadmap has moved to
749
+ * table-based tracking, not checklist-based tracking. Callers that decide
750
+ * whether to treat bare checklist entries as phase declarations (#4899's
751
+ * roadmap.cts synthesis fallback) must see this signal even when
752
+ * `collectTablePhaseRows` itself returns nothing for the same content —
753
+ * otherwise a thin, name-less Progress table cannot suppress a checklist
754
+ * synthesis it was never meant to coexist with.
755
+ */
756
+ function hasPhaseListingTableHeader(content) {
757
+ const unfenced = (0, markdown_sectionizer_cjs_1.stripFencedCode)(content).text;
758
+ return unfenced.split(/\r?\n/).some((line) => PHASE_LISTING_HEADER_RE.test(line));
759
+ }
674
760
  /**
675
761
  * #3262: the sole owner of "which phase ids does THIS milestone window
676
762
  * declare". Extracted verbatim from `getMilestonePhaseFilter`'s former inline
@@ -965,6 +1051,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
965
1051
  const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : detailsOpenIdx;
966
1052
  const preamble = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(content.slice(0, preambleCutoff), 'details')
967
1053
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1054
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
968
1055
  .replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]{0,200}\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
969
1056
  .replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
970
1057
  const value = preamble + content.slice(detailsOpenIdx, detailsEnd);
@@ -1200,6 +1287,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
1200
1287
  // every phase (phase_count: 0, exit 0). Only strip preamble phase details when
1201
1288
  // the selected milestone section actually contains its own — otherwise the
1202
1289
  // preamble phases ARE this milestone's phases and must be preserved.
1290
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1203
1291
  const currentSectionHasPhaseDetails = /^#{2,4}\s*Phase\s+\S/im.test(currentSection);
1204
1292
  const preambleBase = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(beforeMilestones, 'details');
1205
1293
  // #3235: the conditional wraps the REPLACE, not the pattern. This used to select between the
@@ -1208,6 +1296,7 @@ function extractCurrentMilestoneScoped(content, cwd, ws, phaseIdConvention) {
1208
1296
  // replacement argument, so changing `''` would silently give the no-op branch a real effect.
1209
1297
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1210
1298
  const preambleWithoutPhaseDetails = currentSectionHasPhaseDetails
1299
+ // phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
1211
1300
  ? preambleBase.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]{0,200}\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
1212
1301
  : preambleBase;
1213
1302
  // Unconditional in BOTH branches -- the #730 `Phase Details` heading strip is independent of
@@ -2056,6 +2145,69 @@ function currentMilestoneRawRanges(content, cwd) {
2056
2145
  }
2057
2146
  return { primary: { start: sectionStart, end: sectionEnd }, details };
2058
2147
  }
2148
+ /**
2149
+ * #4731 — extract a bold-labeled phase field (Goal, Requirements, ...) reading
2150
+ * past hard-wrapped continuation lines. The roadmapper soft-wraps long fields
2151
+ * at ~85 chars, so a single-line capture silently truncated every wrapped
2152
+ * Goal/Requirements. Stops at: another `**Field**` label line, a blank line,
2153
+ * or any heading. Wrapped lines are trimmed and joined with single spaces; a
2154
+ * single-line field is returned unchanged (trimmed). Returns null when the
2155
+ * label is absent.
2156
+ */
2157
+ function extractPhaseFieldMultiline(section, label) {
2158
+ // #2769 label shapes: `**X:**`, `**X**:`, and the spaced `**X** :`.
2159
+ // #4837: anchored to line start (`^[ \t]*`, `m` flag) so an inline
2160
+ // `**Label**` mention embedded in an EARLIER field's own body text cannot
2161
+ // shadow the real field's own declaration line. `[ \t]*` (not `\s*`) keeps
2162
+ // the anchor scoped to same-line leading whitespace under the `m` flag.
2163
+ const labelRe = new RegExp('^[ \\t]*\\*\\*' + label + '(?::\\*\\*|\\*\\*\\s*:?)\\s*([^\\n]+)', 'im');
2164
+ const match = section.match(labelRe);
2165
+ if (!match)
2166
+ return null;
2167
+ const startIdx = match.index ?? 0;
2168
+ const after = section.slice(startIdx + match[0].length);
2169
+ const firstLine = match[1].trim();
2170
+ // #4837 (isolated-review finding): the continuation loop's fence-stop check
2171
+ // only ever sees lines AFTER the label's own line — a fence opener on the
2172
+ // SAME line as the label (`**Requirements:** ```js`) was invisible to it,
2173
+ // leaking one line of fence content before the closing fence line
2174
+ // coincidentally matched the same stop check. Checked here too, so a fence
2175
+ // opening on the label's own line is caught just as conservatively.
2176
+ if (/^(?:`{3,}|~{3,})/.test(firstLine))
2177
+ return null;
2178
+ const contLines = [];
2179
+ const lines = after.split('\n');
2180
+ for (let li = 0; li < lines.length; li++) {
2181
+ const raw = lines[li];
2182
+ // The split's first element is the remainder of the captured first line's
2183
+ // own newline — an empty leading element is the line break, not a blank
2184
+ // continuation line.
2185
+ if (li === 0 && !raw.trim())
2186
+ continue;
2187
+ if (!raw.trim())
2188
+ break;
2189
+ // #4837: a list item is a structural boundary — the real corruption
2190
+ // vector (a `- Deferred to Phase N: OTHER-ID` bullet folding a foreign
2191
+ // REQ-ID into this field's citation-scan input).
2192
+ if (/^\s*[-*+]\s/.test(raw))
2193
+ break;
2194
+ // #4837: case-insensitive label-start character class — a
2195
+ // lowercase-first-letter label (`**requirements:**`) is as much a field
2196
+ // boundary as a capitalized one.
2197
+ if (/^\s*\*\*[A-Za-z][A-Za-z ]*:?(\*\*)?:?\s/.test(raw))
2198
+ break;
2199
+ if (/^\s*#{1,4}\s/.test(raw))
2200
+ break;
2201
+ if (/^\s*\|/.test(raw))
2202
+ break;
2203
+ // #4837: a fenced code block opener stops the scan — folding fence
2204
+ // content is never safe, and stopping early is conservative.
2205
+ if (/^\s*(?:`{3,}|~{3,})/.test(raw))
2206
+ break;
2207
+ contLines.push(raw.trim());
2208
+ }
2209
+ return [firstLine, ...contLines].join(' ').trim() || null;
2210
+ }
2059
2211
  module.exports = {
2060
2212
  stripShippedMilestones,
2061
2213
  extractCurrentMilestone,
@@ -2077,6 +2229,7 @@ module.exports = {
2077
2229
  // own doc comment. milestone.cts's destructive-consumer guard consumes
2078
2230
  // this instead of composing locate+select+section-end itself.
2079
2231
  sliceMilestoneWindow,
2232
+ milestoneSections,
2080
2233
  hasVersionedMilestones,
2081
2234
  hasMilestoneSectioning,
2082
2235
  // #3642: the >=1 sibling buildStateFrontmatter's unbounded branch consumes.
@@ -2090,9 +2243,16 @@ module.exports = {
2090
2243
  // guards and the edit-phase workflow's pre/post capture are built on.
2091
2244
  scanMilestonePhaseIds,
2092
2245
  collectTablePhaseRows,
2246
+ hasPhaseListingTableHeader,
2093
2247
  findMilestoneScopeHeadingLines,
2094
2248
  // #3641: the scope axis's phase-ENTRY predicate, exported so roadmap
2095
2249
  // validate's V004 document-level check routes through the same single
2096
2250
  // owner (and its convention gate) instead of a private inline copy.
2251
+ extractPhaseFieldMultiline,
2097
2252
  hasPhaseEntries,
2253
+ // #4144 round 5 Blocker 1: the readers' own phase-heading grammar, so the
2254
+ // bracket roadmap migrator's "phase-like but unparsed" refusal
2255
+ // (src/roadmap-upgrade.cts) can gate on it instead of a private, looser
2256
+ // "starts with the word Phase" regex.
2257
+ isPhaseHeadingText,
2098
2258
  };