@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
@@ -41,6 +41,8 @@ const { SCOPE } = planningScopeMod;
41
41
  const secrets_cjs_1 = require("./secrets.cjs");
42
42
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
43
43
  const scanPhasePlans = require("./plan-scan.cjs");
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-document.cjs is an export= CommonJS module
45
+ const planDocument = require("./plan-document.cjs");
44
46
  const state_document_cjs_1 = require("./state-document.cjs");
45
47
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
46
48
  const host_runtime_detection_cjs_1 = require("./host-runtime-detection.cjs");
@@ -87,9 +89,9 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
87
89
  const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocator;
88
90
  const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
89
91
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
90
- const { comparePhaseNum, normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName } = phaseId;
92
+ const { comparePhaseNum, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
91
93
  const { pruneOrphanedWorktrees } = worktreeSafety;
92
- const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
94
+ const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, resolveEnvWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, resolvePhaseIdConvention, } = planningWorkspace;
93
95
  const { determinePhaseStatus } = commandsMod;
94
96
  const { extractFrontmatter } = frontmatterMod;
95
97
  const { isPhaseComplete, resolveVerificationFile, resolveUatFile } = verificationMod;
@@ -100,7 +102,6 @@ const { resolveCapabilityRuntimeState } = capabilityStateMod;
100
102
  // Unused but imported for structural parity
101
103
  void stripShippedMilestones;
102
104
  // Accept all bold/colon variants of the Requirements header (#2769)
103
- const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
104
105
  // #2056/#2104: isForeignPrefixedPhaseQuery is imported from phase-id.cts
105
106
  // (the canonical predicate). parsePhasePrefix is no longer needed locally.
106
107
  // phaseInfoMatchesExactPrefix and roadmapPhaseMatchesExactPrefix are local
@@ -110,9 +111,18 @@ function phaseInfoMatchesExactPrefix(phaseInfo, phase) {
110
111
  const numStr = typeof num === 'string' ? num : (typeof num === 'number' ? String(num) : '');
111
112
  return numStr.toUpperCase() === phase.toUpperCase();
112
113
  }
114
+ // #4906 Phase 5 (#4984): NOT migrated onto the heading-baseline selector —
115
+ // this site was never one of the five ADR-4910 §8 call sites this phase owns
116
+ // (init milestone/progress heading scans + milestone.cts's unstarted-phase
117
+ // guard, all via buildPhaseHeadingScanRegex), and adding a NEW direct
118
+ // any-bracket selector consumer here inflated init.cts's
119
+ // tests/adr-612-bracket-heading-selection.test.cjs census past its pinned
120
+ // count. Left bracket-blind deliberately, matching this call's pre-#4984
121
+ // behavior; folding it into the census is Phase 6 work.
113
122
  function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
114
123
  const sectionRaw = roadmapPhase?.['section'];
115
124
  const section = typeof sectionRaw === 'string' ? sectionRaw : '';
125
+ // phase-id-owner: deliberately unmigrated (#4984 revert) — see comment above the function.
116
126
  return new RegExp(`^#{2,4}\\s*Phase\\s+${(0, pattern_cjs_1.escapeRegex)(phase)}(?:\\b|\\s|:)`, 'i').test(section);
117
127
  }
118
128
  // #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
