@opengsd/gsd-core 1.9.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -22,8 +22,12 @@
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
24
  exports.deriveProgressFromRoadmap = deriveProgressFromRoadmap;
25
+ exports.clampPercentFromFraction = clampPercentFromFraction;
25
26
  exports.clampPercent = clampPercent;
26
27
  const markdown_table_cjs_1 = require("./markdown-table.cjs");
28
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
29
+ const phaseIdMod = require("./phase-id.cjs");
30
+ const { isSentinelPhaseId } = phaseIdMod;
27
31
  /**
28
32
  * Derive completed_phases, total_phases, and total_plans from ROADMAP content.
29
33
  * Root cause fix for issue #4 — see gen-phase-lifecycle.mjs for full documentation.
@@ -81,10 +85,11 @@ function deriveProgressFromRoadmap(roadmapContent) {
81
85
  const allRows = table.rows;
82
86
  const completed = allRows.filter((r) => /^complete$/i.test((r['Status'] ?? '').trim())).length;
83
87
  completedPhases = completed > 0 ? completed : null;
84
- // Data rows only (exclude 999.x backlog phases). Mirrors init.cts /^999(?:\.|$)/ filter.
88
+ // Data rows only (exclude sentinel phases 0 and 999.x).
89
+ // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
85
90
  const dataRows = allRows.filter((r) => {
86
91
  const phase = (r['Phase'] ?? '').trim();
87
- return /^\d/.test(phase) && !/^999\b/.test(phase);
92
+ return /^\d/.test(phase) && !isSentinelPhaseId(phase);
88
93
  });
89
94
  totalPhases = dataRows.length > 0 ? dataRows.length : null;
90
95
  let totalPlansSum = 0;
@@ -98,12 +103,32 @@ function deriveProgressFromRoadmap(roadmapContent) {
98
103
  }
99
104
  return { completedPhases, totalPhases, totalPlans };
100
105
  }
106
+ /**
107
+ * Compute progress percent clamped to 100 from an already-computed FRACTION.
108
+ *
109
+ * ADR-3180 Decision 7 (#3180): the completion-RATIO derivation has exactly one
110
+ * owner, and this is its kernel — the single place the `fraction -> integer
111
+ * percent` rounding and the 100 ceiling are expressed. `clampPercent` below is
112
+ * the count-shaped entry point and delegates here; a caller that already holds a
113
+ * fraction (rather than a completed/total pair) calls this directly instead of
114
+ * re-deriving `Math.min(100, Math.round(f * 100))` locally.
115
+ *
116
+ * Enforced by `scripts/lint-completion-ratio-drift.cjs`.
117
+ */
118
+ function clampPercentFromFraction(fraction) {
119
+ return Math.min(100, Math.round(fraction * 100));
120
+ }
101
121
  /**
102
122
  * Compute progress percent clamped to 100.
103
123
  * Root cause fix for issue #4 — see gen-phase-lifecycle.mjs for full documentation.
124
+ *
125
+ * A non-positive (or absent) denominator yields `0` — "nothing to complete" is
126
+ * reported as 0%, never as 100%. Every `.planning/` completion percentage in this
127
+ * codebase routes through here (ADR-3180 Decision 7); the `total > 0 ? ... : 0`
128
+ * ternary that used to precede each inline copy IS this function's first line.
104
129
  */
105
130
  function clampPercent(completed, total) {
106
131
  if (!total || total <= 0)
107
132
  return 0;
108
- return Math.min(100, Math.round((completed / total) * 100));
133
+ return clampPercentFromFraction(completed / total);
109
134
  }
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Dependencies (leaf modules only — no loadConfig):
14
14
  * - node:fs / node:path (stdlib)
15
- * - ./phase-id.cjs (normalizePhaseName, phaseTokenMatches, extractPhaseToken)
15
+ * - ./phase-id.cjs (normalizePhaseName, matchPhaseDirs, phaseNumberForMatch)
16
16
  * - ./core-utils.cjs (readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath)
17
17
  * - ./planning-workspace.cjs (planningDir)
18
18
  */
