@opengsd/gsd-core 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (544) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +12 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-doc-synthesizer.md +2 -4
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +22 -14
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +5 -3
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +23 -2
  15. package/agents/gsd-plan-checker.md +50 -53
  16. package/agents/gsd-planner.md +50 -50
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +63 -4
  21. package/agents/gsd-ui-researcher.md +41 -3
  22. package/agents/gsd-user-profiler.md +3 -0
  23. package/agents/gsd-verifier.md +13 -4
  24. package/bin/install.js +1448 -1103
  25. package/commands/gsd/code-review.md +1 -1
  26. package/commands/gsd/discuss-phase.md +1 -1
  27. package/commands/gsd/execute-phase.md +1 -1
  28. package/commands/gsd/import.md +1 -1
  29. package/commands/gsd/map-codebase.md +1 -1
  30. package/commands/gsd/mempalace-capture.md +1 -1
  31. package/commands/gsd/mempalace-recall.md +1 -1
  32. package/commands/gsd/new-milestone.md +1 -1
  33. package/commands/gsd/quick.md +9 -5
  34. package/commands/gsd/review-backlog.md +2 -1
  35. package/commands/gsd/verify-work.md +1 -1
  36. package/gsd-core/bin/gsd-tools.cjs +1035 -138
  37. package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
  38. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  39. package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
  40. package/gsd-core/bin/lib/api-coverage.cjs +33 -14
  41. package/gsd-core/bin/lib/artifacts.cjs +5 -0
  42. package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
  43. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  44. package/gsd-core/bin/lib/audit.cjs +1026 -268
  45. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  46. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  47. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  48. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  49. package/gsd-core/bin/lib/capability-registry.cjs +845 -130
  50. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  51. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  52. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  53. package/gsd-core/bin/lib/capability-validator.cjs +700 -40
  54. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  55. package/gsd-core/bin/lib/check-command-router.cjs +216 -42
  56. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  57. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  58. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  59. package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
  60. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  61. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  62. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  63. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  64. package/gsd-core/bin/lib/commands.cjs +1172 -108
  65. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  66. package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
  67. package/gsd-core/bin/lib/config-loader.cjs +187 -23
  68. package/gsd-core/bin/lib/config.cjs +102 -3
  69. package/gsd-core/bin/lib/configuration.cjs +129 -37
  70. package/gsd-core/bin/lib/core-utils.cjs +208 -33
  71. package/gsd-core/bin/lib/decisions.cjs +23 -0
  72. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  73. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  74. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  75. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  76. package/gsd-core/bin/lib/frontmatter.cjs +899 -229
  77. package/gsd-core/bin/lib/gap-checker.cjs +95 -10
  78. package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
  79. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  80. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  81. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  82. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
  83. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  84. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  85. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  86. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
  87. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  88. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  89. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
  90. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  91. package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
  92. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  93. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  94. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  95. package/gsd-core/bin/lib/init.cjs +439 -168
  96. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  97. package/gsd-core/bin/lib/install-engine.cjs +811 -259
  98. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  99. package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
  100. package/gsd-core/bin/lib/install-profiles.cjs +212 -61
  101. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  102. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  103. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  104. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  105. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  106. package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
  107. package/gsd-core/bin/lib/intel.cjs +101 -26
  108. package/gsd-core/bin/lib/io.cjs +170 -15
  109. package/gsd-core/bin/lib/learnings.cjs +85 -14
  110. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  111. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  112. package/gsd-core/bin/lib/markdown-table.cjs +183 -22
  113. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  114. package/gsd-core/bin/lib/milestone.cjs +842 -73
  115. package/gsd-core/bin/lib/model-catalog.cjs +232 -16
  116. package/gsd-core/bin/lib/model-resolver.cjs +193 -68
  117. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  118. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  119. package/gsd-core/bin/lib/pattern.cjs +122 -0
  120. package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
  121. package/gsd-core/bin/lib/phase-id.cjs +514 -40
  122. package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
  123. package/gsd-core/bin/lib/phase-locator.cjs +262 -34
  124. package/gsd-core/bin/lib/phase.cjs +1038 -214
  125. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  126. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  127. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  128. package/gsd-core/bin/lib/plan-scan.cjs +98 -3
  129. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  130. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  131. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  132. package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
  133. package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
  134. package/gsd-core/bin/lib/probe-core.cjs +5 -2
  135. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  136. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  137. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  138. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  139. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
  140. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  141. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
  142. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  143. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  144. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  145. package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
  146. package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
  147. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  148. package/gsd-core/bin/lib/roadmap.cjs +442 -96
  149. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
  150. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  151. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
  152. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  153. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  154. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
  155. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  156. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  157. package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
  158. package/gsd-core/bin/lib/security.cjs +104 -5
  159. package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
  160. package/gsd-core/bin/lib/smart-entry.cjs +133 -23
  161. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  162. package/gsd-core/bin/lib/state-command-router.cjs +52 -19
  163. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  164. package/gsd-core/bin/lib/state-document.cjs +338 -8
  165. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  166. package/gsd-core/bin/lib/state-transition.cjs +846 -176
  167. package/gsd-core/bin/lib/state.cjs +2589 -369
  168. package/gsd-core/bin/lib/surface.cjs +33 -11
  169. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  170. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  171. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  172. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  173. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  174. package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
  175. package/gsd-core/bin/lib/uat.cjs +1761 -167
  176. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  177. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  178. package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
  179. package/gsd-core/bin/lib/unusable-input.cjs +37 -0
  180. package/gsd-core/bin/lib/update-context.cjs +8 -2
  181. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  182. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  183. package/gsd-core/bin/lib/validate.cjs +20 -6
  184. package/gsd-core/bin/lib/vendor/README.md +75 -0
  185. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  186. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  187. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  188. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  189. package/gsd-core/bin/lib/verification.cjs +272 -9
  190. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  191. package/gsd-core/bin/lib/verify.cjs +453 -918
  192. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  193. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  194. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  195. package/gsd-core/bin/lib/workstream.cjs +2 -2
  196. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  197. package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
  198. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
  199. package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
  200. package/gsd-core/bin/shared/exit-codes.json +8 -0
  201. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  202. package/gsd-core/bin/shared/model-catalog.json +8 -1
  203. package/gsd-core/references/agent-contracts.md +44 -26
  204. package/gsd-core/references/api-coverage.md +24 -2
  205. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  206. package/gsd-core/references/checkpoints.md +39 -21
  207. package/gsd-core/references/context-budget.md +1 -1
  208. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  209. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  210. package/gsd-core/references/doc-conflict-engine.md +1 -1
  211. package/gsd-core/references/edge-probe.md +8 -0
  212. package/gsd-core/references/execute-mvp-tdd.md +4 -6
  213. package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
  214. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  215. package/gsd-core/references/execute-phase-response-language.md +1 -1
  216. package/gsd-core/references/execute-phase-wave-guard.md +17 -11
  217. package/gsd-core/references/failing-direction.md +78 -0
  218. package/gsd-core/references/gate-prompts.md +1 -1
  219. package/gsd-core/references/git-integration.md +5 -5
  220. package/gsd-core/references/git-planning-commit.md +5 -4
  221. package/gsd-core/references/gsd-run-resolver.md +1 -1
  222. package/gsd-core/references/loop-hook-dispatch.md +61 -2
  223. package/gsd-core/references/model-profiles.md +12 -4
  224. package/gsd-core/references/mvp-concepts.md +9 -9
  225. package/gsd-core/references/nyquist-compliance.md +74 -0
  226. package/gsd-core/references/offer-next.md +3 -5
  227. package/gsd-core/references/phase-argument-parsing.md +3 -3
  228. package/gsd-core/references/planner-failing-direction.md +53 -0
  229. package/gsd-core/references/planner-guidance.md +3 -9
  230. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  231. package/gsd-core/references/planner-preconditions.md +1 -1
  232. package/gsd-core/references/planner-reviews.md +1 -1
  233. package/gsd-core/references/planner-revision.md +1 -1
  234. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  235. package/gsd-core/references/planning-config.md +44 -13
  236. package/gsd-core/references/reviewer-instances.md +31 -0
  237. package/gsd-core/references/revision-loop.md +1 -1
  238. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  239. package/gsd-core/references/specless-probe-fallback.md +1 -1
  240. package/gsd-core/references/tdd.md +1 -3
  241. package/gsd-core/references/ui-brand.md +65 -21
  242. package/gsd-core/references/ui-consideration-probe.md +1 -1
  243. package/gsd-core/references/universal-anti-patterns.md +5 -5
  244. package/gsd-core/references/verifier-phase-gates.md +192 -0
  245. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  246. package/gsd-core/references/verify-mvp-mode.md +2 -2
  247. package/gsd-core/references/workstream-flag.md +33 -17
  248. package/gsd-core/templates/README.md +1 -1
  249. package/gsd-core/templates/SECURITY.md +3 -3
  250. package/gsd-core/templates/UI-SPEC.md +25 -3
  251. package/gsd-core/templates/VALIDATION.md +3 -3
  252. package/gsd-core/templates/discussion-log.md +1 -1
  253. package/gsd-core/templates/phase-prompt.md +5 -4
  254. package/gsd-core/templates/state.md +11 -4
  255. package/gsd-core/templates/verification-report.md +9 -1
  256. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  257. package/gsd-core/workflows/add-backlog.md +1 -1
  258. package/gsd-core/workflows/add-phase.md +3 -3
  259. package/gsd-core/workflows/add-tests.md +3 -8
  260. package/gsd-core/workflows/add-todo.md +1 -1
  261. package/gsd-core/workflows/ai-integration-phase.md +13 -20
  262. package/gsd-core/workflows/audit-fix.md +12 -3
  263. package/gsd-core/workflows/audit-milestone.md +9 -9
  264. package/gsd-core/workflows/audit-uat.md +17 -2
  265. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  266. package/gsd-core/workflows/autonomous.md +11 -27
  267. package/gsd-core/workflows/check-todos.md +1 -1
  268. package/gsd-core/workflows/cleanup.md +64 -5
  269. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
  270. package/gsd-core/workflows/code-review-fix.md +38 -11
  271. package/gsd-core/workflows/code-review.md +159 -52
  272. package/gsd-core/workflows/complete-milestone.md +151 -23
  273. package/gsd-core/workflows/debug.md +12 -8
  274. package/gsd-core/workflows/diagnose-issues.md +47 -15
  275. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  276. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
  277. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  278. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  279. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  280. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  281. package/gsd-core/workflows/discuss-phase.md +1 -1
  282. package/gsd-core/workflows/do.md +3 -6
  283. package/gsd-core/workflows/docs-update.md +5 -4
  284. package/gsd-core/workflows/edit-phase.md +27 -2
  285. package/gsd-core/workflows/eval-review.md +7 -14
  286. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  287. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
  288. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  289. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  290. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  291. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -4
  292. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  293. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  294. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  295. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  296. package/gsd-core/workflows/execute-phase.md +72 -100
  297. package/gsd-core/workflows/execute-plan.md +52 -15
  298. package/gsd-core/workflows/explore.md +131 -4
  299. package/gsd-core/workflows/extract-learnings.md +1 -1
  300. package/gsd-core/workflows/fast.md +10 -2
  301. package/gsd-core/workflows/forensics.md +1 -1
  302. package/gsd-core/workflows/graduation.md +5 -5
  303. package/gsd-core/workflows/health.md +76 -10
  304. package/gsd-core/workflows/import.md +18 -15
  305. package/gsd-core/workflows/inbox.md +4 -5
  306. package/gsd-core/workflows/ingest-docs.md +49 -16
  307. package/gsd-core/workflows/insert-phase.md +5 -5
  308. package/gsd-core/workflows/list-seeds.md +5 -3
  309. package/gsd-core/workflows/list-workspaces.md +1 -1
  310. package/gsd-core/workflows/manager.md +12 -23
  311. package/gsd-core/workflows/map-codebase.md +1 -1
  312. package/gsd-core/workflows/milestone-summary.md +1 -1
  313. package/gsd-core/workflows/mvp-phase.md +8 -5
  314. package/gsd-core/workflows/new-milestone.md +22 -29
  315. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  316. package/gsd-core/workflows/new-project.md +26 -40
  317. package/gsd-core/workflows/new-workspace.md +1 -1
  318. package/gsd-core/workflows/next.md +14 -2
  319. package/gsd-core/workflows/pause-work.md +1 -1
  320. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  321. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  322. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  323. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  324. package/gsd-core/workflows/plan-phase.md +162 -59
  325. package/gsd-core/workflows/plan-review-convergence.md +96 -11
  326. package/gsd-core/workflows/plant-seed.md +2 -2
  327. package/gsd-core/workflows/pr-branch.md +187 -51
  328. package/gsd-core/workflows/profile-user.md +16 -14
  329. package/gsd-core/workflows/progress.md +61 -18
  330. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  331. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
  332. package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
  333. package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
  334. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  335. package/gsd-core/workflows/quick.md +55 -44
  336. package/gsd-core/workflows/remove-phase.md +4 -4
  337. package/gsd-core/workflows/remove-workspace.md +2 -2
  338. package/gsd-core/workflows/resume-project.md +8 -12
  339. package/gsd-core/workflows/review.md +219 -20
  340. package/gsd-core/workflows/scan.md +1 -1
  341. package/gsd-core/workflows/secure-phase.md +3 -3
  342. package/gsd-core/workflows/session-report.md +2 -1
  343. package/gsd-core/workflows/settings-advanced.md +7 -9
  344. package/gsd-core/workflows/settings-integrations.md +64 -31
  345. package/gsd-core/workflows/settings.md +69 -7
  346. package/gsd-core/workflows/ship.md +116 -50
  347. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  348. package/gsd-core/workflows/sketch.md +12 -18
  349. package/gsd-core/workflows/smart-entry.md +3 -5
  350. package/gsd-core/workflows/spec-phase.md +53 -13
  351. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  352. package/gsd-core/workflows/spike.md +20 -31
  353. package/gsd-core/workflows/stats.md +2 -2
  354. package/gsd-core/workflows/sync-skills.md +64 -9
  355. package/gsd-core/workflows/thread.md +11 -7
  356. package/gsd-core/workflows/transition.md +49 -14
  357. package/gsd-core/workflows/ui-phase.md +15 -21
  358. package/gsd-core/workflows/ui-review.md +8 -12
  359. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  360. package/gsd-core/workflows/undo.md +8 -16
  361. package/gsd-core/workflows/update.md +7 -11
  362. package/gsd-core/workflows/validate-phase.md +3 -3
  363. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  364. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  365. package/gsd-core/workflows/verify-work.md +66 -25
  366. package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
  367. package/hooks/dist/gsd-check-update-worker.js +56 -13
  368. package/hooks/dist/gsd-check-update.js +19 -1
  369. package/hooks/dist/gsd-config-reload.js +18 -12
  370. package/hooks/dist/gsd-context-monitor.js +19 -10
  371. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  372. package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
  373. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  374. package/hooks/dist/gsd-cursor-stop.js +2 -1
  375. package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
  376. package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
  377. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  378. package/hooks/dist/gsd-graphify-update.sh +22 -18
  379. package/hooks/dist/gsd-node-runner.sh +76 -0
  380. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  381. package/hooks/dist/gsd-prompt-guard.js +37 -27
  382. package/hooks/dist/gsd-read-guard.js +16 -7
  383. package/hooks/dist/gsd-read-injection-scanner.js +55 -32
  384. package/hooks/dist/gsd-session-state.sh +1 -0
  385. package/hooks/dist/gsd-statusline.js +231 -24
  386. package/hooks/dist/gsd-update-banner.js +22 -1
  387. package/hooks/dist/gsd-validate-commit.sh +80 -6
  388. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  389. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  390. package/hooks/dist/gsd-workflow-guard.js +162 -46
  391. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  392. package/hooks/dist/gsd-write-guard.js +35 -25
  393. package/hooks/dist/lib/cli-exit.js +560 -0
  394. package/hooks/dist/lib/exit-code-registry.js +98 -0
  395. package/hooks/dist/lib/git-cmd.js +92 -59
  396. package/hooks/dist/lib/git-probe.js +84 -0
  397. package/hooks/dist/lib/hook-exit.js +81 -0
  398. package/hooks/dist/lib/injection-patterns.js +45 -0
  399. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  400. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  401. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  402. package/hooks/gsd-agent-isolation-guard.js +158 -30
  403. package/hooks/gsd-check-update-worker.js +56 -13
  404. package/hooks/gsd-check-update.js +19 -1
  405. package/hooks/gsd-config-reload.js +18 -12
  406. package/hooks/gsd-context-monitor.js +19 -10
  407. package/hooks/gsd-cursor-post-tool.js +3 -1
  408. package/hooks/gsd-cursor-pre-tool.js +2 -3
  409. package/hooks/gsd-cursor-session-start.js +2 -1
  410. package/hooks/gsd-cursor-stop.js +2 -1
  411. package/hooks/gsd-cursor-subagent-start.js +83 -3
  412. package/hooks/gsd-cursor-subagent-stop.js +6 -3
  413. package/hooks/gsd-ensure-canonical-path.js +2 -1
  414. package/hooks/gsd-graphify-update.sh +22 -18
  415. package/hooks/gsd-node-runner.sh +76 -0
  416. package/hooks/gsd-phase-boundary.sh +1 -0
  417. package/hooks/gsd-prompt-guard.js +37 -27
  418. package/hooks/gsd-read-guard.js +16 -7
  419. package/hooks/gsd-read-injection-scanner.js +55 -32
  420. package/hooks/gsd-session-state.sh +1 -0
  421. package/hooks/gsd-statusline.js +231 -24
  422. package/hooks/gsd-update-banner.js +22 -1
  423. package/hooks/gsd-validate-commit.sh +80 -6
  424. package/hooks/gsd-windsurf-pre-command.js +16 -11
  425. package/hooks/gsd-windsurf-pre-write.js +22 -13
  426. package/hooks/gsd-workflow-guard.js +162 -46
  427. package/hooks/gsd-worktree-path-guard.js +36 -21
  428. package/hooks/gsd-write-guard.js +35 -25
  429. package/hooks/lib/cli-exit.js +560 -0
  430. package/hooks/lib/exit-code-registry.js +98 -0
  431. package/hooks/lib/git-cmd.js +92 -59
  432. package/hooks/lib/git-probe.js +84 -0
  433. package/hooks/lib/hook-exit.js +81 -0
  434. package/hooks/lib/injection-patterns.js +45 -0
  435. package/hooks/lib/isolation-deny-reason.js +39 -0
  436. package/hooks/lib/isolation-sentinel.js +9 -0
  437. package/hooks/managed-hooks-registry.cjs +3 -0
  438. package/package.json +28 -11
  439. package/pi/gsd.cjs +19 -5
  440. package/scripts/base64-scan.sh +74 -12
  441. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  442. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  443. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  444. package/scripts/build-hooks.js +5 -0
  445. package/scripts/changeset/lint.cjs +60 -5
  446. package/scripts/check-alias-drift.cjs +7 -43
  447. package/scripts/check-contract-drift.cjs +297 -0
  448. package/scripts/check-glossary-refs.cjs +77 -15
  449. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  450. package/scripts/ci-check-job-near-cap.cjs +49 -0
  451. package/scripts/ci-pr-mergeability.cjs +262 -0
  452. package/scripts/ci-test-scope.cjs +64 -14
  453. package/scripts/ci-timeout-report.cjs +230 -0
  454. package/scripts/command-contract-helpers.cjs +903 -1
  455. package/scripts/docs-guard-registry.cjs +396 -0
  456. package/scripts/gen-adr-index.cjs +728 -38
  457. package/scripts/gen-capability-registry.cjs +11 -21
  458. package/scripts/gen-context-index.cjs +2 -11
  459. package/scripts/gen-exit-code-docs.cjs +318 -0
  460. package/scripts/gen-exit-code-registry.cjs +891 -0
  461. package/scripts/gen-features.cjs +836 -0
  462. package/scripts/gen-health-docs.cjs +390 -0
  463. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  464. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  465. package/scripts/gen-inventory-manifest.cjs +50 -4
  466. package/scripts/gen-loop-host-contract.cjs +138 -25
  467. package/scripts/gen-registry.cjs +3 -14
  468. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  469. package/scripts/gen-state-md-docs.cjs +727 -0
  470. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  471. package/scripts/lib/alias-drift-families.cjs +46 -0
  472. package/scripts/lib/ci-job-timing.cjs +72 -0
  473. package/scripts/lib/cli-exit.cjs +546 -44
  474. package/scripts/lib/drift-scan.cjs +308 -0
  475. package/scripts/lib/exit-code-registry.cjs +98 -0
  476. package/scripts/lib/ndjson-reporter.cjs +119 -0
  477. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  478. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  479. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  480. package/scripts/lint-canary-version-leak.cjs +73 -0
  481. package/scripts/lint-command-contract.cjs +96 -13
  482. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  483. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  484. package/scripts/lint-default-flip-documentation.cjs +193 -0
  485. package/scripts/lint-docs-guard-registration.cjs +495 -0
  486. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  487. package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
  488. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  489. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  490. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  491. package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
  492. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  493. package/scripts/lint-milestone-window-drift.cjs +468 -0
  494. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  495. package/scripts/lint-phase-enumeration-drift.cjs +492 -0
  496. package/scripts/lint-plan-count-drift.cjs +318 -0
  497. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  498. package/scripts/lint-planning-prompt-drift.cjs +471 -0
  499. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  500. package/scripts/lint-regression-test-names.cjs +15 -13
  501. package/scripts/lint-removed-but-needed.cjs +488 -0
  502. package/scripts/lint-seam-enforcement.cjs +182 -0
  503. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  504. package/scripts/lint-source-test-name-collision.cjs +241 -0
  505. package/scripts/lint-state-field-drift.cjs +805 -0
  506. package/scripts/lint-state-write-path-drift.cjs +950 -0
  507. package/scripts/lint-test-file-count.allowlist.json +137 -8
  508. package/scripts/lint-test-file-count.cjs +25 -3
  509. package/scripts/lint-unreachable-guard-drift.cjs +830 -0
  510. package/scripts/lint-vendored-deps.cjs +297 -0
  511. package/scripts/mutation-matrix.cjs +599 -50
  512. package/scripts/pr-changed-files.cjs +63 -0
  513. package/scripts/pr-template-policy.cjs +14 -4
  514. package/scripts/prompt-injection-scan.sh +100 -14
  515. package/scripts/require-issue-link-policy.cjs +192 -0
  516. package/scripts/secret-scan.sh +75 -13
  517. package/scripts/select-docs-guards.cjs +56 -0
  518. package/scripts/sync-runtime-launcher.cjs +24 -7
  519. package/skills/gsd-autonomous/SKILL.md +0 -1
  520. package/skills/gsd-code-review/SKILL.md +1 -1
  521. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  522. package/skills/gsd-execute-phase/SKILL.md +1 -2
  523. package/skills/gsd-import/SKILL.md +1 -1
  524. package/skills/gsd-map-codebase/SKILL.md +1 -1
  525. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  526. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  527. package/skills/gsd-new-milestone/SKILL.md +1 -1
  528. package/skills/gsd-next/SKILL.md +0 -1
  529. package/skills/gsd-plan-phase/SKILL.md +0 -1
  530. package/skills/gsd-progress/SKILL.md +0 -1
  531. package/skills/gsd-quick/SKILL.md +9 -5
  532. package/skills/gsd-review-backlog/SKILL.md +2 -1
  533. package/skills/gsd-stats/SKILL.md +0 -1
  534. package/skills/gsd-verify-work/SKILL.md +1 -1
  535. package/vscode/package.json +1 -1
  536. package/bin/lib/ui-safety-gate.cjs +0 -107
  537. package/gsd-core/workflows/discovery-phase.md +0 -298
  538. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  539. package/gsd-core/workflows/verify-phase.md +0 -574
  540. package/scripts/affected-tests-lib.cjs +0 -554
  541. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  542. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  543. package/scripts/run-affected-tests.cjs +0 -7
  544. package/scripts/run-tests.cjs +0 -1051