@@ -584,6 +594,13 @@ function detectPhaseMvpMode(cwd, phaseNumber) {
584
594
  const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
585
595
  const content = extractCurrentMilestone(rawContent, cwd);
586
596
  const escapedPhase = (0, pattern_cjs_1.escapeRegex)(phaseNumber);
597
+ // #4906 Phase 5 (#4984): NOT migrated onto phaseHeadingPrefixSrcFor — same
598
+ // out-of-scope reasoning as roadmapPhaseMatchesExactPrefix above (only the
599
+ // five buildPhaseHeadingScanRegex sites in init.cts/milestone.cts are this
600
+ // phase's owned migration); a direct selector call here would have added
601
+ // a fourth uncounted ANY_BRACKET consumer to init.cts's pinned
602
+ // tests/adr-612-bracket-heading-selection.test.cjs census.
603
+ // phase-id-owner: deliberately unmigrated (#4984 revert) — see comment above.
587
604
  const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
588
605
  const headerMatch = content.match(phaseHeader);
589
606
  if (!headerMatch || headerMatch.index === undefined)
@@ -751,6 +768,54 @@ function buildSectionManifestField(cwd, phaseInfo, options, workflow, overrides
751
768
  function milestoneRecord(cwd) {
752
769
  return (getMilestoneInfo(cwd).value ?? {});
753
770
  }
771
+ /**
772
+ * #4683 — threat IDs claimed by more than one of the phase's live PLAN files.
773
+ * `scanPhasePlans().planFiles` is the right input set twice over: it excludes
774
+ * derivative files (OUTLINE / PLAN-REVIEW / pre-bounce) AND `status: superseded`
775
+ * plans (#2349) — a superseded plan's IDs were deliberately reassigned to its
776
+ * replacement, so they must not hold against it. The per-document row parse is
777
+ * planDocument.extractThreatRegisterIds; only `<threat_model>` register rows
778
+ * count, and the reserved `T-{phase}-SC` shape is excluded there by grammar
779
+ * (every plan keeps that row by design). A missing/unreadable phase directory
780
+ * degrades to "no duplicates" — planning a brand-new phase has nothing to
781
+ * collide with.
782
+ */
783
+ function findDuplicateThreatIds(cwd, phaseDirRel) {
784
+ if (!phaseDirRel)
785
+ return [];
786
+ const phaseDir = node_path_1.default.join(cwd, phaseDirRel);
787
+ let planFiles;
788
+ try {
789
+ planFiles = scanPhasePlans(phaseDir).planFiles;
790
+ }
791
+ catch {
792
+ return [];
793
+ }
794
+ const owners = new Map();
795
+ for (const planFile of planFiles) {
796
+ let content;
797
+ try {
798
+ content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, planFile), 'utf-8');
799
+ }
800
+ catch {
801
+ continue;
802
+ }
803
+ for (const id of planDocument.extractThreatRegisterIds(content)) {
804
+ const claimed = owners.get(id);
805
+ if (claimed) {
806
+ if (!claimed.includes(planFile))
807
+ claimed.push(planFile);
808
+ }
809
+ else {
810
+ owners.set(id, [planFile]);
811
+ }
812
+ }
813
+ }
814
+ return [...owners.entries()]
815
+ .filter(([, claims]) => claims.length > 1)
816
+ .map(([id, plans]) => ({ id, plans: [...plans].sort() }))
817
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
818
+ }
754
819
  function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
755
820
  if (!phase) {
756
821
  error('phase required for init execute-phase');
@@ -788,9 +853,14 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
788
853
  has_reviews: false,
789
854
  };
790
855
  });
791
- const reqMatch = roadmapPhase?.['section']?.match(REQUIREMENTS_HEADER_RE);
792
- const reqExtracted = reqMatch
793
- ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
856
+ // #4731: multiline-aware — the Requirements field may hard-wrap, so the
857
+ // value is extracted past the line break before the ID scan.
858
+ const phaseSection = roadmapPhase?.['section'];
859
+ const reqLine = phaseSection
860
+ ? roadmapParser.extractPhaseFieldMultiline(phaseSection, 'Requirements')
861
+ : null;
862
+ const reqExtracted = reqLine
863
+ ? reqLine.replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
794
864
  : null;
795
865
  const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
796
866
  // #3188: these paths are null when the file is absent, matching the contract
@@ -800,6 +870,8 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
800
870
  const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
801
871
  const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
802
872
  const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