@@ -23,18 +23,104 @@ const node_fs_1 = __importDefault(require("node:fs"));
23
23
  const node_path_1 = __importDefault(require("node:path"));
24
24
  // eslint-disable-next-line @typescript-eslint/no-require-imports
25
25
  const phaseIdModule = require("./phase-id.cjs");
26
- const { normalizePhaseName, phaseTokenMatches, extractPhaseToken } = phaseIdModule;
26
+ const { normalizePhaseName, matchPhaseDirs, phaseNumberForMatch, isSentinelPhaseId, comparePhaseNum } = phaseIdModule;
27
27
  // eslint-disable-next-line @typescript-eslint/no-require-imports
28
28
  const coreUtilsModule = require("./core-utils.cjs");
29
- const { readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath } = coreUtilsModule;
29
+ const { readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath, findUnsummarizedPlans } = coreUtilsModule;
30
30
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
31
  const planningWorkspace = require("./planning-workspace.cjs");
32
32
  const { planningDir } = planningWorkspace;
33
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
34
+ const frontmatterModule = require("./frontmatter.cjs");
35
+ const { extractFrontmatter } = frontmatterModule;
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
37
+ const planDependencyGraphModule = require("./plan-dependency-graph.cjs");
38
+ const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted } = planDependencyGraphModule;
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ const roadmapParserModule = require("./roadmap-parser.cjs");
41
+ const { getMilestonePhaseFilter } = roadmapParserModule;
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const planningScopeMod = require("./planning-scope.cjs");
44
+ const { SCOPE } = planningScopeMod;
45
+ /**
46
+ * #2830: parse a plan file's `depends_on` frontmatter. Returns [] — never
47
+ * throws — on a missing/unreadable/malformed plan or absent field, matching
48
+ * this primitive's existing fail-safe posture (a plan directory this
49
+ * primitive can otherwise read must never throw here).
50
+ */
51
+ function parsePlanDependsOn(phaseDir, planFile) {
52
+ try {
53
+ const planPath = node_path_1.default.join(phaseDir, planFile);
54
+ const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
55
+ const fm = extractFrontmatter(content, planPath);
56
+ const fmDeps = fm['depends_on'];
57
+ if (Array.isArray(fmDeps))
58
+ return fmDeps.map(String);
59
+ if (typeof fmDeps === 'string' && fmDeps.trim() !== '')
60
+ return [fmDeps];
61
+ return [];
62
+ }
63
+ catch {
64
+ return [];
65
+ }
66
+ }
33
67
  // ─── Phase search helpers ─────────────────────────────────────────────────────