@@ -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,21 @@ 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
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- verify-command-grounding.cjs is an export= CommonJS module
78
+ const verifyCommandGrounding = require("./verify-command-grounding.cjs");
79
+ const { harvestPriorVerifyCommands } = verifyCommandGrounding;
80
+ const { output, error, ERROR_REASON, formatDiagnosticToken } = io;
76
81
  const { loadConfig, loadConfigResolved } = configLoader;
77
82
  const { resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride } = modelResolver;
78
- const { findPhaseInternal } = phaseLocator;
79
- const { getRoadmapPhaseInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
83
+ const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocator;
84
+ const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
80
85
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
81
- const { escapeRegex, normalizePhaseName, phaseTokenMatches, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery } = phaseId;
86
+ const { normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase } = phaseId;
82
87
  const { pruneOrphanedWorktrees } = worktreeSafety;
83
- const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, getActiveWorkstream, findContextMdIn, } = planningWorkspace;
88
+ const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
84
89
  const { determinePhaseStatus } = commandsMod;
85
90
  const { extractFrontmatter } = frontmatterMod;
86
- const { readVerificationStatus } = verificationMod;
91
+ const { isPhaseComplete, resolveVerificationFile, resolveUatFile } = verificationMod;
87
92
  const { evaluateUatPassed } = uatPredicateMod;