873
+ // #4683: computed once — see the threat_id_duplicates fields in the payload.
874
+ const threatIdDuplicates = findDuplicateThreatIds(cwd, phaseInfo?.['directory']);
803
875
  const result = {
804
876
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
805
877
  verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
@@ -819,6 +891,13 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
819
891
  ? toPosixPath(node_path_1.default.join(cwd, phaseInfo['directory']))
820
892
  : null,
821
893
  phase_number: phaseInfo?.['phase_number'] || null,
894
+ // #4748: the disk path hands back the directory's padded number (`03A`)
895
+ // but the ROADMAP fallback above hands back the heading's bare one (`3A`),
896
+ // and execute-phase.md's review lookup needs the padded form for
897
+ // `{PADDED}-REVIEW.md`. It used to re-pad in shell with `printf "%02d"`,
898
+ // which cannot pad a letter id and reads an already-padded `08` as octal.
899
+ // Emit the canonical normalization, as the plan-phase/code-review inits do.
900
+ padded_phase: phaseInfo?.['phase_number'] ? normalizePhaseName(phaseInfo['phase_number']) : null,
822
901
  // #3171: prefer the ROADMAP's curated display name for `phase_name`. When
823
902
  // the phase directory already exists on disk, the disk-lookup path
824
903
  // (searchPhaseInDir) derives phase_name from the directory-name remainder
@@ -837,6 +916,12 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
837
916
  incomplete_plans: phaseInfo?.['incomplete_plans'] || [],
838
917
  plan_count: phaseInfo?.['plans']?.length || 0,
839
918
  incomplete_count: phaseInfo?.['incomplete_plans']?.length || 0,
919
+ // #4683: cross-plan threat-ID collisions (gap-closure plans renumbering
920
+ // from T-{phase}-01 again). execute-phase.md hard-stops on a non-empty
921
+ // list BEFORE any dispatch — SECURITY.md rows and VALIDATION.md's Threat
922
+ // Ref column key on this ID, so a reused ID is ambiguous downstream.
923
+ threat_id_duplicates: threatIdDuplicates,
924
+ threat_id_duplicate_count: threatIdDuplicates.length,
840
925
  // #2830: the halt-aware view, forwarded from the shared computation in
841
926
  // phase-locator. Additive — `incomplete_plans`/`incomplete_count` above keep
842
927
  // their exact name, type and semantics. Without this passthrough the shared
@@ -930,9 +1015,14 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
930
1015
  has_reviews: false,
931
1016
  };
932
1017
  });
933
- const reqMatch = roadmapPhase?.['section']?.match(REQUIREMENTS_HEADER_RE);
934
- const reqExtracted = reqMatch
935
- ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
1018
+ // #4731: multiline-aware — the Requirements field may hard-wrap, so the
1019
+ // value is extracted past the line break before the ID scan.
1020
+ const phaseSection = roadmapPhase?.['section'];
1021
+ const reqLine = phaseSection
1022
+ ? roadmapParser.extractPhaseFieldMultiline(phaseSection, 'Requirements')
1023
+ : null;
1024
+ const reqExtracted = reqLine
1025
+ ? reqLine.replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
936
1026
  : null;
937
1027
  const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
938
1028
  const phaseDirPlan = phaseInfo?.['directory'] || null;
@@ -952,6 +1042,8 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
952
1042
  }
953
1043
  const granularityOverride = options['granularity'];
954
1044
  assertValidGranularityOverride(granularityOverride, error);
1045
+ // #4683: computed once — see the threat_id_duplicates fields in the payload.
1046
+ const threatIdDuplicatesPlan = findDuplicateThreatIds(cwd, phaseDirPlan);
955
1047
  const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
956
1048
  // #3188: see cmdInitExecutePhase — null when absent, parity with the
957
1049
  // conditional sibling fields in this same result object.
@@ -996,6 +1088,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
996
1088
  has_reviews: phaseInfo?.['has_reviews'] || false,
997
1089
  has_plans: (phaseInfo?.['plans']?.length || 0) > 0,
998
1090
  plan_count: phaseInfo?.['plans']?.length || 0,
1091
+ // #4683: the same cross-plan threat-ID duplicate list execute-phase gates
1092
+ // on, surfaced at PLAN time so the checker/reviewer catches the collision
1093
+ // before the plans are approved — not just before execution.
1094
+ threat_id_duplicates: threatIdDuplicatesPlan,
1095
+ threat_id_duplicate_count: threatIdDuplicatesPlan.length,
999
1096
  planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
1000
1097
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1001
1098
  // #2376: absolute — see comment on phase_dir above.
@@ -1336,7 +1433,7 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1336
1433
  // would otherwise silently delete a stale/invalid pointer as a side effect
1337
1434
  // of building a JSON report field, and (per #3579) could change what a