68
+ /**
69
+ * #2855: single source of truth for resolving and enumerating a project's
70
+ * (or, when a workstream is active, that workstream's OWN) archived-milestone
71
+ * directories — `<planningDir(cwd)>/milestones/vX.Y-phases/`. Both
72
+ * `findPhaseInternal`'s archive fallback and `getArchivedPhaseDirs` used to
73
+ * carry independent copies of this resolve-then-enumerate logic, which is
74
+ * exactly the shape that let the original #2855 bug (hardcoded root path)
75
+ * exist in one copy and not the other. Sharing this seam means a future
76
+ * change to how the archive tree is located only needs to happen once.
77
+ * Most-recent-milestone-first order, compared numerically segment-by-segment
78
+ * on the version (e.g. `v1.10` before `v1.9`) — NOT lexicographically. A
79
+ * lexicographic `.sort().reverse()` (the prior implementation) ranks `v1.9`
80
+ * ahead of `v1.10` because the string `"1.9"` sorts after `"1.10"`; that is
81
+ * deterministic but wrong for every double-digit-or-higher minor/patch
82
+ * version, and #3458 is what first surfaces archived phases in audit output
83
+ * where the misordering becomes user-visible.
84
+ * Never throws: an absent/unreadable milestones/ dir yields [].
85
+ */
86
+ function compareArchiveVersionDesc(aName, bName) {
87
+ const aParts = (aName.match(/^v([\d.]+)-phases$/)?.[1] ?? '').split('.').map(Number);
88
+ const bParts = (bName.match(/^v([\d.]+)-phases$/)?.[1] ?? '').split('.').map(Number);
89
+ const len = Math.max(aParts.length, bParts.length);
90
+ for (let i = 0; i < len; i++) {
91
+ const a = aParts[i] ?? 0;
92
+ const b = bParts[i] ?? 0;
93
+ if (a !== b)
94
+ return b - a; // descending: newest (numerically largest) first
95
+ }
96
+ return 0;
97
+ }
98
+ function listArchiveVersionDirs(cwd) {
99
+ const milestonesDir = node_path_1.default.join(planningDir(cwd), 'milestones');
100
+ if (!node_fs_1.default.existsSync(milestonesDir))
101
+ return [];
102
+ try {
103
+ const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
104
+ return milestoneEntries
105
+ .filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
106
+ .map(e => e.name)
107
+ .sort(compareArchiveVersionDesc)
108
+ .map(archiveName => ({
109
+ version: archiveName.match(/^(v[\d.]+)-phases$/)[1],
110
+ archivePath: node_path_1.default.join(milestonesDir, archiveName),
111
+ }));
112
+ }
113
+ catch {
114
+ return [];
115
+ }
116
+ }
34
117
  function searchPhaseInDir(baseDir, relBase, normalized) {
35
118
  try {
36
119
  const dirs = readSubdirectories(baseDir, true);
37
- const matches = dirs.filter(d => phaseTokenMatches(d, normalized));
120
+ // #2528: canonical two-pass selection (exact token match, then the
121
+ // bare-integer leading-digit-run fallback) shared with the find-phase and
122
+ // phase-plan-index scans — see phase-id.cts::matchPhaseDirs.
123
+ const { matches, usedBareFallback } = matchPhaseDirs(dirs, normalized);
38
124
  if (matches.length === 0)
39
125
  return null;
40
126
  // #2237: fail loud when multiple directories match the same bare phase
@@ -55,10 +141,13 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
55
141
  has_verification: false,
56
142
  has_reviews: false,
57
143
  ambiguous_matches: matches,
144
+ halted_plans: [],
145
+ blocked_by: {},
146
+ runnable_plans: [],
58
147
  };
59
148
  }
60
149
  const match = matches[0];
61
- const phaseToken = extractPhaseToken(match);
150
+ const phaseToken = phaseNumberForMatch(match, usedBareFallback);
62
151
  const phaseNumber = phaseToken || normalized;
63
152
  const afterToken = match.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
64
153
  const phaseName = afterToken || null;
@@ -66,16 +155,57 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
66
155
  const { plans: unsortedPlans, summaries: unsortedSummaries, hasResearch, hasContext, hasVerification, hasReviews } = getPhaseFileStats(phaseDir);
67
156
  const plans = unsortedPlans.sort();
68
157
  const summaries = unsortedSummaries.sort();
