@opengsd/gsd-core 1.10.0 → 1.11.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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -14,6 +14,7 @@ const node_path_1 = __importDefault(require("node:path"));
14
14
  const node_os_1 = __importDefault(require("node:os"));
15
15
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
16
16
  const clock_cjs_1 = require("./clock.cjs");
17
+ const pattern_cjs_1 = require("./pattern.cjs");
17
18
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
18
19
  const io = require("./io.cjs");
19
20
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
@@ -38,6 +39,7 @@ const secrets_cjs_1 = require("./secrets.cjs");
38
39
  const scanPhasePlans = require("./plan-scan.cjs");
39
40
  const state_document_cjs_1 = require("./state-document.cjs");
40
41
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
42
+ const host_runtime_detection_cjs_1 = require("./host-runtime-detection.cjs");
41
43
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- commands.cjs is an export= CommonJS module
42
44
  const commandsMod = require("./commands.cjs");
43
45
  const security_cjs_1 = require("./security.cjs");
@@ -72,18 +74,18 @@ const resolution_cjs_1 = require("./resolution.cjs");
72
74
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- onboard-projection.cjs is an export= CommonJS module
73
75
  const onboardProjection = require("./onboard-projection.cjs");
74
76
  const { REQUIRED_CODEBASE_MAP_FILES, buildOnboardProjection, hasCodeFilesInternal, hasPackageFileInternal, listCodebaseMapFiles, } = onboardProjection;
75
- const { output, error } = io;
77
+ const { output, error, ERROR_REASON } = io;
76
78
  const { loadConfig, loadConfigResolved } = configLoader;
77
79
  const { resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride } = modelResolver;
78
- const { findPhaseInternal } = phaseLocator;
79
- const { getRoadmapPhaseInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
80
+ const { findPhaseInternal, listMilestonePhaseDirs } = phaseLocator;
81
+ const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
80
82
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
81
- const { escapeRegex, normalizePhaseName, phaseTokenMatches, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery } = phaseId;
83
+ const { normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase } = phaseId;
82
84
  const { pruneOrphanedWorktrees } = worktreeSafety;
83
- const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, getActiveWorkstream, findContextMdIn, } = planningWorkspace;
85
+ const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
84
86
  const { determinePhaseStatus } = commandsMod;
85
87
  const { extractFrontmatter } = frontmatterMod;
86
- const { readVerificationStatus } = verificationMod;
88
+ const { isPhaseComplete, resolveVerificationFile, resolveUatFile } = verificationMod;
87
89
  const { evaluateUatPassed } = uatPredicateMod;
88
90
  const { resolveLoopHooks } = loopResolverMod;
89
91
  const { loadRegistry } = capabilityLoaderMod;
@@ -104,7 +106,7 @@ function phaseInfoMatchesExactPrefix(phaseInfo, phase) {
104
106
  function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
105
107
  const sectionRaw = roadmapPhase?.['section'];
106
108
  const section = typeof sectionRaw === 'string' ? sectionRaw : '';
107
- return new RegExp(`^#{2,4}\\s*Phase\\s+${escapeRegex(phase)}(?:\\b|\\s|:)`, 'i').test(section);
109
+ return new RegExp(`^#{2,4}\\s*Phase\\s+${(0, pattern_cjs_1.escapeRegex)(phase)}(?:\\b|\\s|:)`, 'i').test(section);
108
110
  }
109
111
  // #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
110
112
  // with the #2056 foreign-prefix guard, so every init command gets the same