1338
1435
  // LATER resolution in the same process observes.
1339
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
1436
+ const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
1340
1437
  const workstreamActive = !!resolvedWorkstream;
1341
1438
  const flatMode = !workstreamActive;
1342
1439
  // #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
@@ -2225,14 +2322,16 @@ function cmdInitMilestoneOp(cwd, raw) {
2225
2322
  const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
2226
2323
  const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
2227
2324
  const currentSection = extractCurrentMilestone(roadmapRaw, cwd);
2228
- // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
2229
- const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
2325
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
2326
+ // buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
2327
+ // bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
2328
+ const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, resolvePhaseIdConvention(cwd));
2230
2329
  let m;
2231
2330
  while ((m = phasePattern.exec(currentSection)) !== null) {
2232
2331
  // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
2233
- if (isSentinelPhaseId(m[1]))
2332
+ if (isSentinelPhaseId(m[phaseNumGroup]))
2234
2333
  continue;
2235
- roadmapPhaseNumbers.push(m[1]);
2334
+ roadmapPhaseNumbers.push(m[phaseNumGroup]);
2236
2335
  }
2237
2336
  }
2238
2337
  catch {
@@ -2375,6 +2474,10 @@ function cmdInitManager(cwd, raw) {
2375
2474
  const config = loadConfig(cwd);
2376
2475
  const milestone = milestoneRecord(cwd);
2377
2476
  const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
2477
+ const phaseIdConvention = resolvePhaseIdConvention(cwd);
2478
+ const capturesBracketId = phaseIdConvention === 'bracket';
2479
+ const phaseHeadingPrefix = phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, phaseIdConvention, capturesBracketId);
2480
+ const phaseHeadingPrefixNoCapture = phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, phaseIdConvention);
2378
2481
  const paths = planningPaths(cwd);