69
- const completedPlanIds = new Set(summaries.flatMap(s => {
70
- const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
71
- const canonical = extractCanonicalPlanId(s);
72
- return canonical === exact ? [exact] : [exact, canonical];
73
- }));
74
- const incompletePlans = plans.filter(p => {
75
- const planId = p.replace('-PLAN.md', '').replace('PLAN.md', '');
158
+ // #3183 (ADR-3180 Decision 2): the summary→plan pairing used to be a
159
+ // bespoke rule local to this function (a third pairing rule alongside
160
+ // scanPhasePlans's completion check and countMatchedSummaries). Routed
161
+ // through the canonical core-utils.findUnsummarizedPlans instead, which
162
+ // shares its `summaryCandidates` matching rule with countMatchedSummaries
163
+ // so the count and this named list can never disagree.
164
+ const incompletePlans = findUnsummarizedPlans(plans, summaries);
165
+ // #2830: reverse lookup from a completed plan's id (exact or canonical) to
166
+ // its actual summary filename. Shared builder (also used by phase.cts's
167
+ // cmdPhasePlanIndex) so the two can never disagree about which summary
168
+ // belongs to which plan.
169
+ const summaryFileByPlanId = buildSummaryFileIndex(summaries, extractCanonicalPlanId);
170
+ // #2830: this primitive previously never parsed depends_on at all — see
171
+ // src/plan-dependency-graph.cts's file header. Build the same
172
+ // PlanHaltNode[] shape phase.cts's cmdPhasePlanIndex builds (id resolution
173
+ // mirrors its planMap/canonicalToId pattern) and hand it to the ONE
174
+ // shared halt-propagation traversal so this reader and the wave-grouping
175
+ // reader can never diverge on the halt rule again.
176
+ const planIds = plans.map(p => p.replace('-PLAN.md', '').replace('PLAN.md', ''));
177
+ const planIdByLower = new Map(planIds.map(id => [id.toLowerCase(), id]));
178
+ const canonicalToPlanId = new Map(plans.map((p, i) => [extractCanonicalPlanId(p).toLowerCase(), planIds[i]]));
179
+ const haltNodes = plans.map((p, i) => {
180
+ const planId = planIds[i];
76
181
  const canonical = extractCanonicalPlanId(p);
77
- return !completedPlanIds.has(planId) && !completedPlanIds.has(canonical);
182
+ const summaryFile = summaryFileByPlanId.get(planId) ?? summaryFileByPlanId.get(canonical);
183
+ const halted = summaryFile !== undefined && isSummaryFileHalted(node_path_1.default.join(phaseDir, summaryFile));
184
+ const resolvedDependsOn = parsePlanDependsOn(phaseDir, p)
185
+ .map((dep) => {
186
+ const lower = dep.toLowerCase();
187
+ return planIdByLower.get(lower) ?? canonicalToPlanId.get(lower) ?? null;
188
+ })
189
+ .filter((id) => id !== null);
190
+ return { id: planId, resolvedDependsOn, halted };
78
191
  });
192
+ const { blockedBy } = computeHaltPropagation(haltNodes);
193
+ const haltedPlans = plans.filter((_, i) => haltNodes[i].halted);
194
+ const incompletePlanSet = new Set(incompletePlans);
195
+ const blockedByFiles = {};
196
+ const runnablePlans = [];
197
+ for (let i = 0; i < plans.length; i++) {
198
+ const p = plans[i];
199
+ if (!incompletePlanSet.has(p))
200
+ continue;
201
+ const causes = blockedBy.get(planIds[i]) ?? [];
202
+ if (causes.length > 0) {
203
+ blockedByFiles[p] = causes;
204
+ }
205
+ else {
206
+ runnablePlans.push(p);
207
+ }
208
+ }
79
209
  return {
80
210
  found: true,
81
211
  directory: toPosixPath(node_path_1.default.join(relBase, match)),
@@ -89,6 +219,9 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
89
219
  has_context: hasContext,
90
220
  has_verification: hasVerification,
91
221
  has_reviews: hasReviews,
222
+ halted_plans: haltedPlans,
223
+ blocked_by: blockedByFiles,
224
+ runnable_plans: runnablePlans,
92
225
  };
93
226
  }
94
227
  catch {
@@ -104,63 +237,130 @@ function findPhaseInternal(cwd, phase) {
104
237
  const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized);
105
238
  if (current)
106
239
  return current;
107
- const milestonesDir = node_path_1.default.join(cwd, '.planning', 'milestones');
108
- if (!node_fs_1.default.existsSync(milestonesDir))
109
- return null;
110
- try {
111
- const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
112
- const archiveDirs = milestoneEntries
113
- .filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
114
- .map(e => e.name)
115
- .sort()
116
- .reverse();
117
- for (const archiveName of archiveDirs) {
118
- const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
119
- const version = versionMatch[1];
120
- const archivePath = node_path_1.default.join(milestonesDir, archiveName);
121
- const relBase = '.planning/milestones/' + archiveName;
122
- const result = searchPhaseInDir(archivePath, relBase, normalized);
123
- if (result) {
124
- result.archived = version;
125
- return result;
126
- }
240
+ // #2855: scope the archived-milestone fallback to the SAME workstream as the
241
+ // active-phase search above (planningDir(cwd) resolves GSD_WORKSTREAM/GSD_PROJECT
242
+ // the identical way both places), not the hardcoded project-root tree. Archived
243
+ // phases genuinely live under a workstream's own `.planning/workstreams/<ws>/
244
+ // milestones/` — that is where archivePhaseDirectories (milestone.cts) writes
245
+ // them via the same planningDir(cwd) resolution. Hardcoding root here let a
246
+ // pending workstream phase resolve to an unrelated workstream's (or a flat-mode
247
+ // project's) archived phase that merely shares a phase number. Shared with
248
+ // getArchivedPhaseDirs via listArchiveVersionDirs (see its doc comment).
249
+ for (const { version, archivePath } of listArchiveVersionDirs(cwd)) {
250
+ const relBase = toPosixPath(node_path_1.default.relative(cwd, archivePath));
251
+ const result = searchPhaseInDir(archivePath, relBase, normalized);
252
+ if (result) {
253
+ result.archived = version;
254
+ return result;
127
255
  }
128
256
  }
129
- catch { /* intentionally empty */ }
130
257
  return null;
131
258
  }
259
+ /**
260
+ * #3185 (epic #3180 Phase 3, ADR-3180 Decision 1 row "Phase enumeration"):
261
+ * the SINGLE canonical owner of "which phase directories belong to the current
262
+ * milestone". Applies the milestone window AND the sentinel filter, in that
263
+ * order, and returns the surviving directory names.
264
+ *
265
+ * Before this existed the derivation had four independent implementations and
266
+ * only `cmdRoadmapAnalyze` carried both halves; `cmdProgressRender`,
267
+ * `cmdStats` and `cmdPhasesList` each carried neither or one.
268
+ *
269
+ * TWO THINGS THIS GETS RIGHT THAT A HEADING-SIDE FILTER CANNOT:
270
+ *
271
+ * 1. The sentinel test runs against DIRECTORY NAMES and is UNCONDITIONAL.
272
+ * `getMilestonePhaseFilter` excludes sentinels from its ROADMAP HEADING
273
+ * set, but when that set is empty it degrades to a literal `() => true`
274
+ * pass-all predicate and never consults the heading set at all — so its
275
+ * own sentinel exclusion becomes unreachable exactly when it is needed,
276
+ * and every directory on disk (backlog included) is reported as a
277
+ * current-milestone phase. That degrade is the #3167 symptom path.
278
+ *
279
+ * 2. The sentinel predicate is the canonical `isSentinelPhaseId`
280
+ * (`src/phase-id.cts`, SENTINEL_RANGES [0, 999]), not a local literal.
281
+ * The rule had five copies and three different regexes before this phase,
282
+ * and they disagreed about Phase 0.
283
+ *
284
+ * The pass-all degrade is narrowed MINIMALLY: it stays over-inclusive for
285
+ * non-sentinel directories, so a project whose window declares no phases
286
+ * still sees its real phase directories. Only sentinels are refused.
287
+ *
288
+ * `scope` distinguishes a REAL empty from a NON-answer (ADR-3180 Decision 2):
289
+ * an absent `phasesDir` is a real empty (a new project genuinely has no
290
+ * phases) and inherits the window's scope, whereas a `phasesDir` that exists
291
+ * but cannot be read is UNREADABLE.
292
+ *
293
+ * `opts.ws` is tri-state, matching `planningDir`'s own contract: `undefined`
294
+ * (the default — do not pass `ws` at all) resolves the AMBIENT workstream
295
+ * from `GSD_WORKSTREAM`; `null` FORCES the project root regardless of any
296
+ * ambient workstream; a string forces that specific workstream.
297
+ */
298
+ function listMilestonePhaseDirs(phasesDir, opts = {}) {
299
+ // #3597: `ws` must default to `undefined`, NOT `null`. `undefined` means
300
+ // "resolve the ambient workstream" (mirrors planningDir's own contract,
301
+ // src/planning-workspace.cts:124); `null` means "force the project root".
302
+ // Every cwd-bearing caller derives `phasesDir` ambiently (planningPaths(cwd)
303
+ // / planningDir(cwd) with no explicit ws), so defaulting `ws` to `null` here
304
+ // forced the milestone WINDOW to the root ROADMAP while the caller's
305
+ // `phasesDir` stayed workstream-scoped — numerator and denominator drawn
306
+ // from different scoped sets (ADR-3180 §7.6 rule 3 violation). That is what
307
+ // made `--ws <name> progress` read `phase_scope: "unreadable"` and withhold
308
+ // `percent` once `workstream create` migrated the root ROADMAP away.
309
+ const { cwd, ws, versionOverride = null, phaseIdConvention = null } = opts;
310
+ // Without a cwd there is nothing to scope AGAINST — the caller asked for an
311
+ // unscoped read, which is a real answer (mirrors extractCurrentMilestoneScoped's
312
+ // row 1). Sentinels are still refused: they are never milestone phases.
313
+ let inWindow = () => true;
314
+ let scope = SCOPE.COMPLETE;
315
+ if (cwd) {
316
+ const filter = getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws);
317
+ inWindow = filter;
318
+ scope = filter.scope;
319
+ }
320
+ // An ABSENT phases dir is a real empty, not a failure: a freshly-created
321
+ // project genuinely has no phase directories yet. Distinguishing this from
322
+ // the unreadable case below is the whole point of the scope discriminator.
323
+ if (!node_fs_1.default.existsSync(phasesDir))
324
+ return { value: [], scope };
325
+ let names;
326
+ try {
327
+ names = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
328
+ .filter((e) => e.isDirectory())
329
+ .map((e) => e.name);
330
+ }
331
+ catch {
332
+ // The directory EXISTS but could not be read (EACCES/EIO). An empty list
333
+ // here is a NON-answer and must not be reported as "this milestone has no
334
+ // phases" — that collapse is the defect class this epic removes.
335
+ return { value: [], scope: SCOPE.UNREADABLE };
336
+ }
337
+ const value = names
338
+ .filter((name) => inWindow(name) && !isSentinelPhaseId(name, phaseIdConvention ?? undefined))
339
+ .sort((a, b) => comparePhaseNum(a, b));
340
+ return { value, scope };
341
+ }
132
342
  function getArchivedPhaseDirs(cwd) {
133
- const milestonesDir = node_path_1.default.join(cwd, '.planning', 'milestones');
343
+ // #2855: same workstream-scoped resolution as findPhaseInternal above, via
344
+ // the shared listArchiveVersionDirs helper. `phase.list --include-archived`
345
+ // (the primary non-init consumer) must not leak a different workstream's
346
+ // archive either.
134
347
  const results = [];
135
- if (!node_fs_1.default.existsSync(milestonesDir))
136
- return results;
137
- try {
138
- const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
139
- const phaseDirs = milestoneEntries
140
- .filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
141
- .map(e => e.name)
142
- .sort()
143
- .reverse();
144
- for (const archiveName of phaseDirs) {
145
- const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
146
- const version = versionMatch[1];
147
- const archivePath = node_path_1.default.join(milestonesDir, archiveName);
148
- const dirs = readSubdirectories(archivePath, true);
149
- for (const dir of dirs) {
150
- results.push({
151
- name: dir,
152
- milestone: version,
153
- basePath: node_path_1.default.join('.planning', 'milestones', archiveName),
154
- fullPath: node_path_1.default.join(archivePath, dir),
155
- });
156
- }
348
+ for (const { version, archivePath } of listArchiveVersionDirs(cwd)) {
349
+ const dirs = readSubdirectories(archivePath, true);
350
+ for (const dir of dirs) {
351
+ results.push({
352
+ name: dir,
353
+ milestone: version,
354
+ basePath: toPosixPath(node_path_1.default.relative(cwd, archivePath)),
355
+ fullPath: node_path_1.default.join(archivePath, dir),
356
+ });
157
357
  }
158
358
  }
159
- catch { /* intentionally empty */ }
160
359
  return results;
161
360
  }
162
361
  module.exports = {
163
362
  searchPhaseInDir,
164
363
  findPhaseInternal,
165
364
  getArchivedPhaseDirs,
365
+ listMilestonePhaseDirs,
166
366
  };