@@ -170,39 +172,49 @@ function listPhaseSummaryFiles(phaseDir) {
170
172
  function listPhasePlanFiles(phaseDir) {
171
173
  return scanPhasePlans(phaseDir)['planFiles'];
172
174
  }
173
- function projectCompletionStatus(implementationComplete, verificationPassed) {
174
- if (implementationComplete && verificationPassed)
175
+ function projectCompletionStatus(implementationComplete, phaseComplete) {
176
+ if (phaseComplete)
175
177
  return 'complete';
176
178
  if (implementationComplete)
177
179
  return 'executed';
178
180
  return 'incomplete';
179
181
  }
180
182
  function buildPhaseCompletionProjection(cwd, phaseNumber, phaseDir, planCount, summaryCount, slashRuntime) {
183
+ // ADR-3180 §7.4 (issue #3186) / DO-NOT-MIGRATE exemption
184
+ // (scripts/lint-completion-predicate-drift.cjs FUNCTION_SCOPED_EXEMPTIONS,
185
+ // declared deviation): `implementation_complete` answers "are the plans
186
+ // done" (a `scanPhasePlans`-shaped different question, per the design's
187
+ // 0.x-split), NOT "is the phase complete" — it is kept for the
188
+ // 'executed'-vs-'planned' disk_status distinction downstream consumers
189
+ // still rely on, which `isPhaseComplete`'s locked `{ complete, verification
190
+ // }` return shape does not carry.
181
191
  const implementationComplete = planCount > 0 && summaryCount >= planCount;
182
192
  const phaseFullDir = phaseDir ? node_path_1.default.join(cwd, phaseDir) : '';
183
- // #2617: ONE verification-routing seam. init used to re-derive next_command
184
- // from the status with its own projector, which had drifted from the router's
185
- // table — it appended the phase number and answered `human_needed`; the table
186
- // did neither. The router now owns both the content and the runtime
187
- // projection, and init passes the phase number it already knows (its phaseDir
193
+ // #3168 / ADR-3180 §7.4 (disk-strict, #2957): route through the canonical
194
+ // owner (`src/verification.cts` · `isPhaseComplete`), which calls
195
+ // readVerificationStatus UNCONDITIONALLY — plan count is NOT a
196
+ // precondition. A zero-plan phase with a passing `*-VERIFICATION.md` is
197
+ // complete; init used to gate the read on `implementationComplete` and
198
+ // synthesize a `not_required` sentinel instead, which is the #3168 defect.
199
+ // #2617: the router still owns both the message content and the runtime
200
+ // projection; init passes the phase number it already knows (its phaseDir
188
201
  // is unresolved in some branches, where the router could not derive one).
189
- const verificationStatus = implementationComplete
190
- ? readVerificationStatus(phaseFullDir, { runtime: slashRuntime, phaseNumber })
191
- : { status: 'not_required', next_action: '', next_command: '' };
202
+ const completionResult = isPhaseComplete(phaseFullDir, { runtime: slashRuntime, phaseNumber });
203
+ const verificationStatus = completionResult.value.verification;
192
204
  const projectedVerificationStatus = verificationStatus.status;
193
205
  const projectedVerificationAction = verificationStatus.next_action;
194
206
  const verificationPassed = projectedVerificationStatus === 'passed';
195
- const phaseComplete = implementationComplete && verificationPassed;
207
+ const phaseComplete = completionResult.value.complete;
196
208
  return {
197
209
  implementation_complete: implementationComplete,
198
210
  verification_status: projectedVerificationStatus,
199
211
  verification_passed: verificationPassed,
200
212
  phase_complete: phaseComplete,
201
- completion_status: projectCompletionStatus(implementationComplete, verificationPassed),
213
+ completion_status: projectCompletionStatus(implementationComplete, phaseComplete),
202
214
  verification_next_action: projectedVerificationAction,
203
215
  verification_next_command: verificationStatus.next_command,
204
- // #3057 B3: only readVerificationStatus's result ever carries this flag —
205
- // the `not_required` synthetic object above never does.
216
+ // #3057 B3: readVerificationStatus's result carries this flag when its
217
+ // internal staleness check could not run to completion.
206
218
  verification_stale_check_indeterminate: 'staleCheckIndeterminate' in verificationStatus
207
219
  && verificationStatus.staleCheckIndeterminate === true,
208
220
  };
@@ -222,7 +234,8 @@ function getLatestCompletedMilestone(cwd) {
222
234
  }
223
235
  function withProjectRoot(cwd, result) {
224
236
  result['project_root'] = cwd;
225
- const activeRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
237
+ // #3245: the reported agent_runtime gets a host-detection rung below the two explicit sources; every other resolveRuntime caller keeps the old ladder (ADR-2313 scope boundary).
238
+ const activeRuntime = (0, host_runtime_detection_cjs_1.resolveReportedRuntime)(cwd);
226
239
  const agentStatus = checkAgentsInstalled(activeRuntime, cwd);
227
240
  result['agents_installed'] = agentStatus.agents_installed;
228
241
  result['missing_agents'] = agentStatus.missing_agents;
@@ -415,7 +428,12 @@ function detectHasPriorPhases(cwd, phaseInfo) {
415
428
  catch {
416
429
  continue;
417
430
  }
418
- if (files.some((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md')) {
431
+ // #3511-class: scope the raw listing to THIS entry's own phase artifacts
432
+ // before the bare `.some()` predicate runs, so a stray `07-VERIFICATION.md`
433
+ // physically sitting in another phase's directory cannot make that
434
+ // directory appear to have its own verification report.
435
+ const scopedFiles = scopeToPhase(files, entry.name);
436
+ if (scopedFiles.some((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md')) {
419
437
  return true;
420
438
  }
421
439
  }
@@ -537,7 +555,7 @@ function detectPhaseMvpMode(cwd, phaseNumber) {
537
555
  return false;
538
556
  const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
539
557
  const content = extractCurrentMilestone(rawContent, cwd);
540
- const escapedPhase = escapeRegex(phaseNumber);
558
+ const escapedPhase = (0, pattern_cjs_1.escapeRegex)(phaseNumber);
541
559
  const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
542
560
  const headerMatch = content.match(phaseHeader);
543
561
  if (!headerMatch || headerMatch.index === undefined)
@@ -594,7 +612,11 @@ function detectUiPhaseActive(cwd, phaseInfo) {
594
612
  // comments elsewhere in this file), same technique as detectHasPriorPhases above.
595
613
  const dirName = node_path_1.default.basename(rawDir);
596
614
  const files = node_fs_1.default.readdirSync(node_path_1.default.join(planningDir(cwd), 'phases', dirName));
597
- hasUiSpecFile = files.some((f) => f.endsWith('-UI-SPEC.md') || f === 'UI-SPEC.md');
615
+ // #3511-class: scope the raw listing to this phase dir before the
616
+ // phase-numbered -UI-SPEC.md predicate, so a stray cross-phase
617
+ // UI-SPEC file cannot flip this phase's ui-phase-active flag.
618
+ const scopedFiles = scopeToPhase(files, dirName);
619
+ hasUiSpecFile = scopedFiles.some((f) => f.endsWith('-UI-SPEC.md') || f === 'UI-SPEC.md');
598
620
  }
599
621
  catch {
600
622
  hasUiSpecFile = false;
@@ -691,13 +713,32 @@ function buildSectionManifestField(cwd, phaseInfo, options, workflow, overrides
691
713
  return null;
692
714
  }
693
715
  }
716
+ /**
717
+ * #3216 review Finding 1: `getMilestoneInfo(cwd).value` unwrap-and-cast was
718
+ * repeated identically (comment included) at five init call sites — factored
719
+ * out once so the cast and its `?? {}` "no milestone resolved" fallback live
720
+ * in exactly one place. Behavior-preserving: same call, same fallback, same
721
+ * cast, for every caller.
722
+ */
723
+ function milestoneRecord(cwd) {
724
+ return (getMilestoneInfo(cwd).value ?? {});
725
+ }
694
726
  function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
695
727
  if (!phase) {
696
728
  error('phase required for init execute-phase');
697
729
  }
698
730
  const config = loadConfig(cwd);
699
731
  let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
700
- const milestone = getMilestoneInfo(cwd);
732
+ // #3216: getMilestoneInfo now returns a ScopedResult — `.value` carries the
733
+ // MilestoneInfo (or null on any non-COMPLETE scope). NOT display-only: when
734
+ // `branching_strategy === 'milestone'`, `milestone['version']`/`['name']`
735
+ // below feed `branch_name` construction (see the milestone_branch_template
736
+ // branch below), so an unresolved milestone changes the constructed branch
737
+ // name, not merely what gets printed. bracket-access below naturally reads
738
+ // `undefined` when unresolved; the `milestone_version`/`milestone_name`
739
+ // output fields below coerce that to an explicit `null` (#3216 review
740
+ // Finding 2) so the key is never silently omitted from the JSON bundle.
741
+ const milestone = milestoneRecord(cwd);
701
742
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
702
743
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
703
744
  const phaseName = rp['phase_name'];
@@ -725,6 +766,13 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
725
766
  : null;
726
767
  const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
727
768
  const wf = (config.workflow ?? {});
769
+ // #3188: these paths are null when the file is absent, matching the contract
770
+ // the conditional sibling fields (context_path, patterns_path, ...) already
771
+ // honour and that ultraplan-phase.md / execute-phase.md gate on. Hoisted so
772
+ // the existence check and the emitted path share one source of truth.
773
+ const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
774
+ const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
775
+ const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
728
776
  const result = {
729
777
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
730
778
  verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
@@ -744,7 +792,17 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
744
792
  ? toPosixPath(node_path_1.default.join(cwd, phaseInfo['directory']))
745
793
  : null,
746
794
  phase_number: phaseInfo?.['phase_number'] || null,
747
- phase_name: phaseInfo?.['phase_name'] || null,
795
+ // #3171: prefer the ROADMAP's curated display name for `phase_name`. When
796
+ // the phase directory already exists on disk, the disk-lookup path
797
+ // (searchPhaseInDir) derives phase_name from the directory-name remainder
798
+ // — itself an already-slugified value (`phase.add` writes `${num}-${slug}`
799
+ // dirs), so phase_name and phase_slug come out byte-identical. An
800
+ // orchestrator wiring this field into `state begin-phase --name` then
801
+ // lands a raw slug in STATE.md's current_phase_name. The ROADMAP carries
802
+ // the human-curated display name (`### Phase N: <Name>`); prefer it,
803
+ // matching the no-disk fallback above. phase_slug stays disk-derived — it
804
+ // correctly feeds branch-name construction below and is unchanged here.
805
+ phase_name: (roadmapPhase?.['phase_name']) || (phaseInfo?.['phase_name']) || null,
748
806
  phase_slug: phaseInfo?.['phase_slug'] || null,
749
807
  phase_req_ids,
750
808
  plans: phaseInfo?.['plans'] || [],
@@ -768,22 +826,23 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
768
826
  .replace('{slug}', phaseInfo['phase_slug'] || 'phase')
769
827
  : config.branching_strategy === 'milestone'
770
828
  ? config.milestone_branch_template
771
- .replace('{milestone}', milestone['version'])
829
+ .replace('{milestone}', milestone['version'] ?? '')
772
830
  .replace('{slug}', generateSlugInternal(milestone['name']) || 'milestone')
773
831
  : null,
774
- milestone_version: milestone['version'],
775
- milestone_name: milestone['name'],
832
+ milestone_version: milestone['version'] ?? null,
833
+ milestone_name: milestone['name'] ?? null,
776
834
  milestone_slug: generateSlugInternal(milestone['name']),
777
835
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
778
836
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
779
837
  config_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'config.json')),
780
838
  // #2376: emit absolute paths — see comment above on phase_dir.
781
- state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
782
- roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
839
+ // #3188: null when the file is absent (parity with patterns_path/context_path).
840
+ state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
841
+ roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
783
842
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
784
843
  // #2376: execute-phase.md's verify_phase_goal step reads this instead of
785
844
  // hardcoding '.planning/REQUIREMENTS.md' into the gsd-verifier spawn prompt.
786
- requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
845
+ requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
787
846
  };
788
847
  if (options['validate']) {
789
848
  try {
@@ -860,6 +919,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
860
919
  assertValidGranularityOverride(granularityOverride, error);
861
920
  const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
862
921
  const wf = (config.workflow ?? {});
922
+ // #3188: see cmdInitExecutePhase — null when absent, parity with the
923
+ // conditional sibling fields in this same result object.
924
+ const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
925
+ const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
926
+ const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
863
927
  const result = {
864
928
  researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
865
929
  planner_model: resolveModelInternal(cwd, 'gsd-planner'),
@@ -896,36 +960,70 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
896
960
  planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
897
961
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
898
962
  // #2376: absolute — see comment on phase_dir above.
899
- state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
900
- roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
901
- requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
963
+ // #3188: null when the file is absent (parity with patterns_path below).
964
+ state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
965
+ roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
966
+ requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
902
967
  patterns_path: null,
903
968
  };
904
969
  if (phaseInfo?.['directory']) {
905
970
  const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']);
906
971
  try {
907
972
  const files = node_fs_1.default.readdirSync(phaseDirFull);
908
- const contextFile = findContextMdIn(phaseDirFull);
973
+ const phaseDirName = node_path_1.default.basename(phaseDirFull);
974
+ // #3511 BLOCKER-3: scope the raw listing to THIS phase's own artifacts
975
+ // before any bare `.find()` predicate runs, so a `04-UAT.md` (or
976
+ // `04-RESEARCH.md`/`04-REVIEWS.md`/`04-PATTERNS.md`) sitting in phase
977
+ // 03's directory cannot win a phase-03 lookup — the same
978
+ // `isPhaseArtifact` membership rule `resolveVerificationFile` already
979
+ // applies via `phaseDirName` below. `findContextMdIn` is passed the
980
+ // scoped array (rather than the raw directory path) so this call site
981
+ // alone is scoped; its other call sites are unaffected.
982
+ const scopedFiles = scopeToPhase(files, phaseDirName);
983
+ const contextFile = findContextMdIn(scopedFiles);
909
984
  if (contextFile) {
910
985
  result['context_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, contextFile));
911
986
  }
912
- const researchFile = files.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
987
+ const researchFile = scopedFiles.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
913
988
  if (researchFile) {
914
989
  result['research_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, researchFile));
915
990
  }
916
- const verificationFile = files.find((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
991
+ // #3473 F2: routed through the shared resolver — readdir order is
992
+ // filesystem-dependent, so the prior hand-rolled `.find()` could pick
993
+ // either file when a phase held both a canonical report and an ad-hoc
994
+ // `-CORRECTION-VERIFICATION.md` worksheet (#3357).
995
+ // #3492: pin selection to THIS phase's own token so a stray cross-phase
996
+ // or sentinel-numbered canonically-shaped file cannot outrank this
997
+ // phase's own (possibly non-canonical) report.
998
+ const phaseToken = extractPhaseToken(phaseDirName);
999
+ const verificationFile = resolveVerificationFile(files, {
1000
+ allowBare: true,
1001
+ phaseToken,
1002
+ phaseDirName,
1003
+ });
917
1004
  if (verificationFile) {
918
1005
  result['verification_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, verificationFile));
919
1006
  }
920
- const uatFile = files.find((f) => f.endsWith('-UAT.md') || f === 'UAT.md');
1007
+ // #3518: routed through the shared UAT resolver — the prior hand-rolled
1008
+ // `.find()` over unsorted readdir order had no phase check and no
1009
+ // ordering, so a stray cross-phase 02-UAT.md could become this phase's
1010
+ // uat_path, filesystem-dependently. Pinned to this phase's own token
1011
+ // (same rule as verification_path above), and phase-scoped via
1012
+ // phaseDirName (#3511) so the alphabetically-first fallback tier also
1013
+ // excludes cross-phase strays.
1014
+ const uatFile = resolveUatFile(files, {
1015
+ allowBare: true,
1016
+ phaseToken,
1017
+ phaseDirName,
1018
+ });
921
1019
  if (uatFile) {
922
1020
  result['uat_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, uatFile));
923
1021
  }
924
- const reviewsFile = files.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
1022
+ const reviewsFile = scopedFiles.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
925
1023
  if (reviewsFile) {
926
1024
  result['reviews_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, reviewsFile));
927
1025
  }
928
- const patternsFile = files.find((f) => f.endsWith('-PATTERNS.md') || f === 'PATTERNS.md');
1026
+ const patternsFile = scopedFiles.find((f) => f.endsWith('-PATTERNS.md') || f === 'PATTERNS.md');
929
1027
  if (patternsFile) {
930
1028
  result['patterns_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, patternsFile));
931
1029
  }
@@ -1010,22 +1108,14 @@ function cmdInitNewProject(cwd, raw, options = {}) {
1010
1108
  }
1011
1109
  function cmdInitNewMilestone(cwd, raw, options = {}) {
1012
1110
  const config = loadConfig(cwd);
1013
- const milestone = getMilestoneInfo(cwd);
1111
+ const milestone = milestoneRecord(cwd);
1014
1112
  const latestCompleted = getLatestCompletedMilestone(cwd);
1015
1113
  const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
1016
- let phaseDirCount = 0;
1017
- try {
1018
- if (node_fs_1.default.existsSync(phasesDir)) {
1019
- const isDirInMilestone = getMilestonePhaseFilter(cwd);
1020
- phaseDirCount = node_fs_1.default
1021
- .readdirSync(phasesDir, { withFileTypes: true })
1022
- .filter((entry) => entry.isDirectory() && isDirInMilestone(entry.name))
1023
- .length;
1024
- }
1025
- }
1026
- catch {
1027
- /* intentionally empty */
1028
- }
1114
+ // #3185 (ADR-3180 Decision 1): "how many phase directories belong to the
1115
+ // CURRENT milestone" is exactly the scoped question listMilestonePhaseDirs
1116
+ // owns — routed through it instead of a local readdirSync + hand-rolled
1117
+ // window filter (which also never excluded sentinels, unlike the owner).
1118
+ const phaseDirCount = listMilestonePhaseDirs(phasesDir, { cwd }).value.length;
1029
1119
  const wf = (config.workflow ?? {});
1030
1120
  const result = {
1031
1121
  researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
@@ -1033,8 +1123,12 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1033
1123
  roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
1034
1124
  commit_docs: config.commit_docs,
1035
1125
  research_enabled: wf['research'],
1036
- current_milestone: milestone['version'],
1037
- current_milestone_name: milestone['name'],
1126
+ // #3216 review Finding 2: `?? null` so an unresolved milestone still emits
1127
+ // the key with an explicit `null` rather than letting JSON.stringify drop
1128
+ // it — an omitted key reaches the prompt layer's `{current_milestone}`
1129
+ // placeholder as literal, un-substituted text.
1130
+ current_milestone: milestone['version'] ?? null,
1131
+ current_milestone_name: milestone['name'] ?? null,
1038
1132
  latest_completed_milestone: latestCompleted?.version || null,
1039
1133
  latest_completed_milestone_name: latestCompleted?.name || null,
1040
1134
  phase_dir_count: phaseDirCount,
@@ -1070,7 +1164,13 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1070
1164
  // source as `cmdInitTransition`: `GSD_WORKSTREAM` env, falling back to the
1071
1165
  // stored active-workstream pointer (mirrors `cmdInitProgress`'s own
1072
1166
  // resolution above).
1073
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
1167
+ //
1168
+ // #3579 root-cause fix: this is a read-only informational field (no write
1169
+ // follows), so use the non-mutating peek — getActiveWorkstream's self-heal
1170
+ // would otherwise silently delete a stale/invalid pointer as a side effect
1171
+ // of building a JSON report field, and (per #3579) could change what a
1172
+ // LATER resolution in the same process observes.
1173
+ const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
1074
1174
  const workstreamActive = !!resolvedWorkstream;
1075
1175
  const flatMode = !workstreamActive;
1076
1176
  // #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
@@ -1082,7 +1182,7 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1082
1182
  }
1083
1183
  function cmdInitQuick(cwd, description, raw, options = {}) {
1084
1184
  const config = loadConfig(cwd);
1085
- const now = new Date();
1185
+ const now = new Date(clock_cjs_1.realClock.now());
1086
1186
  const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
1087
1187
  const yy = String(now.getFullYear()).slice(-2);
1088
1188
  const mm = String(now.getMonth() + 1).padStart(2, '0');
@@ -1548,6 +1648,11 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1548
1648
  expectedPhaseDir = toPosixPath(node_path_1.default.join(planningPaths(cwd).phases, dirName));
1549
1649
  }
1550
1650
  }
1651
+ // #3188: see cmdInitExecutePhase — null when absent, parity with the
1652
+ // conditional sibling fields in this same result object.
1653
+ const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
1654
+ const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
1655
+ const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
1551
1656
  const result = {
1552
1657
  commit_docs: config.commit_docs,
1553
1658
  brave_search: typeof config.brave_search === 'string'
@@ -1576,31 +1681,59 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1576
1681
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1577
1682
  planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
1578
1683
  // #2376: absolute — see comment on phase_dir above.
1579
- state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1580
- roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1581
- requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
1684
+ // #3188: null when the file is absent (parity with context_path/research_path).
1685
+ state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
1686
+ roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
1687
+ requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
1582
1688
  };
1583
1689
  if (phaseInfo?.['directory']) {
1584
1690
  const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']);
1585
1691
  try {
1586
1692
  const files = node_fs_1.default.readdirSync(phaseDirFull);
1587
- const contextFile = findContextMdIn(phaseDirFull);
1693
+ const phaseDirName = node_path_1.default.basename(phaseDirFull);
1694
+ // #3511 BLOCKER-3: see the parallel site above — scope before any bare
1695
+ // `.find()` predicate so a misfiled cross-phase artifact cannot win.
1696
+ const scopedFiles = scopeToPhase(files, phaseDirName);
1697
+ const contextFile = findContextMdIn(scopedFiles);
1588
1698
  if (contextFile) {
1589
1699
  result['context_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, contextFile));
1590
1700
  }
1591
- const researchFile = files.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
1701
+ const researchFile = scopedFiles.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
1592
1702
  if (researchFile) {
1593
1703
  result['research_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, researchFile));
1594
1704
  }
1595
- const verificationFile = files.find((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
1705
+ // #3473 F2: routed through the shared resolver — readdir order is
1706
+ // filesystem-dependent, so the prior hand-rolled `.find()` could pick
1707
+ // either file when a phase held both a canonical report and an ad-hoc
1708
+ // `-CORRECTION-VERIFICATION.md` worksheet (#3357).
1709
+ // #3492: pin selection to THIS phase's own token so a stray cross-phase
1710
+ // or sentinel-numbered canonically-shaped file cannot outrank this
1711
+ // phase's own (possibly non-canonical) report.
1712
+ const phaseToken = extractPhaseToken(phaseDirName);
1713
+ const verificationFile = resolveVerificationFile(files, {
1714
+ allowBare: true,
1715
+ phaseToken,
1716
+ phaseDirName,
1717
+ });
1596
1718
  if (verificationFile) {
1597
1719
  result['verification_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, verificationFile));
1598
1720
  }
1599
- const uatFile = files.find((f) => f.endsWith('-UAT.md') || f === 'UAT.md');
1721
+ // #3518: routed through the shared UAT resolver — the prior hand-rolled
1722
+ // `.find()` over unsorted readdir order had no phase check and no
1723
+ // ordering, so a stray cross-phase 02-UAT.md could become this phase's
1724
+ // uat_path, filesystem-dependently. Pinned to this phase's own token
1725
+ // (same rule as verification_path above), and phase-scoped via
1726
+ // phaseDirName (#3511) so the alphabetically-first fallback tier also
1727
+ // excludes cross-phase strays.
1728
+ const uatFile = resolveUatFile(files, {
1729
+ allowBare: true,
1730
+ phaseToken,
1731
+ phaseDirName,
1732
+ });
1600
1733
  if (uatFile) {
1601
1734
  result['uat_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, uatFile));
1602
1735
  }
1603
- const reviewsFile = files.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
1736
+ const reviewsFile = scopedFiles.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
1604
1737
  if (reviewsFile) {
1605
1738
  result['reviews_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, reviewsFile));
1606
1739
  }
@@ -1669,7 +1802,7 @@ function cmdInitTodos(cwd, area, raw) {
1669
1802
  }
1670
1803
  function cmdInitMilestoneOp(cwd, raw) {
1671
1804
  const config = loadConfig(cwd);
1672
- const milestone = getMilestoneInfo(cwd);
1805
+ const milestone = milestoneRecord(cwd);
1673
1806
  let phaseCount = 0;
1674
1807
  let completedPhases = 0;
1675
1808
  const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
@@ -1682,7 +1815,8 @@ function cmdInitMilestoneOp(cwd, raw) {
1682
1815
  const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
1683
1816
  let m;
1684
1817
  while ((m = phasePattern.exec(currentSection)) !== null) {
1685
- if (/^999(?:\.|$)/.test(m[1]))
1818
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
1819
+ if (isSentinelPhaseId(m[1]))
1686
1820
  continue;
1687
1821
  roadmapPhaseNumbers.push(m[1]);
1688
1822
  }
@@ -1727,8 +1861,12 @@ function cmdInitMilestoneOp(cwd, raw) {
1727
1861
  }
1728
1862
  else {
1729
1863
  try {
1730
- const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
1731
- const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
1864
+ // #3185 (ADR-3180 Decision 1): the ROADMAP heading scan above found no
1865
+ // current-milestone phase headings — fall back to asking the canonical
1866
+ // owner "which phase directories belong to the current milestone"
1867
+ // directly, instead of a hand-rolled readdirSync over every directory
1868
+ // on disk (which also never excluded sentinels, unlike the owner).
1869
+ const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
1732
1870
  phaseCount = dirs.length;
1733
1871
  for (const dir of dirs) {
1734
1872
  try {
@@ -1758,8 +1896,12 @@ function cmdInitMilestoneOp(cwd, raw) {
1758
1896
  }
1759
1897
  const result = {
1760
1898
  commit_docs: config.commit_docs,
1761
- milestone_version: milestone['version'],
1762
- milestone_name: milestone['name'],
1899
+ // #3216 review Finding 2: `?? null` so an unresolved milestone still emits
1900
+ // the key with an explicit `null` rather than letting JSON.stringify drop
1901
+ // it — an omitted key reaches the prompt layer's `{milestone_version}`
1902
+ // placeholder as literal, un-substituted text.
1903
+ milestone_version: milestone['version'] ?? null,
1904
+ milestone_name: milestone['name'] ?? null,
1763
1905
  milestone_slug: generateSlugInternal(milestone['name']),
1764
1906
  phase_count: phaseCount,
1765
1907
  completed_phases: completedPhases,
@@ -1803,7 +1945,7 @@ function cmdInitMapCodebase(cwd, raw) {
1803
1945
  }
1804
1946
  function cmdInitManager(cwd, raw) {
1805
1947
  const config = loadConfig(cwd);
1806
- const milestone = getMilestoneInfo(cwd);
1948
+ const milestone = milestoneRecord(cwd);
1807
1949
  const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
1808
1950
  const paths = planningPaths(cwd);
1809
1951
  if (!node_fs_1.default.existsSync(paths.roadmap)) {
@@ -1815,18 +1957,12 @@ function cmdInitManager(cwd, raw) {
1815
1957
  const rawContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf-8');
1816
1958
  const content = extractCurrentMilestone(rawContent, cwd);
1817
1959
  const phasesDir = paths.phases;
1818
- const isDirInMilestone = getMilestonePhaseFilter(cwd);
1819
- const _phaseDirEntries = (() => {
1820
- try {
1821
- return node_fs_1.default
1822
- .readdirSync(phasesDir, { withFileTypes: true })
1823
- .filter((e) => e.isDirectory())
1824
- .map((e) => e.name);
1825
- }
1826
- catch {
1827
- return [];
1828
- }
1829
- })();
1960
+ // #3185 (ADR-3180 Decision 1): "which phase directories belong to the
1961
+ // CURRENT milestone" is the scoped question listMilestonePhaseDirs owns —
1962
+ // routed through it instead of a hand-rolled readdirSync + a separate
1963
+ // getMilestonePhaseFilter window check (which also never excluded
1964
+ // sentinels, unlike the owner).
1965
+ const _phaseDirEntries = listMilestonePhaseDirs(phasesDir, { cwd }).value;
1830
1966
  const _checkboxStates = new Map();
1831
1967
  const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
1832
1968
  let _cbMatch;
@@ -1861,16 +1997,24 @@ function cmdInitManager(cwd, raw) {
1861
1997
  let isActive = false;
1862
1998
  let completion = buildPhaseCompletionProjection(cwd, phaseNum, null, planCount, summaryCount, _slashRuntime);
1863
1999
  try {
1864
- const dirs = _phaseDirEntries.filter(isDirInMilestone);
1865
- const dirMatch = dirs.find((d) => phaseTokenMatches(d, normalized));
2000
+ // #3185 (ADR-3180 Decision 2) moved this lookup off the
2001
+ // milestone-scoped set and onto the physical one; that scope choice is
2002
+ // kept. Only the matcher is this PR's: matchPhaseDirs resolves
2003
+ // digit-leading directory names the token predicate cannot (#2528).
2004
+ const dirMatch = matchPhaseDirs(_phaseDirEntries, normalized).matches[0];
1866
2005
  if (dirMatch) {
1867
2006
  const fullDir = node_path_1.default.join(phasesDir, dirMatch);
1868
2007
  const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, fullDir));
1869
2008
  const phaseFiles = node_fs_1.default.readdirSync(fullDir);
1870
2009
  planCount = listPhasePlanFiles(fullDir).length;
1871
2010
  summaryCount = listPhaseSummaryFiles(fullDir).length;
1872
- hasContext = findContextMdIn(fullDir) !== null;
1873
- hasResearch = phaseFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
2011
+ // #3511-class: scope the raw listing to THIS phase's own artifacts
2012
+ // before the hasContext/hasResearch predicates run, so a stray
2013
+ // cross-phase `-CONTEXT.md`/`-RESEARCH.md` sitting in this directory
2014
+ // cannot win this phase's lookup.
2015
+ const scopedFiles = scopeToPhase(phaseFiles, dirMatch);
2016
+ hasContext = findContextMdIn(scopedFiles) !== null;
2017
+ hasResearch = scopedFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
1874
2018
  completion = buildPhaseCompletionProjection(cwd, phaseNum, phaseDirRel, planCount, summaryCount, _slashRuntime);
1875
2019
  if (completion.phase_complete)
1876
2020
  diskStatus = 'complete';
@@ -1886,7 +2030,7 @@ function cmdInitManager(cwd, raw) {
1886
2030
  diskStatus = 'discussed';
1887
2031
  else
1888
2032
  diskStatus = 'empty';
1889
- const nowMs = Date.now();
2033
+ const nowMs = clock_cjs_1.realClock.now();
1890
2034
  let newestMtime = 0;
1891
2035
  for (const f of phaseFiles) {
1892
2036
  try {
@@ -1907,18 +2051,20 @@ function cmdInitManager(cwd, raw) {
1907
2051
  catch {
1908
2052
  /* intentionally empty */
1909
2053
  }
2054
+ // ADR-3180 §7.4 (disk-strict, #2957, maintainer decision 2026-08-08):
2055
+ // `roadmapComplete` is reported below as metadata only — it carries NO
2056
+ // machine authority over `diskStatus`. The #3033 checkbox override that
2057
+ // used to live here (treating a zero-plan phase as complete whenever the
2058
+ // ROADMAP checkbox was ticked, layered on top of
2059
+ // buildPhaseCompletionProjection's own output) is DELETED, not
2060
+ // generalized: `diskStatus` now comes entirely from `completion`, which
2061
+ // already routes through the canonical owner (`isPhaseComplete`) and
2062
+ // itself resolves a zero-plan phase as complete whenever a passing
2063
+ // `*-VERIFICATION.md` exists (#3168) — with no dependency on the
2064
+ // checkbox. A zero-plan phase whose completion previously relied SOLELY
2065
+ // on a ticked checkbox (no passing verification) now reports incomplete;
2066
+ // this is the deliberate Tier-2 break (ADR-3180 §7.4 Decision 3).
1910
2067
  const roadmapComplete = _checkboxStates.get(phaseNum) || false;
1911
- // #3033: a zero-plan phase (split parent — intentionally plan-less, holds
1912
- // shared context for sub-phases) whose roadmap checkbox is marked complete
1913
- // must resolve as complete. The original gate required completion.phase_complete
1914
- // (derived from plan/summary counts), which is always false for zero-plan
1915
- // phases — so the checkbox override never fired and the parent was permanently
1916
- // stuck as 'researched' (an in-progress state eligible for current-phase
1917
- // selection). Now: when the roadmap marks it complete AND it has zero plans,
1918
- // treat it as complete regardless of the plan-count derivation.
1919
- if (roadmapComplete && (completion.phase_complete || planCount === 0) && diskStatus !== 'complete') {
1920
- diskStatus = 'complete';
1921
- }
1922
2068
  phases.push({
1923
2069
  number: phaseNum,
1924
2070
  name: phaseName,
@@ -2023,7 +2169,8 @@ function cmdInitManager(cwd, raw) {
2023
2169
  for (const phase of phases) {
2024
2170
  if (phase['disk_status'] === 'complete')
2025
2171
  continue;
2026
- if (/^999(?:\.|$)/.test(phase['number']))
2172
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
2173
+ if (isSentinelPhaseId(phase['number']))
2027
2174
  continue;
2028
2175
  if (phase['disk_status'] === 'executed') {
2029
2176
  recommendedActions.push({
@@ -2077,7 +2224,8 @@ function cmdInitManager(cwd, raw) {
2077
2224
  }
2078
2225
  return true;
2079
2226
  });
2080
- const nonBacklogPhases = phases.filter((p) => !/^999(?:\.|$)/.test(p['number']));
2227
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
2228
+ const nonBacklogPhases = phases.filter((p) => !isSentinelPhaseId(p['number']));
2081
2229
  const completedCount = nonBacklogPhases.filter((p) => p['phase_complete'] === true).length;
2082
2230
  const sanitizeFlags = (rawVal) => {
2083
2231
  const val = typeof rawVal === 'string' ? rawVal : '';
@@ -2100,8 +2248,12 @@ function cmdInitManager(cwd, raw) {
2100
2248
  execute: sanitizeFlags(mgrFlags?.['execute']),
2101
2249
  };
2102
2250
  const result = {
2103
- milestone_version: milestone['version'],
2104
- milestone_name: milestone['name'],
2251
+ // #3216 review Finding 2: `?? null` so an unresolved milestone still emits
2252
+ // the key with an explicit `null` rather than letting JSON.stringify drop
2253
+ // it — an omitted key reaches the prompt layer's `{milestone_version}`
2254
+ // placeholder as literal, un-substituted text.
2255
+ milestone_version: milestone['version'] ?? null,
2256
+ milestone_name: milestone['name'] ?? null,
2105
2257
  phases,
2106
2258
  phase_count: phases.length,
2107
2259
  completed_count: completedCount,
@@ -2212,7 +2364,7 @@ function cmdInitDocsUpdate(cwd, raw, options = {}) {
2212
2364
  * `state:chunked-mode`/`state:plan-strategy-converge`). This DELIBERATELY
2213
2365
  * does not replace `update.md`'s own `parse_update_channel` case-statement
2214
2366
  * (`TAG="next"`/`TAG="latest"`) — issue #815's regression test
2215
- * (`tests/issue-815-update-next-channel.test.cjs`) asserts that literal
2367
+ * (`tests/update-workflow.test.cjs`) asserts that literal
2216
2368
  * case-statement text stays in `update.md` verbatim (the npm dist-tag
2217
2369
  * selection has to run in the workflow's own shell before any `gsd_run`
2218
2370
  * round-trip), so `next_channel` exists purely to gate the `channel-banner`
@@ -2258,7 +2410,9 @@ function cmdInitUpdate(cwd, raw, options = {}) {
2258
2410
  * pure JSON consumer with no `gsd_run` call of its own.
2259
2411
  */
2260
2412
  function cmdInitTransition(cwd, raw, options = {}) {
2261
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
2413
+ // #3579 root-cause fix: read-only informational field — peek, don't
2414
+ // self-heal (see cmdInitNewMilestone's identical rationale above).
2415
+ const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
2262
2416
  const workstreamActive = !!resolvedWorkstream;
2263
2417
  const result = {
2264
2418
  other_active_workstreams: workstreamActive
@@ -2339,7 +2493,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
2339
2493
  /* intentionally empty */
2340
2494
  }
2341
2495
  const config = loadConfig(cwd);
2342
- const milestone = getMilestoneInfo(cwd);
2496
+ const milestone = milestoneRecord(cwd);
2343
2497
  const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
2344
2498
  // #1912: fail safe in workstream mode with no active workstream. With no active
2345
2499
  // workstream and no --ws, planningDir(cwd) resolves to root .planning — silently
@@ -2347,11 +2501,27 @@ function cmdInitProgress(cwd, raw, options = {}) {
2347
2501
  // Mirror planningDir's resolution (GSD_WORKSTREAM env > stored active pointer) so
2348
2502
  // an explicit --ws (which sets GSD_WORKSTREAM) satisfies the check.
2349
2503
  const _availableWorkstreams = listAvailableWorkstreams(cwd);
2350
- const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
2504
+ // #3579 root-cause fix: this is a check, not a consuming read — use the
2505
+ // non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
2506
+ // here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
2507
+ // which would misreport a present-but-bad marker as no marker at all.
2508
+ const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
2351
2509
  if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) {
2510
+ // #3579: getActiveWorkstream now inherits a pointer-less session's read
2511
+ // from the shared .planning/active-workstream marker, so reaching this
2512
+ // branch with a marker actually present means the marker EXISTED but
2513
+ // didn't resolve (invalid name, or its workstream dir is gone) — a
2514
+ // materially different situation from "nothing was ever set" and one
2515
+ // that deserves its own diagnostic instead of the generic message below.
2516
+ const _diagnosis = diagnoseUnresolvedActiveWorkstream(cwd);
2517
+ if (_diagnosis.present) {
2518
+ error(`init.progress requires a workstream in workstream mode — the active-workstream marker names '${_diagnosis.value}', but it did not resolve: ${describeUnresolvedWorkstreamReason(_diagnosis.reason)}. Root STATE.md (likely stale) would be reported otherwise. ` +
2519
+ `Pass --ws <name> or run ${(0, runtime_slash_cjs_1.formatGsdSlash)('workstream set', _slashRuntime)} to point it at an existing workstream. ` +
2520
+ `Available workstreams: ${_availableWorkstreams.join(', ')}`, ERROR_REASON.WORKSTREAM_MODE_MARKER_UNRESOLVED, { marker_value: _diagnosis.value, marker_reason: _diagnosis.reason });
2521
+ }
2352
2522
  error(`init.progress requires a workstream in workstream mode — no active workstream is set, so root STATE.md (likely stale) would be reported. ` +
2353
2523
  `Pass --ws <name> or run ${(0, runtime_slash_cjs_1.formatGsdSlash)('workstream set', _slashRuntime)} first. ` +
2354
- `Available workstreams: ${_availableWorkstreams.join(', ')}`);
2524
+ `Available workstreams: ${_availableWorkstreams.join(', ')}`, ERROR_REASON.WORKSTREAM_MODE_NONE_ACTIVE);
2355
2525
  }
2356
2526
  const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
2357
2527
  const phases = [];
@@ -2378,21 +2548,15 @@ function cmdInitProgress(cwd, raw, options = {}) {
2378
2548
  catch {
2379
2549
  /* intentionally empty */
2380
2550
  }
2381
- const isDirInMilestone = getMilestonePhaseFilter(cwd);
2382
2551
  const seenPhaseNums = new Set();
2383
2552
  try {
2384
- const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
2385
- const dirs = entries
2386
- .filter((e) => e.isDirectory())
2387
- .map((e) => e.name)
2388
- .filter(isDirInMilestone)
2389
- .sort((a, b) => {
2390
- const pa = a.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
2391
- const pb = b.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
2392
- if (!pa || !pb)
2393
- return a.localeCompare(b);
2394
- return parseInt(pa[1], 10) - parseInt(pb[1], 10);
2395
- });
2553
+ // #3185 (ADR-3180 Decision 1): "which phase directories belong to the
2554
+ // CURRENT milestone" — routed through the canonical owner instead of a
2555
+ // hand-rolled readdirSync + isDirInMilestone filter + local sort (which
2556
+ // also never excluded sentinels, unlike the owner; the final `phases`
2557
+ // array is re-sorted below anyway, so dropping the local sort here is
2558
+ // behavior-preserving).
2559
+ const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
2396
2560
  for (const dir of dirs) {
2397
2561
  const dirMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})-?(.*)`, 'i'));
2398
2562
  const phaseNumber = dirMatch ? dirMatch[1] : dir;
@@ -2402,7 +2566,12 @@ function cmdInitProgress(cwd, raw, options = {}) {
2402
2566
  const phaseFiles = node_fs_1.default.readdirSync(phasePath);
2403
2567
  const plans = listPhasePlanFiles(phasePath);
2404
2568
  const summaries = listPhaseSummaryFiles(phasePath);
2405
- const hasResearch = phaseFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
2569
+ // #3511-class: scope the raw listing to THIS phase's own artifacts
2570
+ // before the hasResearch predicate runs, so a stray cross-phase
2571
+ // `-RESEARCH.md` sitting in this directory cannot win this phase's
2572
+ // lookup.
2573
+ const scopedPhaseFiles = scopeToPhase(phaseFiles, dir);
2574
+ const hasResearch = scopedPhaseFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
2406
2575
  const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'phases', dir)));
2407
2576
  const completion = buildPhaseCompletionProjection(cwd, phaseNumber, phaseDirRel, plans.length, summaries.length, _slashRuntime);
2408
2577
  const status = completion.phase_complete
@@ -2464,6 +2633,25 @@ function cmdInitProgress(cwd, raw, options = {}) {
2464
2633
  }
2465
2634
  }
2466
2635
  phases.sort((a, b) => parseInt(a['number'], 10) - parseInt(b['number'], 10));
2636
+ // #3581: the frontier is ROADMAP ORDER, not artifact presence. The disk loop
2637
+ // above could claim nextPhase from a stray out-of-order artifact directory
2638
+ // (a phase-9 UAT evidence dir while roadmap phase 8 was pending and
2639
+ // unscaffolded), silently skipping 8 — and init.progress then disagreed with
2640
+ // roadmap.analyze on the same tree. Re-derive from the sorted union: the
2641
+ // first phase that has not begun ('pending' | 'not_started') and is not
2642
+ // roadmap-complete wins; artifacts still feed each entry's status and
2643
+ // completion (corroborating evidence) but no longer outrank the ordering.
2644
+ // Aligned trees derive the identical frontier as the loops above; an
2645
+ // all-complete milestone finds none and keeps nextPhase null for the
2646
+ // completion flow.
2647
+ {
2648
+ const frontier = phases.find((p) => {
2649
+ const st = p['status'];
2650
+ return (st === 'pending' || st === 'not_started') && p['roadmap_complete'] !== true;
2651
+ });
2652
+ if (frontier)
2653
+ nextPhase = frontier;
2654
+ }
2467
2655
  let pausedAt = null;
2468
2656
  const state = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'STATE.md'));
2469
2657
  if (state !== null) {
@@ -2488,8 +2676,12 @@ function cmdInitProgress(cwd, raw, options = {}) {
2488
2676
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
2489
2677
  planner_model: resolveModelInternal(cwd, 'gsd-planner'),
2490
2678
  commit_docs: config.commit_docs,
2491
- milestone_version: milestone['version'],
2492
- milestone_name: milestone['name'],
2679
+ // #3216 review Finding 2: `?? null` so an unresolved milestone still emits
2680
+ // the key with an explicit `null` rather than letting JSON.stringify drop
2681
+ // it — an omitted key reaches the prompt layer's `{milestone_version}`
2682
+ // placeholder as literal, un-substituted text.
2683
+ milestone_version: milestone['version'] ?? null,
2684
+ milestone_name: milestone['name'] ?? null,
2493
2685
  phases,
2494
2686
  phase_count: phases.length,
2495
2687
  completed_count: phases.filter((p) => p['status'] === 'complete').length,
@@ -3043,7 +3235,7 @@ function buildSkillManifest(cwd, skillsDir = null) {
3043
3235
  seenNamesInRoot.add(name);
3044
3236
  const description = frontmatter['description'] || '';
3045
3237
  const triggers = [];
3046
- const bodyMatch = content.match(/^---[\s\S]*?---\s*\n([\s\S]*)$/);
3238
+ const bodyMatch = content.match(/^---[\s\S]*?---\s*\r?\n([\s\S]*)$/);
3047
3239
  if (bodyMatch) {
3048
3240
  const body = bodyMatch[1];
3049
3241
  const triggerLines = body.match(/^TRIGGER\s+when:\s*(.+)$/gmi);