88
93
  const { resolveLoopHooks } = loopResolverMod;
89
94
  const { loadRegistry } = capabilityLoaderMod;
@@ -104,7 +109,7 @@ function phaseInfoMatchesExactPrefix(phaseInfo, phase) {
104
109
  function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
105
110
  const sectionRaw = roadmapPhase?.['section'];
106
111
  const section = typeof sectionRaw === 'string' ? sectionRaw : '';
107
- return new RegExp(`^#{2,4}\\s*Phase\\s+${escapeRegex(phase)}(?:\\b|\\s|:)`, 'i').test(section);
112
+ return new RegExp(`^#{2,4}\\s*Phase\\s+${(0, pattern_cjs_1.escapeRegex)(phase)}(?:\\b|\\s|:)`, 'i').test(section);
108
113
  }
109
114
  // #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
110
115
  // with the #2056 foreign-prefix guard, so every init command gets the same
@@ -127,9 +132,12 @@ function guardedGetRoadmapPhase(cwd, phase, projectCode) {
127
132
  // directory exists yet) identically at every synthetic-fallback call site
128
133
  // below — factored out once so the slugification formula itself cannot drift.
129
134
  function slugifyPhaseName(phaseName) {
130
- return phaseName
131
- ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
132
- : null;
135
+ // #3883 (ADR-3473 §8.3): delegate to the canonical slug formula
136
+ // (generateSlugInternal, core-utils.cts) rather than re-implementing it.
137
+ // `maxLen: null` preserves this site's pre-migration untruncated contract —
138
+ // the 60-char default would collapse two distinct >60-char phase names onto
139
+ // the same reported phase_slug.
140
+ return phaseName ? coreUtils.generateSlugInternal(phaseName, null) : null;
133
141
  }
134
142
  /**
135
143
  * #2994 (review finding, DEFECT.GENERATIVE-FIX): shared archived/not-found
@@ -170,39 +178,49 @@ function listPhaseSummaryFiles(phaseDir) {
170
178
  function listPhasePlanFiles(phaseDir) {
171
179
  return scanPhasePlans(phaseDir)['planFiles'];
172
180
  }
173
- function projectCompletionStatus(implementationComplete, verificationPassed) {
174
- if (implementationComplete && verificationPassed)
181
+ function projectCompletionStatus(implementationComplete, phaseComplete) {
182
+ if (phaseComplete)
175
183
  return 'complete';
176
184
  if (implementationComplete)
177
185
  return 'executed';
178
186
  return 'incomplete';
179
187
  }
180
188
  function buildPhaseCompletionProjection(cwd, phaseNumber, phaseDir, planCount, summaryCount, slashRuntime) {
189
+ // ADR-3180 §7.4 (issue #3186) / DO-NOT-MIGRATE exemption
190
+ // (scripts/lint-completion-predicate-drift.cjs FUNCTION_SCOPED_EXEMPTIONS,
191
+ // declared deviation): `implementation_complete` answers "are the plans
192
+ // done" (a `scanPhasePlans`-shaped different question, per the design's
193
+ // 0.x-split), NOT "is the phase complete" — it is kept for the
194
+ // 'executed'-vs-'planned' disk_status distinction downstream consumers
195
+ // still rely on, which `isPhaseComplete`'s locked `{ complete, verification
196
+ // }` return shape does not carry.
181
197
  const implementationComplete = planCount > 0 && summaryCount >= planCount;
182
198
  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
199
+ // #3168 / ADR-3180 §7.4 (disk-strict, #2957): route through the canonical
200
+ // owner (`src/verification.cts` · `isPhaseComplete`), which calls
201
+ // readVerificationStatus UNCONDITIONALLY — plan count is NOT a
202
+ // precondition. A zero-plan phase with a passing `*-VERIFICATION.md` is
203
+ // complete; init used to gate the read on `implementationComplete` and
204
+ // synthesize a `not_required` sentinel instead, which is the #3168 defect.
205
+ // #2617: the router still owns both the message content and the runtime
206
+ // projection; init passes the phase number it already knows (its phaseDir
188
207
  // 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: '' };
208
+ const completionResult = isPhaseComplete(phaseFullDir, { runtime: slashRuntime, phaseNumber });
209
+ const verificationStatus = completionResult.value.verification;
192
210
  const projectedVerificationStatus = verificationStatus.status;
193
211
  const projectedVerificationAction = verificationStatus.next_action;
194
212
  const verificationPassed = projectedVerificationStatus === 'passed';
195
- const phaseComplete = implementationComplete && verificationPassed;
213
+ const phaseComplete = completionResult.value.complete;
196
214
  return {
197
215
  implementation_complete: implementationComplete,
198
216
  verification_status: projectedVerificationStatus,
199
217
  verification_passed: verificationPassed,
200
218
  phase_complete: phaseComplete,
201
- completion_status: projectCompletionStatus(implementationComplete, verificationPassed),
219
+ completion_status: projectCompletionStatus(implementationComplete, phaseComplete),
202
220
  verification_next_action: projectedVerificationAction,
203
221
  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.
222
+ // #3057 B3: readVerificationStatus's result carries this flag when its
223
+ // internal staleness check could not run to completion.
206
224
  verification_stale_check_indeterminate: 'staleCheckIndeterminate' in verificationStatus
207
225
  && verificationStatus.staleCheckIndeterminate === true,
208
226
  };
@@ -222,7 +240,8 @@ function getLatestCompletedMilestone(cwd) {
222
240
  }
223
241
  function withProjectRoot(cwd, result) {
224
242
  result['project_root'] = cwd;
225
- const activeRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
243
+ // #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).
244
+ const activeRuntime = (0, host_runtime_detection_cjs_1.resolveReportedRuntime)(cwd);
226
245
  const agentStatus = checkAgentsInstalled(activeRuntime, cwd);
227
246
  result['agents_installed'] = agentStatus.agents_installed;
228
247
  result['missing_agents'] = agentStatus.missing_agents;
@@ -415,7 +434,12 @@ function detectHasPriorPhases(cwd, phaseInfo) {
415
434
  catch {
416
435
  continue;
417
436
  }
418
- if (files.some((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md')) {
437
+ // #3511-class: scope the raw listing to THIS entry's own phase artifacts
438
+ // before the bare `.some()` predicate runs, so a stray `07-VERIFICATION.md`
439
+ // physically sitting in another phase's directory cannot make that
440
+ // directory appear to have its own verification report.
441
+ const scopedFiles = scopeToPhase(files, entry.name);
442
+ if (scopedFiles.some((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md')) {
419
443
  return true;
420
444
  }
421
445
  }
@@ -537,7 +561,7 @@ function detectPhaseMvpMode(cwd, phaseNumber) {
537
561
  return false;
538
562
  const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
539
563
  const content = extractCurrentMilestone(rawContent, cwd);
540
- const escapedPhase = escapeRegex(phaseNumber);
564
+ const escapedPhase = (0, pattern_cjs_1.escapeRegex)(phaseNumber);
541
565
  const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
542
566
  const headerMatch = content.match(phaseHeader);
543
567
  if (!headerMatch || headerMatch.index === undefined)
@@ -594,7 +618,11 @@ function detectUiPhaseActive(cwd, phaseInfo) {
594
618
  // comments elsewhere in this file), same technique as detectHasPriorPhases above.
595
619
  const dirName = node_path_1.default.basename(rawDir);
596
620
  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');
621
+ // #3511-class: scope the raw listing to this phase dir before the
622
+ // phase-numbered -UI-SPEC.md predicate, so a stray cross-phase
623
+ // UI-SPEC file cannot flip this phase's ui-phase-active flag.
624
+ const scopedFiles = scopeToPhase(files, dirName);
625
+ hasUiSpecFile = scopedFiles.some((f) => f.endsWith('-UI-SPEC.md') || f === 'UI-SPEC.md');
598
626
  }
599
627
  catch {
600
628
  hasUiSpecFile = false;
@@ -691,13 +719,32 @@ function buildSectionManifestField(cwd, phaseInfo, options, workflow, overrides
691
719
  return null;
692
720
  }
693
721
  }
722
+ /**
723
+ * #3216 review Finding 1: `getMilestoneInfo(cwd).value` unwrap-and-cast was
724
+ * repeated identically (comment included) at five init call sites — factored
725
+ * out once so the cast and its `?? {}` "no milestone resolved" fallback live
726
+ * in exactly one place. Behavior-preserving: same call, same fallback, same
727
+ * cast, for every caller.
728
+ */
729
+ function milestoneRecord(cwd) {
730
+ return (getMilestoneInfo(cwd).value ?? {});
731
+ }
694
732
  function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
695
733
  if (!phase) {
696
734
  error('phase required for init execute-phase');
697
735
  }
698
736
  const config = loadConfig(cwd);
699
737
  let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
700
- const milestone = getMilestoneInfo(cwd);
738
+ // #3216: getMilestoneInfo now returns a ScopedResult — `.value` carries the
739
+ // MilestoneInfo (or null on any non-COMPLETE scope). NOT display-only: when
740
+ // `branching_strategy === 'milestone'`, `milestone['version']`/`['name']`
741
+ // below feed `branch_name` construction (see the milestone_branch_template
742
+ // branch below), so an unresolved milestone changes the constructed branch
743
+ // name, not merely what gets printed. bracket-access below naturally reads
744
+ // `undefined` when unresolved; the `milestone_version`/`milestone_name`
745
+ // output fields below coerce that to an explicit `null` (#3216 review
746
+ // Finding 2) so the key is never silently omitted from the JSON bundle.
747
+ const milestone = milestoneRecord(cwd);
701
748
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
702
749
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
703
750
  const phaseName = rp['phase_name'];
@@ -725,6 +772,13 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
725
772
  : null;
726
773
  const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
727
774
  const wf = (config.workflow ?? {});
775
+ // #3188: these paths are null when the file is absent, matching the contract
776
+ // the conditional sibling fields (context_path, patterns_path, ...) already
777
+ // honour and that ultraplan-phase.md / execute-phase.md gate on. Hoisted so
778
+ // the existence check and the emitted path share one source of truth.
779
+ const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
780
+ const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
781
+ const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
728
782
  const result = {
729
783
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
730
784
  verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
@@ -744,7 +798,17 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
744
798
  ? toPosixPath(node_path_1.default.join(cwd, phaseInfo['directory']))
745
799
  : null,
746
800
  phase_number: phaseInfo?.['phase_number'] || null,
747
- phase_name: phaseInfo?.['phase_name'] || null,
801
+ // #3171: prefer the ROADMAP's curated display name for `phase_name`. When
802
+ // the phase directory already exists on disk, the disk-lookup path
803
+ // (searchPhaseInDir) derives phase_name from the directory-name remainder
804
+ // — itself an already-slugified value (`phase.add` writes `${num}-${slug}`
805
+ // dirs), so phase_name and phase_slug come out byte-identical. An
806
+ // orchestrator wiring this field into `state begin-phase --name` then
807
+ // lands a raw slug in STATE.md's current_phase_name. The ROADMAP carries
808
+ // the human-curated display name (`### Phase N: <Name>`); prefer it,
809
+ // matching the no-disk fallback above. phase_slug stays disk-derived — it
810
+ // correctly feeds branch-name construction below and is unchanged here.
811
+ phase_name: (roadmapPhase?.['phase_name']) || (phaseInfo?.['phase_name']) || null,
748
812
  phase_slug: phaseInfo?.['phase_slug'] || null,
749
813
  phase_req_ids,
750
814
  plans: phaseInfo?.['plans'] || [],
@@ -768,22 +832,23 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
768
832
  .replace('{slug}', phaseInfo['phase_slug'] || 'phase')
769
833
  : config.branching_strategy === 'milestone'
770
834
  ? config.milestone_branch_template
771
- .replace('{milestone}', milestone['version'])
835
+ .replace('{milestone}', milestone['version'] ?? '')
772
836
  .replace('{slug}', generateSlugInternal(milestone['name']) || 'milestone')
773
837
  : null,
774
- milestone_version: milestone['version'],
775
- milestone_name: milestone['name'],
838
+ milestone_version: milestone['version'] ?? null,
839
+ milestone_name: milestone['name'] ?? null,
776
840
  milestone_slug: generateSlugInternal(milestone['name']),
777
841
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
778
842
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
779
843
  config_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'config.json')),