2379
2482
  if (!node_fs_1.default.existsSync(paths.roadmap)) {
2380
2483
  error(`No ROADMAP.md found. Run ${(0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', _slashRuntime)} first.`);
@@ -2384,38 +2487,48 @@ function cmdInitManager(cwd, raw) {
2384
2487
  }
2385
2488
  const rawContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf-8');
2386
2489
  const content = extractCurrentMilestone(rawContent, cwd);
2387
- const phasesDir = paths.phases;
2388
2490
  // #3185 (ADR-3180 Decision 1): "which phase directories belong to the
2389
2491
  // CURRENT milestone" is the scoped question listMilestonePhaseDirs owns —
2390
2492
  // routed through it instead of a hand-rolled readdirSync + a separate
2391
2493
  // getMilestonePhaseFilter window check (which also never excluded
2392
2494
  // sentinels, unlike the owner).
2393
- const _phaseDirEntries = listMilestonePhaseDirs(phasesDir, { cwd }).value;
2394
2495
  const _checkboxStates = new Map();
2395
- const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2496
+ const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2396
2497
  let _cbMatch;
2397
2498
  while ((_cbMatch = _cbPattern.exec(content)) !== null) {
2398
- _checkboxStates.set(_cbMatch[2], _cbMatch[1].toLowerCase() === 'x');
2499
+ const phaseGroup = capturesBracketId ? 3 : 2;
2500
+ _checkboxStates.set(_cbMatch[phaseGroup], _cbMatch[1].toLowerCase() === 'x');
2399
2501
  }
2400
2502
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
2401
- const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
2503
+ const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
2402
2504
  const phases = [];
2403
2505
  let match;
2404
2506
  while ((match = phasePattern.exec(content)) !== null) {
2405
- const phaseNum = match[1];
2406
- const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
2507
+ const bracketId = capturesBracketId ? match[1] : undefined;
2508
+ const phaseNum = capturesBracketId ? match[2] : match[1];
2509
+ const phaseName = (capturesBracketId ? match[3] : match[2])
2510
+ .replace(/\(INSERTED\)/i, '')
2511
+ .trim();
2512
+ let displayId;
2513
+ if (bracketId) {
2514
+ try {
2515
+ displayId = renderPhaseId(parsePhaseId(`${bracketId}-${phaseNum}`));
2516
+ }
2517
+ catch {
2518
+ // The bracket selector is deliberately read-tolerant. If a heading is
2519
+ // non-canonical, retain the manager row without fabricating display_id.
2520
+ }
2521
+ }
2407
2522
  const sectionStart = match.index;
2408
2523
  const restOfContent = content.slice(sectionStart);
2409
- const nextHeader = restOfContent.match(/\n#{2,4}\s+Phase\s+\d[\d.]*/i);
2524
+ const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}\\d[\\d.]*`, 'i'));
2410
2525
  const sectionEnd = nextHeader
2411
2526
  ? sectionStart + nextHeader.index
2412
2527
  : content.length;
2413
2528
  const section = content.slice(sectionStart, sectionEnd);
2414
- const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
2415
- const goal = goalMatch ? goalMatch[1].trim() : null;
2529
+ const goal = roadmapParser.extractPhaseFieldMultiline(section, 'Goal');
2416
2530
  const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
2417
2531
  const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
2418
- const normalized = normalizePhaseName(phaseNum);
2419
2532
  let diskStatus = 'no_directory';
2420
2533
  let planCount = 0;
2421
2534
  let summaryCount = 0;
@@ -2428,14 +2541,23 @@ function cmdInitManager(cwd, raw) {
2428
2541
  let contextScope = SCOPE.COMPLETE;
2429
2542
  let completion = buildPhaseCompletionProjection(cwd, phaseNum, null, planCount, summaryCount, _slashRuntime);
2430
2543
  try {
2431
- // #3185 (ADR-3180 Decision 2) moved this lookup off the
2432
- // milestone-scoped set and onto the physical one; that scope choice is
2433
- // kept. Only the matcher is this PR's: matchPhaseDirs resolves
2434
- // digit-leading directory names the token predicate cannot (#2528).
2435
- const dirMatch = matchPhaseDirs(_phaseDirEntries, normalized).matches[0];
2544
+ // #4801: resolve through the canonical locator instead of a private
2545
+ // current-milestone-only scan. findPhaseInternal searches the live
2546
+ // .planning/phases directory FIRST (same set the retired
2547
+ // matchPhaseDirs/_phaseDirEntries pair scanned — the #3185 physical-dir
2548
+ // scope choice is inherited by the locator's live arm) and then falls
2549
+ // back through listArchiveVersionDirs (workstream-scoped, #2855), so an
2550
+ // ARCHIVED phase directory with a passing verification resolves instead
2551
+ // of reporting no_directory/phase_complete:false. Five other init
2552
+ // commands already route through this same primitive; this was the
2553
+ // holdout private copy (#4793-family declared-owner drift).
2554
+ const located = findPhaseInternal(cwd, phaseNum, phaseIdConvention);
2555
+ const dirMatch = located && located['found']
2556
+ ? node_path_1.default.posix.basename(String(located['directory']))
2557
+ : null;
2436
2558
  if (dirMatch) {
2437
- const fullDir = node_path_1.default.join(phasesDir, dirMatch);
2438
- const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, fullDir));
2559
+ const fullDir = node_path_1.default.join(cwd, String(located['directory']));
2560
+ const phaseDirRel = String(located['directory']);
2439
2561
  // #4014 (epic #3473 B4-unreadable): this whole block used to swallow
2440
2562
  // ANY readdirSync failure below into the bare `catch { /* empty */ }`
2441
2563
  // at the bottom — an unreadable phase directory reported the exact
@@ -2509,6 +2631,7 @@ function cmdInitManager(cwd, raw) {
2509
2631
  const roadmapComplete = _checkboxStates.get(phaseNum) || false;
2510
2632
  phases.push({
2511
2633
  number: phaseNum,
2634
+ ...(displayId ? { display_id: displayId } : {}),
2512
2635
  name: phaseName,
2513
2636
  goal,
2514
2637
  depends_on,
@@ -2549,7 +2672,7 @@ function cmdInitManager(cwd, raw) {
2549
2672
  .filter((p) => p['phase_complete'] === true)
2550
2673
  .map((p) => normalizePhaseNumber(p['number'])));
2551
2674
  const phaseMap = new Map(phases.map((p) => [normalizePhaseNumber(p['number']), p]));
2552
- const _allCompletedPattern = new RegExp(`-\\s*\\[x\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2675
+ const _allCompletedPattern = new RegExp(`-\\s*\\[x\\]\\s*.*${phaseHeadingPrefixNoCapture}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2553
2676
  let _allMatch;
2554
2677
  while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
2555
2678
  const phaseNum = normalizePhaseNumber(_allMatch[1]);
@@ -2575,13 +2698,47 @@ function cmdInitManager(cwd, raw) {
2575
2698
  function hasDepRelationship(numA, numB) {
2576
2699
  return reaches(numA, numB) || reaches(numB, numA);
2577
2700
  }
2701
+ // #4764: a phase reference in depends_on prose is a PHASE-SHAPED token in
2702
+ // context — directly following "Phase"/"Phases" — never a bare digit run.
2703
+ // The previous whole-field scrape matched the token grammar against every
2704
+ // digit run, so calendar dates ("2026-09-14" → 2026, 09, 14), git shas
2705
+ // ("8bf403100d" → 8b, 403100d, …), bracketed ledger ids (WINDOWS #1843) and
2706
+ // the row's OWN number all became "dependencies", and deps_satisfied came
2707
+ // back false for phases whose prose declares none (50 of 92 phases in the
2708
+ // reporter's milestone). The anchored grammar (owned by phase-id.cts as
2709
+ // PHASE_DEP_REF_SOURCE, shared with planning-inspect's dependencies) keeps
2710
+ // lists fully extracted ("Phases 601 and 602", "Phase 601, 602, and 603",
2711
+ // "Phase 1-3") — silently dropping a REAL dependency would clear
2712
+ // deps_satisfied prematurely, the dangerous direction. Negation prose
2713
+ // ("dropped the dependency on Phase 654") is NOT detected: the issue's own
2714
+ // minimum keeps such tokens.
2715
+ const depPhaseRefRe = new RegExp(`${PHASE_DEP_REF_SOURCE}`, 'gi');
2716
+ const depTokenRe = new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi');
2578
2717
  for (const phase of phases) {
2579
2718
  if (!phase['depends_on'] ||
2580
2719
  /^none$/i.test(phase['depends_on'].trim())) {
2581
2720
  phase['deps_satisfied'] = true;
2582
2721
  }
2583
2722
  else {
2584
- const depNums = phase['depends_on'].match(new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi')) || [];
2723
+ const prose = phase['depends_on'];
2724
+ const ownNumber = normalizePhaseNumber(phase['number']);
2725
+ const depNums = [];
2726
+ const seen = new Set();
2727
+ let refMatch;
2728
+ depPhaseRefRe.lastIndex = 0;
2729
+ while ((refMatch = depPhaseRefRe.exec(prose)) !== null) {
2730
+ let tok;
2731
+ depTokenRe.lastIndex = 0;
2732
+ while ((tok = depTokenRe.exec(refMatch[1])) !== null) {
2733
+ const normalized = normalizePhaseNumber(tok[0]);
2734
+ if (normalized === ownNumber)
2735
+ continue; // #4764: never the row's own phase
2736
+ if (seen.has(normalized))
2737
+ continue;
2738
+ seen.add(normalized);
2739
+ depNums.push(tok[0]);
2740
+ }
2741
+ }
2585
2742
  phase['deps_satisfied'] = depNums.every((n) => completedNums.has(normalizePhaseNumber(n)));
2586
2743
  phase['dep_phases'] = depNums;
2587
2744
  }
@@ -2921,7 +3078,7 @@ function cmdInitUpdate(cwd, raw, options = {}) {
2921
3078
  function cmdInitTransition(cwd, raw, options = {}) {
2922
3079
  // #3579 root-cause fix: read-only informational field — peek, don't
2923
3080
  // self-heal (see cmdInitNewMilestone's identical rationale above).
2924
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
3081
+ const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
2925
3082
  const workstreamActive = !!resolvedWorkstream;
2926
3083
  const result = {
2927
3084
  other_active_workstreams: workstreamActive
@@ -3013,7 +3170,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
3013
3170
  // non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
3014
3171
  // here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
3015
3172
  // which would misreport a present-but-bad marker as no marker at all.
3016
- const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
3173
+ const _resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
3017
3174
  if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) {
3018
3175
  // #3579: getActiveWorkstream now inherits a pointer-less session's read
3019
3176
  // from the shared .planning/active-workstream marker, so reaching this
@@ -3040,13 +3197,22 @@ function cmdInitProgress(cwd, raw, options = {}) {
3040
3197
  const roadmapCheckboxStates = new Map();
3041
3198
  try {
3042
3199
  const roadmapContent = extractCurrentMilestone(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
3043
- // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
3044
- const headingPattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
3200
+ const progressConvention = resolvePhaseIdConvention(cwd);
3201
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
3202
+ // buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
3203
+ // bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
3204
+ const { regex: headingPattern, phaseNumGroup: hNumGroup, phaseNameGroup: hNameGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, progressConvention);
3045
3205
  let hm;
3046
3206
  while ((hm = headingPattern.exec(roadmapContent)) !== null) {
3047
- roadmapPhaseNums.add(hm[1]);
3048
- roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
3049
- }
3207
+ roadmapPhaseNums.add(hm[hNumGroup]);
3208
+ roadmapPhaseNames.set(hm[hNumGroup], hm[hNameGroup].replace(/\(INSERTED\)/i, '').trim());
3209
+ }
3210
+ // #4906 Phase 5 (#4984): NOT migrated onto phaseHeadingPrefixSrcFor — same
3211
+ // out-of-scope reasoning as roadmapPhaseMatchesExactPrefix/detectPhaseMvpMode
3212
+ // above; a direct LABEL_ONLY selector call here would have added a third
3213
+ // uncounted LABEL_ONLY consumer to init.cts's pinned
3214
+ // tests/adr-612-bracket-heading-selection.test.cjs census.
3215
+ // phase-id-owner: deliberately unmigrated (#4984 revert) — see comment above.
3050
3216
  const cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
3051
3217
  let cbm;
3052
3218
  while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
@@ -730,6 +730,16 @@ function _copyStaged(stagedDir, destDir, kind, configDir, runtime) {
730
730
  continue;
731
731
  if (!entry.name.endsWith('.md'))
732
732
  continue;
733
+ // #4782: compact agent variants are consumed ONLY through the non-claude
734
+ // gate in init.cts's agent-skills persona fallback (`runtime !== 'claude'`
735
+ // — claude's contract is a skills-injection path, never a persona
736
+ // fallback). Staging them into Claude's agents directory shipped 29 dead
737
+ // files whose `name:` frontmatter is identical to their canonical
738
+ // sibling's, leaving the harness resolution unstated. Claude never
739
+ // selects compact, so claude is the one runtime whose agents kind skips
740
+ // them; every other runtime's emission is byte-identical.
741
+ if (kind.kind === 'agents' && runtime === 'claude' && entry.name.endsWith('.compact.md'))
742
+ continue;
733
743
  const stem = entry.name.slice(0, -3); // strip .md
734
744
  let destName;
735
745
  if (kind.kind === 'agents') {
@@ -1881,6 +1891,9 @@ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfi
1881
1891
  isWindowsHost: process.platform === 'win32',
1882
1892
  resolvedTarget: (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(configDir)),
1883
1893
  homeDir: (0, shell_command_projection_cjs_1.posixNormalize)(node_os_1.default.homedir()),
1894
+ // #4377: the runtime's own localConfigDir, so an opted-in local install
1895
+ // emits `<dir>/...` instead of this checkout's absolute path.
1896
+ localDirName: runtimeArtifactConversion._localIncludeDirName(runtime),
1884
1897
  });
1885
1898
  // #2329: destDir is derived from the SAME hostBehaviors.flatCommandDir
1886
1899
  // descriptor value read by writeManifest's manifest-key prefix and by
@@ -280,7 +280,14 @@ function readInstallManifest(configDir) {
280
280
  version: typeof m.version === 'string' ? m.version : null,
281
281
  timestamp: typeof m.timestamp === 'string' ? m.timestamp : null,
282
282
  mode: typeof m.mode === 'string' ? m.mode : null,
283
- files: m.files && typeof m.files === 'object' ? m.files : {},
283
+ // #4544 (review): `typeof [] === 'object'` — a manifest whose `files` is a
284
+ // JSON array passed the object-shape guard, and Object.keys() then yielded
285
+ // "0","1",... as install-relative file paths. Consumers iterate these keys,
286
+ // so an array shape must degrade to the empty set exactly like a
287
+ // non-object shape does.
288
+ files: m.files && typeof m.files === 'object' && !Array.isArray(m.files)
289
+ ? m.files
290
+ : {},
284
291
  manifestVersion: normalizeManifestVersion(m.manifestVersion),
285
292
  runtime: normalizeReportedRuntime(rawRuntime),
286
293
  scope: (0, install_scope_cjs_1.isInstallScopeId)(m.scope) ? m.scope : null,
@@ -38,7 +38,7 @@ const stateContract = require("./state-contract.cjs");
38
38
  const { publishStateContract } = stateContract;
39
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
40
  const phaseIdMod = require("./phase-id.cjs");
41
- const { normalizePhaseName, matchPhaseDirs, PHASE_NUMBER_TOKEN_SOURCE, isSentinelPhaseId, isSentinelPhaseDir } = phaseIdMod;
41
+ const { normalizePhaseName, matchPhaseDirs, isSentinelPhaseId, isSentinelPhaseDir, buildPhaseHeadingScanRegex, PHASE_HEADING_BASELINE } = phaseIdMod;
42
42
  const pattern_cjs_1 = require("./pattern.cjs");
43
43
  // eslint-disable-next-line @typescript-eslint/no-require-imports
44
44
  const roadmapParserMod = require("./roadmap-parser.cjs");
@@ -642,8 +642,21 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
642
642
  // windows converge to the same value regardless of which version drove
643
643
  // the lookup.
644
644
  const scopedContent = sliceMilestoneWindow(roadmapContent, version) ?? extractCurrentMilestone(roadmapContent, cwd);
645
- // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
646
- const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
645
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned
646
+ // by buildPhaseHeadingScanRegex (phase-id.cts) so this guard also
647
+ // recognizes bracket-convention headings instead of hand-rolling a
648
+ // literal `Phase\s+`.
649
+ // #4984 fix: resolved ONCE and threaded into BOTH the heading scan and
650
+ // the disk-side matchPhaseDirs check below — the heading scan alone
651
+ // recognizing `[GSD.02] 01:` and extracting the bare phase-number token
652
+ // "01" is only half the fix. Without also passing this convention to
653
+ // matchPhaseDirs, its bracket-qualified match arm never fires, "01"
654
+ // matches no bracket directory name ("GSD.02-01-setup"), and every
655
+ // properly-scaffolded bracket phase reads as disk_status: 'no_directory'
656
+ // — turning a real bracket project's own phases into false "ROADMAP
657
+ // lists N unstarted phase(s)" failures on `milestone complete`.
658
+ const convention = resolvePhaseIdConvention(cwd);
659
+ const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, convention);
647
660
  const noDirectoryPhases = [];
648
661
  let pm;
649
662
  const phaseDirEntries = (() => {
@@ -658,7 +671,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
658
671
  }
659
672
  })();
660
673
  while ((pm = phasePattern.exec(scopedContent)) !== null) {
661
- const phaseNum = pm[1];
674
+ const phaseNum = pm[phaseNumGroup];
662
675
  // Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not
663
676
  // real phases — they legitimately have no directory and must not block
664
677
  // milestone completion. Mirrors the engine-wide sentinel convention
@@ -672,7 +685,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
672
685
  // with a matching token exists on disk. Use the same matchPhaseDirs
673
686
  // owner that roadmap.analyze uses to avoid false positives on decimal
674
687
  // (2.1) and letter-suffix (12A) phase IDs. (#2528)
675
- const hasDirectory = matchPhaseDirs(phaseDirEntries, normalized).matches.length > 0;
688
+ const hasDirectory = matchPhaseDirs(phaseDirEntries, normalized, convention).matches.length > 0;
676
689
  if (!hasDirectory) {
677
690
  noDirectoryPhases.push(phaseNum);
678
691
  }