780
844
  // #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')),
845
+ // #3188: null when the file is absent (parity with patterns_path/context_path).
846
+ state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
847
+ roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
783
848
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
784
849
  // #2376: execute-phase.md's verify_phase_goal step reads this instead of
785
850
  // hardcoding '.planning/REQUIREMENTS.md' into the gsd-verifier spawn prompt.
786
- requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
851
+ requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
787
852
  };
788
853
  if (options['validate']) {
789
854
  try {
@@ -860,6 +925,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
860
925
  assertValidGranularityOverride(granularityOverride, error);
861
926
  const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
862
927
  const wf = (config.workflow ?? {});
928
+ // #3188: see cmdInitExecutePhase — null when absent, parity with the
929
+ // conditional sibling fields in this same result object.
930
+ const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
931
+ const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
932
+ const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
863
933
  const result = {
864
934
  researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
865
935
  planner_model: resolveModelInternal(cwd, 'gsd-planner'),
@@ -896,42 +966,89 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
896
966
  planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
897
967
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
898
968
  // #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')),
969
+ // #3188: null when the file is absent (parity with patterns_path below).
970
+ state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
971
+ roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
972
+ requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
902
973
  patterns_path: null,
903
974
  };
904
975
  if (phaseInfo?.['directory']) {
905
976
  const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']);
906
977
  try {
907
978
  const files = node_fs_1.default.readdirSync(phaseDirFull);
908
- const contextFile = findContextMdIn(phaseDirFull);
979
+ const phaseDirName = node_path_1.default.basename(phaseDirFull);
980
+ // #3511 BLOCKER-3: scope the raw listing to THIS phase's own artifacts
981
+ // before any bare `.find()` predicate runs, so a `04-UAT.md` (or
982
+ // `04-RESEARCH.md`/`04-REVIEWS.md`/`04-PATTERNS.md`) sitting in phase
983
+ // 03's directory cannot win a phase-03 lookup — the same
984
+ // `isPhaseArtifact` membership rule `resolveVerificationFile` already
985
+ // applies via `phaseDirName` below. `findContextMdIn` is passed the
986
+ // scoped array (rather than the raw directory path) so this call site
987
+ // alone is scoped; its other call sites are unaffected.
988
+ const scopedFiles = scopeToPhase(files, phaseDirName);
989
+ const contextFile = findContextMdIn(scopedFiles);
909
990
  if (contextFile) {
910
991
  result['context_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, contextFile));
911
992
  }
912
- const researchFile = files.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
993
+ const researchFile = scopedFiles.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
913
994
  if (researchFile) {
914
995
  result['research_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, researchFile));
915
996
  }
916
- const verificationFile = files.find((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
997
+ // #3473 F2: routed through the shared resolver — readdir order is
998
+ // filesystem-dependent, so the prior hand-rolled `.find()` could pick
999
+ // either file when a phase held both a canonical report and an ad-hoc
1000
+ // `-CORRECTION-VERIFICATION.md` worksheet (#3357).
1001
+ // #3492: pin selection to THIS phase's own token so a stray cross-phase
1002
+ // or sentinel-numbered canonically-shaped file cannot outrank this
1003
+ // phase's own (possibly non-canonical) report.
1004
+ const phaseToken = extractPhaseToken(phaseDirName);
1005
+ const verificationFile = resolveVerificationFile(files, {
1006
+ allowBare: true,
1007
+ phaseToken,
1008
+ phaseDirName,
1009
+ });
917
1010
  if (verificationFile) {
918
1011
  result['verification_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, verificationFile));
919
1012
  }
920
- const uatFile = files.find((f) => f.endsWith('-UAT.md') || f === 'UAT.md');
1013
+ // #3518: routed through the shared UAT resolver — the prior hand-rolled
1014
+ // `.find()` over unsorted readdir order had no phase check and no
1015
+ // ordering, so a stray cross-phase 02-UAT.md could become this phase's
1016
+ // uat_path, filesystem-dependently. Pinned to this phase's own token
1017
+ // (same rule as verification_path above), and phase-scoped via
1018
+ // phaseDirName (#3511) so the alphabetically-first fallback tier also
1019
+ // excludes cross-phase strays.
1020
+ const uatFile = resolveUatFile(files, {
1021
+ allowBare: true,
1022
+ phaseToken,
1023
+ phaseDirName,
1024
+ });
921
1025
  if (uatFile) {
922
1026
  result['uat_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, uatFile));
923
1027
  }
924
- const reviewsFile = files.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
1028
+ const reviewsFile = scopedFiles.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
925
1029
  if (reviewsFile) {
926
1030
  result['reviews_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, reviewsFile));
927
1031
  }
928
- const patternsFile = files.find((f) => f.endsWith('-PATTERNS.md') || f === 'PATTERNS.md');
1032
+ const patternsFile = scopedFiles.find((f) => f.endsWith('-PATTERNS.md') || f === 'PATTERNS.md');
929
1033
  if (patternsFile) {
930
1034
  result['patterns_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, patternsFile));
931
1035
  }
932
1036
  }
933
- catch {
934
- /* intentionally empty */
1037
+ catch (err) {
1038
+ // #3885 (ADR-3473 §8.5): this branch means `phaseInfo['directory']` was
1039
+ // set (the phase was already resolved to an on-disk directory) yet
1040
+ // `readdirSync` still failed — ENOENT here would be a genuine race
1041
+ // (the directory vanished between resolution and this read) and stays
1042
+ // a silent degrade like the prior behavior; any other errno
1043
+ // (EACCES/EIO/...) is an unreadable-not-absent directory and must be
1044
+ // named, or every conditional field this block sets (context_path,
1045
+ // research_path, verification_path, uat_path, reviews_path,
1046
+ // patterns_path) silently reads as "none of these exist".
1047
+ const code = err?.code;
1048
+ if (code !== 'ENOENT') {
1049
+ result['context_read_error'] =
1050
+ `Could not read phase directory ${formatDiagnosticToken(phaseDirFull)}: ${formatDiagnosticToken(err?.message ?? String(err))}`;
1051
+ }
935
1052
  }
936
1053
  }
937
1054
  if (options['validate']) {
@@ -958,6 +1075,29 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
958
1075
  }
959
1076
  // #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
960
1077
  result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, options, 'plan-phase');
1078
+ // #2401: prior-phase verify commands, surfaced UNGATED — additive field, never
1079
+ // conditioned on context_window. Before this, the planner only inherited
1080
+ // prior-phase verify-command context when context_window >= 500000, so at
1081
+ // lower context windows it re-invented (and mis-resolved) the command. The
1082
+ // harvest already degrades to `{commands: [], readError}` rather than
1083
+ // throwing; the try/catch is defense-in-depth so init never breaks on this.
1084
+ let priorVerifyCommands = [];
1085
+ try {
1086
+ // #2401 review fix: harvestPriorVerifyCommands accepts a phase-id token
1087
+ // (string) directly, so a decimal phase like '2.1' is no longer silently
1088
+ // dropped by `Number('2.1')` producing a value the old `number`-only
1089
+ // parameter mishandled for lettered/decimal tokens.
1090
+ if (phaseNumberPlan !== null) {
1091
+ priorVerifyCommands = harvestPriorVerifyCommands({
1092
+ planningDir: planningPaths(cwd).phases,
1093
+ beforePhase: phaseNumberPlan,
1094
+ }).commands;
1095
+ }
1096
+ }
1097
+ catch {
1098
+ priorVerifyCommands = [];
1099
+ }
1100
+ result['prior_verify_commands'] = priorVerifyCommands;
961
1101
  output(withProjectRoot(cwd, result), raw);
962
1102
  }
963
1103
  function cmdInitNewProject(cwd, raw, options = {}) {
@@ -979,7 +1119,7 @@ function cmdInitNewProject(cwd, raw, options = {}) {
979
1119
  synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'),
980
1120
  roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
981
1121
  commit_docs: config.commit_docs,
982
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1122
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
983
1123
  has_codebase_map: hasCodebaseMap,
984
1124
  planning_exists: pathExistsInternal(cwd, '.planning'),
985
1125
  has_existing_code: hasCode,
@@ -1010,22 +1150,14 @@ function cmdInitNewProject(cwd, raw, options = {}) {
1010
1150
  }
1011
1151
  function cmdInitNewMilestone(cwd, raw, options = {}) {
1012
1152
  const config = loadConfig(cwd);
1013
- const milestone = getMilestoneInfo(cwd);
1153
+ const milestone = milestoneRecord(cwd);
1014
1154
  const latestCompleted = getLatestCompletedMilestone(cwd);
1015
1155
  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
- }
1156
+ // #3185 (ADR-3180 Decision 1): "how many phase directories belong to the
1157
+ // CURRENT milestone" is exactly the scoped question listMilestonePhaseDirs
1158
+ // owns — routed through it instead of a local readdirSync + hand-rolled
1159
+ // window filter (which also never excluded sentinels, unlike the owner).
1160
+ const phaseDirCount = listMilestonePhaseDirs(phasesDir, { cwd }).value.length;
1029
1161
  const wf = (config.workflow ?? {});
1030
1162
  const result = {
1031
1163
  researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
@@ -1033,8 +1165,12 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1033
1165
  roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
1034
1166
  commit_docs: config.commit_docs,
1035
1167
  research_enabled: wf['research'],
1036
- current_milestone: milestone['version'],
1037
- current_milestone_name: milestone['name'],
1168
+ // #3216 review Finding 2: `?? null` so an unresolved milestone still emits
1169
+ // the key with an explicit `null` rather than letting JSON.stringify drop
1170
+ // it — an omitted key reaches the prompt layer's `{current_milestone}`
1171
+ // placeholder as literal, un-substituted text.
1172
+ current_milestone: milestone['version'] ?? null,
1173
+ current_milestone_name: milestone['name'] ?? null,
1038
1174
  latest_completed_milestone: latestCompleted?.version || null,
1039
1175
  latest_completed_milestone_name: latestCompleted?.name || null,
1040
1176
  phase_dir_count: phaseDirCount,
@@ -1042,7 +1178,7 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1042
1178
  phase_archive_path: latestCompleted
1043
1179
  ? toPosixPath(node_path_1.default.join(planningRoot(cwd), 'milestones', `${latestCompleted.version}-phases`))
1044
1180
  : null,
1045
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1181
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1046
1182
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1047
1183
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1048
1184
  project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
@@ -1070,7 +1206,13 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1070
1206
  // source as `cmdInitTransition`: `GSD_WORKSTREAM` env, falling back to the
1071
1207
  // stored active-workstream pointer (mirrors `cmdInitProgress`'s own
1072
1208
  // resolution above).
1073
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
1209
+ //
1210
+ // #3579 root-cause fix: this is a read-only informational field (no write
1211
+ // follows), so use the non-mutating peek — getActiveWorkstream's self-heal
1212
+ // would otherwise silently delete a stale/invalid pointer as a side effect
1213
+ // of building a JSON report field, and (per #3579) could change what a
1214
+ // LATER resolution in the same process observes.
1215
+ const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
1074
1216
  const workstreamActive = !!resolvedWorkstream;
1075
1217
  const flatMode = !workstreamActive;
1076
1218
  // #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
@@ -1082,7 +1224,7 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1082
1224
  }
1083
1225
  function cmdInitQuick(cwd, description, raw, options = {}) {
1084
1226
  const config = loadConfig(cwd);
1085
- const now = new Date();
1227
+ const now = new Date(clock_cjs_1.realClock.now());
1086
1228
  const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
1087
1229
  const yy = String(now.getFullYear()).slice(-2);
1088
1230
  const mm = String(now.getMonth() + 1).padStart(2, '0');
@@ -1142,7 +1284,7 @@ function cmdInitQuick(cwd, description, raw, options = {}) {
1142
1284
  function cmdInitIngestDocs(cwd, raw) {
1143
1285
  const config = loadConfig(cwd);
1144
1286
  const result = {
1145
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1287
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1146
1288
  planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
1147
1289
  ...getInitGitState(cwd),
1148
1290
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase. The
@@ -1183,7 +1325,7 @@ function cmdInitResume(cwd, raw) {
1183
1325
  const result = {
1184
1326
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1185
1327
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1186
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1328
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1187
1329
  planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
1188
1330
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1189
1331
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
@@ -1500,9 +1642,11 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1500
1642
  directory: null,
1501
1643
  phase_number: roadmapPhase['phase_number'],
1502
1644
  phase_name: phaseName,
1503
- phase_slug: phaseName
1504
- ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
1505
- : null,
1645
+ // #3883 (ADR-3473 §8.3): delegate to the canonical slug formula
1646
+ // (generateSlugInternal, core-utils.cts) rather than re-implementing
1647
+ // it. `maxLen: null` preserves this site's pre-migration untruncated
1648
+ // contract.
1649
+ phase_slug: phaseName ? coreUtils.generateSlugInternal(phaseName, null) : null,
1506
1650
  plans: [],
1507
1651
  summaries: [],
1508
1652
  incomplete_plans: [],
@@ -1521,9 +1665,11 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1521
1665
  directory: null,
1522
1666
  phase_number: roadmapPhase['phase_number'],
1523
1667
  phase_name: phaseName,
1524
- phase_slug: phaseName
1525
- ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
1526
- : null,
1668
+ // #3883 (ADR-3473 §8.3): delegate to the canonical slug formula
1669
+ // (generateSlugInternal, core-utils.cts) rather than re-implementing
1670
+ // it. `maxLen: null` preserves this site's pre-migration untruncated
1671
+ // contract.
1672
+ phase_slug: phaseName ? coreUtils.generateSlugInternal(phaseName, null) : null,
1527
1673
  plans: [],
1528
1674
  summaries: [],
1529
1675
  incomplete_plans: [],
@@ -1548,6 +1694,11 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1548
1694
  expectedPhaseDir = toPosixPath(node_path_1.default.join(planningPaths(cwd).phases, dirName));
1549
1695
  }
1550
1696
  }
1697
+ // #3188: see cmdInitExecutePhase — null when absent, parity with the
1698
+ // conditional sibling fields in this same result object.
1699
+ const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
1700
+ const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
1701
+ const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
1551
1702
  const result = {
1552
1703
  commit_docs: config.commit_docs,
1553
1704
  brave_search: typeof config.brave_search === 'string'
@@ -1576,37 +1727,75 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1576
1727
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1577
1728
  planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
1578
1729
  // #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')),
1730
+ // #3188: null when the file is absent (parity with context_path/research_path).
1731
+ state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
1732
+ roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
1733
+ requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
1582
1734
  };
1583
1735
  if (phaseInfo?.['directory']) {
1584
1736
  const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']);
1585
1737
  try {
1586
1738
  const files = node_fs_1.default.readdirSync(phaseDirFull);
1587
- const contextFile = findContextMdIn(phaseDirFull);
1739
+ const phaseDirName = node_path_1.default.basename(phaseDirFull);
1740
+ // #3511 BLOCKER-3: see the parallel site above — scope before any bare
1741
+ // `.find()` predicate so a misfiled cross-phase artifact cannot win.
1742
+ const scopedFiles = scopeToPhase(files, phaseDirName);
1743
+ const contextFile = findContextMdIn(scopedFiles);
1588
1744
  if (contextFile) {
1589
1745
  result['context_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, contextFile));
1590
1746
  }
1591
- const researchFile = files.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
1747
+ const researchFile = scopedFiles.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
1592
1748
  if (researchFile) {
1593
1749
  result['research_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, researchFile));
1594
1750
  }
1595
- const verificationFile = files.find((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
1751
+ // #3473 F2: routed through the shared resolver — readdir order is
1752
+ // filesystem-dependent, so the prior hand-rolled `.find()` could pick
1753
+ // either file when a phase held both a canonical report and an ad-hoc
1754
+ // `-CORRECTION-VERIFICATION.md` worksheet (#3357).
1755
+ // #3492: pin selection to THIS phase's own token so a stray cross-phase
1756
+ // or sentinel-numbered canonically-shaped file cannot outrank this
1757
+ // phase's own (possibly non-canonical) report.
1758
+ const phaseToken = extractPhaseToken(phaseDirName);
1759
+ const verificationFile = resolveVerificationFile(files, {
1760
+ allowBare: true,
1761
+ phaseToken,
1762
+ phaseDirName,
1763
+ });
1596
1764
  if (verificationFile) {
1597
1765
  result['verification_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, verificationFile));
1598
1766
  }
1599
- const uatFile = files.find((f) => f.endsWith('-UAT.md') || f === 'UAT.md');
1767
+ // #3518: routed through the shared UAT resolver — the prior hand-rolled
1768
+ // `.find()` over unsorted readdir order had no phase check and no
1769
+ // ordering, so a stray cross-phase 02-UAT.md could become this phase's
1770
+ // uat_path, filesystem-dependently. Pinned to this phase's own token
1771
+ // (same rule as verification_path above), and phase-scoped via
1772
+ // phaseDirName (#3511) so the alphabetically-first fallback tier also
1773
+ // excludes cross-phase strays.
1774
+ const uatFile = resolveUatFile(files, {
1775
+ allowBare: true,
1776
+ phaseToken,
1777
+ phaseDirName,
1778
+ });
1600
1779
  if (uatFile) {
1601
1780
  result['uat_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, uatFile));
1602
1781
  }
1603
- const reviewsFile = files.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
1782
+ const reviewsFile = scopedFiles.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
1604
1783
  if (reviewsFile) {
1605
1784
  result['reviews_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, reviewsFile));
1606
1785
  }
1607
1786
  }
1608
- catch {
1609
- /* intentionally empty */
1787
+ catch (err) {
1788
+ // #3885 (ADR-3473 §8.5): see the parallel site in cmdInitPlanPhase —
1789
+ // ENOENT here is a genuine race (directory vanished after resolution)
1790
+ // and stays a silent degrade; any other errno (EACCES/EIO/...) means
1791
+ // the directory exists but could not be read, and must be named rather
1792
+ // than silently reported the same as "none of context_path/
1793
+ // research_path/verification_path/uat_path/reviews_path exist".
1794
+ const code = err?.code;
1795
+ if (code !== 'ENOENT') {
1796
+ result['context_read_error'] =
1797
+ `Could not read phase directory ${formatDiagnosticToken(phaseDirFull)}: ${formatDiagnosticToken(err?.message ?? String(err))}`;
1798
+ }
1610
1799
  }
1611
1800
  }
1612
1801
  output(withProjectRoot(cwd, result), raw);
@@ -1669,7 +1858,7 @@ function cmdInitTodos(cwd, area, raw) {
1669
1858
  }
1670
1859
  function cmdInitMilestoneOp(cwd, raw) {
1671
1860
  const config = loadConfig(cwd);
1672
- const milestone = getMilestoneInfo(cwd);
1861
+ const milestone = milestoneRecord(cwd);
1673
1862
  let phaseCount = 0;
1674
1863
  let completedPhases = 0;
1675
1864
  const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
@@ -1682,7 +1871,8 @@ function cmdInitMilestoneOp(cwd, raw) {
1682
1871
  const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
1683
1872
  let m;
1684
1873
  while ((m = phasePattern.exec(currentSection)) !== null) {
1685
- if (/^999(?:\.|$)/.test(m[1]))
1874
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
1875
+ if (isSentinelPhaseId(m[1]))
1686
1876
  continue;
1687
1877
  roadmapPhaseNumbers.push(m[1]);
1688
1878
  }
@@ -1694,20 +1884,23 @@ function cmdInitMilestoneOp(cwd, raw) {
1694
1884
  const m = tok.match(/^(\d+)([A-Z]?(?:\.\d+)*)$/);
1695
1885
  return m ? String(parseInt(m[1], 10)) + m[2] : tok;
1696
1886
  };
1887
+ // #3882 (ADR-3473 §8.2): this used to hand-roll a readdirSync over the
1888
+ // phases directory (a heading->directory LOOKUP INDEX, same role as
1889
+ // cmdRoadmapAnalyze's `_phaseDirNames` — `roadmapPhaseNumbers` above is
1890
+ // already scoped/sentinel-excluded, so this map must see the PHYSICAL set
1891
+ // to resolve each heading's phase number to its actual directory name;
1892
+ // scoping it again would look up inside an already-scoped set for no
1893
+ // benefit). Routed through the named "physical set, sentinels included"
1894
+ // axis instead: every `num` looked up below came from `roadmapPhaseNumbers`
1895
+ // (sentinels already excluded there), so a sentinel entry surviving in
1896
+ // this map is never read — inclusion is output-invariant, this only
1897
+ // removes the re-derivation.
1697
1898
  const diskPhaseDirs = new Map();
1698
- try {
1699
- const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
1700
- for (const e of entries) {
1701
- if (!e.isDirectory())
1702
- continue;
1703
- const m = stripProjectCodePrefix(e.name).match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`));
1704
- if (!m)
1705
- continue;
1706
- diskPhaseDirs.set(canonicalizePhase(m[1]), e.name);
1707
- }
1708
- }
1709
- catch {
1710
- /* intentionally empty */
1899
+ for (const name of listAllPhaseDirs(phasesDir, { includeSentinels: true }).value) {
1900
+ const m = stripProjectCodePrefix(name).match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`));
1901
+ if (!m)
1902
+ continue;
1903
+ diskPhaseDirs.set(canonicalizePhase(m[1]), name);
1711
1904
  }
1712
1905
  if (roadmapPhaseNumbers.length > 0) {
1713
1906
  phaseCount = roadmapPhaseNumbers.length;
@@ -1727,8 +1920,12 @@ function cmdInitMilestoneOp(cwd, raw) {
1727
1920
  }
1728
1921
  else {
1729
1922
  try {
1730
- const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
1731
- const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
1923
+ // #3185 (ADR-3180 Decision 1): the ROADMAP heading scan above found no
1924
+ // current-milestone phase headings — fall back to asking the canonical
1925
+ // owner "which phase directories belong to the current milestone"
1926
+ // directly, instead of a hand-rolled readdirSync over every directory
1927
+ // on disk (which also never excluded sentinels, unlike the owner).
1928
+ const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
1732
1929
  phaseCount = dirs.length;
1733
1930
  for (const dir of dirs) {
1734
1931
  try {
@@ -1758,15 +1955,19 @@ function cmdInitMilestoneOp(cwd, raw) {
1758
1955
  }
1759
1956
  const result = {
1760
1957
  commit_docs: config.commit_docs,
1761
- milestone_version: milestone['version'],
1762
- milestone_name: milestone['name'],
1958
+ // #3216 review Finding 2: `?? null` so an unresolved milestone still emits
1959
+ // the key with an explicit `null` rather than letting JSON.stringify drop
1960
+ // it — an omitted key reaches the prompt layer's `{milestone_version}`
1961
+ // placeholder as literal, un-substituted text.
1962
+ milestone_version: milestone['version'] ?? null,
1963
+ milestone_name: milestone['name'] ?? null,
1763
1964
  milestone_slug: generateSlugInternal(milestone['name']),
1764
1965
  phase_count: phaseCount,
1765
1966
  completed_phases: completedPhases,
1766
1967
  all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
1767
1968
  archived_milestones: archivedMilestones,
1768
1969
  archive_count: archivedMilestones.length,
1769
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1970
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1770
1971
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1771
1972
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1772
1973
  archive_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningRoot(cwd), 'archive')),
@@ -1776,7 +1977,12 @@ function cmdInitMilestoneOp(cwd, raw) {
1776
1977
  }
1777
1978
  function cmdInitMapCodebase(cwd, raw) {
1778
1979
  const config = loadConfig(cwd);
1779
- const codebaseDir = node_path_1.default.join(planningRoot(cwd), 'codebase');
1980
+ // #3964: scoped like the payload's own codebase_dir/codebase_dir_exists
1981
+ // below (and verify.cts's codebase drift check) — has_maps/existing_maps
1982
+ // reading the flat root made the same payload claim a scoped codebase dir
1983
+ // exists while reporting zero maps, so map-codebase's Refresh/Skip gate
1984
+ // always forced a re-map under GSD_PROJECT.
1985
+ const codebaseDir = node_path_1.default.join(planningDir(cwd), 'codebase');
1780
1986
  let existingMaps = [];
1781
1987
  try {
1782
1988
  existingMaps = node_fs_1.default.readdirSync(codebaseDir).filter((f) => f.endsWith('.md'));
@@ -1793,17 +1999,20 @@ function cmdInitMapCodebase(cwd, raw) {
1793
1999
  date: clock_cjs_1.realClock.localToday(),
1794
2000
  timestamp: clock_cjs_1.realClock.nowIso(),
1795
2001
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1796
- codebase_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'codebase')),
2002
+ // #3964: scoped like verify.cts's codebase drift check (planningDir, not
2003
+ // the flat planningRoot) so the two surfaces cannot disagree under
2004
+ // GSD_PROJECT.
2005
+ codebase_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'codebase')),
1797
2006
  existing_maps: existingMaps,
1798
2007
  has_maps: existingMaps.length > 0,
1799
2008
  planning_exists: pathExistsInternal(cwd, '.planning'),
1800
- codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'),
2009
+ codebase_dir_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'codebase')))),
1801
2010
  };
1802
2011
  output(withProjectRoot(cwd, result), raw);
1803
2012
  }
1804
2013
  function cmdInitManager(cwd, raw) {
1805
2014
  const config = loadConfig(cwd);
1806
- const milestone = getMilestoneInfo(cwd);
2015
+ const milestone = milestoneRecord(cwd);
1807
2016
  const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
1808
2017
  const paths = planningPaths(cwd);
1809
2018
  if (!node_fs_1.default.existsSync(paths.roadmap)) {
@@ -1815,18 +2024,12 @@ function cmdInitManager(cwd, raw) {
1815
2024
  const rawContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf-8');
1816
2025
  const content = extractCurrentMilestone(rawContent, cwd);
1817
2026
  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
- })();
2027
+ // #3185 (ADR-3180 Decision 1): "which phase directories belong to the
2028
+ // CURRENT milestone" is the scoped question listMilestonePhaseDirs owns —
2029
+ // routed through it instead of a hand-rolled readdirSync + a separate
2030
+ // getMilestonePhaseFilter window check (which also never excluded
2031
+ // sentinels, unlike the owner).
2032
+ const _phaseDirEntries = listMilestonePhaseDirs(phasesDir, { cwd }).value;
1830
2033
  const _checkboxStates = new Map();
1831
2034
  const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
1832
2035
  let _cbMatch;
@@ -1861,16 +2064,24 @@ function cmdInitManager(cwd, raw) {
1861
2064
  let isActive = false;
1862
2065
  let completion = buildPhaseCompletionProjection(cwd, phaseNum, null, planCount, summaryCount, _slashRuntime);
1863
2066
  try {
1864
- const dirs = _phaseDirEntries.filter(isDirInMilestone);
1865
- const dirMatch = dirs.find((d) => phaseTokenMatches(d, normalized));
2067
+ // #3185 (ADR-3180 Decision 2) moved this lookup off the
2068
+ // milestone-scoped set and onto the physical one; that scope choice is
2069
+ // kept. Only the matcher is this PR's: matchPhaseDirs resolves
2070
+ // digit-leading directory names the token predicate cannot (#2528).
2071
+ const dirMatch = matchPhaseDirs(_phaseDirEntries, normalized).matches[0];
1866
2072
  if (dirMatch) {
1867
2073
  const fullDir = node_path_1.default.join(phasesDir, dirMatch);
1868
2074
  const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, fullDir));
1869
2075
  const phaseFiles = node_fs_1.default.readdirSync(fullDir);
1870
2076
  planCount = listPhasePlanFiles(fullDir).length;
1871
2077
  summaryCount = listPhaseSummaryFiles(fullDir).length;
1872
- hasContext = findContextMdIn(fullDir) !== null;
1873
- hasResearch = phaseFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
2078
+ // #3511-class: scope the raw listing to THIS phase's own artifacts
2079
+ // before the hasContext/hasResearch predicates run, so a stray
2080
+ // cross-phase `-CONTEXT.md`/`-RESEARCH.md` sitting in this directory
2081
+ // cannot win this phase's lookup.
2082
+ const scopedFiles = scopeToPhase(phaseFiles, dirMatch);
2083
+ hasContext = findContextMdIn(scopedFiles) !== null;
2084
+ hasResearch = scopedFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
1874
2085
  completion = buildPhaseCompletionProjection(cwd, phaseNum, phaseDirRel, planCount, summaryCount, _slashRuntime);
1875
2086
  if (completion.phase_complete)
1876
2087
  diskStatus = 'complete';
@@ -1886,7 +2097,7 @@ function cmdInitManager(cwd, raw) {
1886
2097
  diskStatus = 'discussed';
1887
2098
  else
1888
2099
  diskStatus = 'empty';
1889
- const nowMs = Date.now();
2100
+ const nowMs = clock_cjs_1.realClock.now();
1890
2101
  let newestMtime = 0;
1891
2102
  for (const f of phaseFiles) {
1892
2103
  try {
@@ -1907,18 +2118,20 @@ function cmdInitManager(cwd, raw) {
1907
2118
  catch {
1908
2119
  /* intentionally empty */
1909
2120
  }
2121
+ // ADR-3180 §7.4 (disk-strict, #2957, maintainer decision 2026-08-08):
2122
+ // `roadmapComplete` is reported below as metadata only — it carries NO
2123
+ // machine authority over `diskStatus`. The #3033 checkbox override that
2124
+ // used to live here (treating a zero-plan phase as complete whenever the
2125
+ // ROADMAP checkbox was ticked, layered on top of
2126
+ // buildPhaseCompletionProjection's own output) is DELETED, not
2127
+ // generalized: `diskStatus` now comes entirely from `completion`, which
2128
+ // already routes through the canonical owner (`isPhaseComplete`) and
2129
+ // itself resolves a zero-plan phase as complete whenever a passing
2130
+ // `*-VERIFICATION.md` exists (#3168) — with no dependency on the
2131
+ // checkbox. A zero-plan phase whose completion previously relied SOLELY
2132
+ // on a ticked checkbox (no passing verification) now reports incomplete;
2133
+ // this is the deliberate Tier-2 break (ADR-3180 §7.4 Decision 3).
1910
2134
  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
2135
  phases.push({
1923
2136
  number: phaseNum,
1924
2137
  name: phaseName,
@@ -2010,7 +2223,13 @@ function cmdInitManager(cwd, raw) {
2010
2223
  }
2011
2224
  let waitingSignal = null;
2012
2225
  try {
2013
- const waitingPath = node_path_1.default.join(cwd, '.planning', 'WAITING.json');
2226
+ // #3964: mirror cmdSignalWaiting's write locations exactly — `.gsd/`
2227
+ // first when it exists, else the project-aware planning dir — so the
2228
+ // signal is read from the project (and location) it is written to.
2229
+ const gsdWaiting = node_path_1.default.join(cwd, '.gsd', 'WAITING.json');
2230
+ const waitingPath = node_fs_1.default.existsSync(node_path_1.default.join(cwd, '.gsd'))
2231
+ ? gsdWaiting
2232
+ : node_path_1.default.join(planningDir(cwd), 'WAITING.json');
2014
2233
  const waitingRaw = (0, shell_command_projection_cjs_1.platformReadSync)(waitingPath);
2015
2234
  if (waitingRaw !== null) {
2016
2235
  waitingSignal = JSON.parse(waitingRaw);
@@ -2023,7 +2242,8 @@ function cmdInitManager(cwd, raw) {
2023
2242
  for (const phase of phases) {
2024
2243
  if (phase['disk_status'] === 'complete')
2025
2244
  continue;
2026
- if (/^999(?:\.|$)/.test(phase['number']))
2245
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
2246
+ if (isSentinelPhaseId(phase['number']))
2027
2247
  continue;
2028
2248
  if (phase['disk_status'] === 'executed') {
2029
2249
  recommendedActions.push({
@@ -2077,7 +2297,8 @@ function cmdInitManager(cwd, raw) {
2077
2297
  }
2078
2298
  return true;
2079
2299
  });
2080
- const nonBacklogPhases = phases.filter((p) => !/^999(?:\.|$)/.test(p['number']));
2300
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
2301
+ const nonBacklogPhases = phases.filter((p) => !isSentinelPhaseId(p['number']));
2081
2302
  const completedCount = nonBacklogPhases.filter((p) => p['phase_complete'] === true).length;
2082
2303
  const sanitizeFlags = (rawVal) => {
2083
2304
  const val = typeof rawVal === 'string' ? rawVal : '';
@@ -2100,8 +2321,12 @@ function cmdInitManager(cwd, raw) {
2100
2321
  execute: sanitizeFlags(mgrFlags?.['execute']),
2101
2322
  };
2102
2323
  const result = {
2103
- milestone_version: milestone['version'],
2104
- milestone_name: milestone['name'],
2324
+ // #3216 review Finding 2: `?? null` so an unresolved milestone still emits
2325
+ // the key with an explicit `null` rather than letting JSON.stringify drop
2326
+ // it — an omitted key reaches the prompt layer's `{milestone_version}`
2327
+ // placeholder as literal, un-substituted text.
2328
+ milestone_version: milestone['version'] ?? null,
2329
+ milestone_name: milestone['name'] ?? null,
2105
2330
  phases,
2106
2331
  phase_count: phases.length,
2107
2332
  completed_count: completedCount,
@@ -2109,7 +2334,7 @@ function cmdInitManager(cwd, raw) {
2109
2334
  recommended_actions: filteredActions,
2110
2335
  waiting_signal: waitingSignal,
2111
2336
  all_complete: completedCount === nonBacklogPhases.length && nonBacklogPhases.length > 0,
2112
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
2337
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
2113
2338
  roadmap_exists: true,
2114
2339
  state_exists: true,
2115
2340
  manager_flags: managerFlags,
@@ -2212,7 +2437,7 @@ function cmdInitDocsUpdate(cwd, raw, options = {}) {
2212
2437
  * `state:chunked-mode`/`state:plan-strategy-converge`). This DELIBERATELY
2213
2438
  * does not replace `update.md`'s own `parse_update_channel` case-statement
2214
2439
  * (`TAG="next"`/`TAG="latest"`) — issue #815's regression test
2215
- * (`tests/issue-815-update-next-channel.test.cjs`) asserts that literal
2440
+ * (`tests/update-workflow.test.cjs`) asserts that literal
2216
2441
  * case-statement text stays in `update.md` verbatim (the npm dist-tag
2217
2442
  * selection has to run in the workflow's own shell before any `gsd_run`
2218
2443
  * round-trip), so `next_channel` exists purely to gate the `channel-banner`
@@ -2258,7 +2483,9 @@ function cmdInitUpdate(cwd, raw, options = {}) {
2258
2483
  * pure JSON consumer with no `gsd_run` call of its own.
2259
2484
  */
2260
2485
  function cmdInitTransition(cwd, raw, options = {}) {
2261
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
2486
+ // #3579 root-cause fix: read-only informational field — peek, don't
2487
+ // self-heal (see cmdInitNewMilestone's identical rationale above).
2488
+ const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
2262
2489
  const workstreamActive = !!resolvedWorkstream;
2263
2490
  const result = {
2264
2491
  other_active_workstreams: workstreamActive
@@ -2339,7 +2566,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
2339
2566
  /* intentionally empty */
2340
2567
  }
2341
2568
  const config = loadConfig(cwd);
2342
- const milestone = getMilestoneInfo(cwd);
2569
+ const milestone = milestoneRecord(cwd);
2343
2570
  const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
2344
2571
  // #1912: fail safe in workstream mode with no active workstream. With no active
2345
2572
  // workstream and no --ws, planningDir(cwd) resolves to root .planning — silently
@@ -2347,11 +2574,27 @@ function cmdInitProgress(cwd, raw, options = {}) {
2347
2574
  // Mirror planningDir's resolution (GSD_WORKSTREAM env > stored active pointer) so
2348
2575
  // an explicit --ws (which sets GSD_WORKSTREAM) satisfies the check.
2349
2576
  const _availableWorkstreams = listAvailableWorkstreams(cwd);
2350
- const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
2577
+ // #3579 root-cause fix: this is a check, not a consuming read — use the
2578
+ // non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
2579
+ // here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
2580
+ // which would misreport a present-but-bad marker as no marker at all.
2581
+ const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
2351
2582
  if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) {
2583
+ // #3579: getActiveWorkstream now inherits a pointer-less session's read
2584
+ // from the shared .planning/active-workstream marker, so reaching this
2585
+ // branch with a marker actually present means the marker EXISTED but
2586
+ // didn't resolve (invalid name, or its workstream dir is gone) — a
2587
+ // materially different situation from "nothing was ever set" and one
2588
+ // that deserves its own diagnostic instead of the generic message below.
2589
+ const _diagnosis = diagnoseUnresolvedActiveWorkstream(cwd);
2590
+ if (_diagnosis.present) {
2591
+ 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. ` +
2592
+ `Pass --ws <name> or run ${(0, runtime_slash_cjs_1.formatGsdSlash)('workstream set', _slashRuntime)} to point it at an existing workstream. ` +
2593
+ `Available workstreams: ${_availableWorkstreams.join(', ')}`, ERROR_REASON.WORKSTREAM_MODE_MARKER_UNRESOLVED, { marker_value: _diagnosis.value, marker_reason: _diagnosis.reason });
2594
+ }
2352
2595
  error(`init.progress requires a workstream in workstream mode — no active workstream is set, so root STATE.md (likely stale) would be reported. ` +
2353
2596
  `Pass --ws <name> or run ${(0, runtime_slash_cjs_1.formatGsdSlash)('workstream set', _slashRuntime)} first. ` +
2354
- `Available workstreams: ${_availableWorkstreams.join(', ')}`);
2597
+ `Available workstreams: ${_availableWorkstreams.join(', ')}`, ERROR_REASON.WORKSTREAM_MODE_NONE_ACTIVE);
2355
2598
  }
2356
2599
  const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
2357
2600
  const phases = [];
@@ -2378,21 +2621,15 @@ function cmdInitProgress(cwd, raw, options = {}) {
2378
2621
  catch {
2379
2622
  /* intentionally empty */
2380
2623
  }
2381
- const isDirInMilestone = getMilestonePhaseFilter(cwd);
2382
2624
  const seenPhaseNums = new Set();
2383
2625
  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
- });
2626
+ // #3185 (ADR-3180 Decision 1): "which phase directories belong to the
2627
+ // CURRENT milestone" — routed through the canonical owner instead of a
2628
+ // hand-rolled readdirSync + isDirInMilestone filter + local sort (which
2629
+ // also never excluded sentinels, unlike the owner; the final `phases`
2630
+ // array is re-sorted below anyway, so dropping the local sort here is
2631
+ // behavior-preserving).
2632
+ const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
2396
2633
  for (const dir of dirs) {
2397
2634
  const dirMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})-?(.*)`, 'i'));
2398
2635
  const phaseNumber = dirMatch ? dirMatch[1] : dir;
@@ -2402,7 +2639,12 @@ function cmdInitProgress(cwd, raw, options = {}) {
2402
2639
  const phaseFiles = node_fs_1.default.readdirSync(phasePath);
2403
2640
  const plans = listPhasePlanFiles(phasePath);
2404
2641
  const summaries = listPhaseSummaryFiles(phasePath);
2405
- const hasResearch = phaseFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
2642
+ // #3511-class: scope the raw listing to THIS phase's own artifacts
2643
+ // before the hasResearch predicate runs, so a stray cross-phase
2644
+ // `-RESEARCH.md` sitting in this directory cannot win this phase's
2645
+ // lookup.
2646
+ const scopedPhaseFiles = scopeToPhase(phaseFiles, dir);
2647
+ const hasResearch = scopedPhaseFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
2406
2648
  const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'phases', dir)));
2407
2649
  const completion = buildPhaseCompletionProjection(cwd, phaseNumber, phaseDirRel, plans.length, summaries.length, _slashRuntime);
2408
2650
  const status = completion.phase_complete
@@ -2448,7 +2690,11 @@ function cmdInitProgress(cwd, raw, options = {}) {
2448
2690
  const status = 'not_started';
2449
2691
  const phaseInfo = {
2450
2692
  number: num,
2451
- name: name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
2693
+ // #3883 (ADR-3473 §8.3): delegate to the canonical slug formula
2694
+ // (generateSlugInternal, core-utils.cts) rather than re-implementing
2695
+ // it. `maxLen: null` preserves this site's pre-migration untruncated
2696
+ // contract.
2697
+ name: coreUtils.generateSlugInternal(name, null) ?? '',
2452
2698
  directory: null,
2453
2699
  status,
2454
2700
  plan_count: 0,
@@ -2464,6 +2710,25 @@ function cmdInitProgress(cwd, raw, options = {}) {
2464
2710
  }
2465
2711
  }
2466
2712
  phases.sort((a, b) => parseInt(a['number'], 10) - parseInt(b['number'], 10));
2713
+ // #3581: the frontier is ROADMAP ORDER, not artifact presence. The disk loop
2714
+ // above could claim nextPhase from a stray out-of-order artifact directory
2715
+ // (a phase-9 UAT evidence dir while roadmap phase 8 was pending and
2716
+ // unscaffolded), silently skipping 8 — and init.progress then disagreed with
2717
+ // roadmap.analyze on the same tree. Re-derive from the sorted union: the
2718
+ // first phase that has not begun ('pending' | 'not_started') and is not
2719
+ // roadmap-complete wins; artifacts still feed each entry's status and
2720
+ // completion (corroborating evidence) but no longer outrank the ordering.
2721
+ // Aligned trees derive the identical frontier as the loops above; an
2722
+ // all-complete milestone finds none and keeps nextPhase null for the
2723
+ // completion flow.
2724
+ {
2725
+ const frontier = phases.find((p) => {
2726
+ const st = p['status'];
2727
+ return (st === 'pending' || st === 'not_started') && p['roadmap_complete'] !== true;
2728
+ });
2729
+ if (frontier)
2730
+ nextPhase = frontier;
2731
+ }
2467
2732
  let pausedAt = null;
2468
2733
  const state = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'STATE.md'));
2469
2734
  if (state !== null) {
@@ -2488,8 +2753,12 @@ function cmdInitProgress(cwd, raw, options = {}) {
2488
2753
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
2489
2754
  planner_model: resolveModelInternal(cwd, 'gsd-planner'),
2490
2755
  commit_docs: config.commit_docs,
2491
- milestone_version: milestone['version'],
2492
- milestone_name: milestone['name'],
2756
+ // #3216 review Finding 2: `?? null` so an unresolved milestone still emits
2757
+ // the key with an explicit `null` rather than letting JSON.stringify drop
2758
+ // it — an omitted key reaches the prompt layer's `{milestone_version}`
2759
+ // placeholder as literal, un-substituted text.
2760
+ milestone_version: milestone['version'] ?? null,
2761
+ milestone_name: milestone['name'] ?? null,
2493
2762
  phases,
2494
2763
  phase_count: phases.length,
2495
2764
  completed_count: phases.filter((p) => p['status'] === 'complete').length,
@@ -2499,7 +2768,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
2499
2768
  paused_at: pausedAt,
2500
2769
  has_work_in_progress: !!currentPhase,
2501
2770
  phase_mvp_mode: phaseMvpMode,
2502
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
2771
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
2503
2772
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
2504
2773
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
2505
2774
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
@@ -3043,7 +3312,7 @@ function buildSkillManifest(cwd, skillsDir = null) {
3043
3312
  seenNamesInRoot.add(name);
3044
3313
  const description = frontmatter['description'] || '';
3045
3314
  const triggers = [];
3046
- const bodyMatch = content.match(/^---[\s\S]*?---\s*\n([\s\S]*)$/);
3315
+ const bodyMatch = content.match(/^---[\s\S]*?---\s*\r?\n([\s\S]*)$/);
3047
3316
  if (bodyMatch) {
3048
3317
  const body = bodyMatch[1];
3049
3318
  const triggerLines = body.match(/^TRIGGER\s+when:\s*(.+)$/gmi);
@@ -3134,7 +3403,9 @@ function cmdSkillManifest(cwd, args, raw) {
3134
3403
  const skillsDir = skillsDirIdx >= 0 && args[skillsDirIdx + 1] ? args[skillsDirIdx + 1] : null;
3135
3404
  const manifest = buildSkillManifest(cwd, skillsDir);
3136
3405
  if (args.includes('--write')) {
3137
- const planDir = node_path_1.default.join(cwd, '.planning');
3406
+ // #3964: write beside the project's own artifacts (planningDir is
3407
+ // project- and workstream-aware), not the flat root.
3408
+ const planDir = planningDir(cwd);
3138
3409
  if (node_fs_1.default.existsSync(planDir)) {
3139
3410
  const manifestPath = node_path_1.default.join(planDir, 'skill-manifest.json');
3140
3411
  (0, shell_command_projection_cjs_1.platformWriteSync)(manifestPath, JSON.stringify(manifest, null, 2));