@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
@@ -0,0 +1,890 @@
1
+ "use strict";
2
+ /**
3
+ * Planning Snapshot — a parsed projection of `.planning/` (Phase 10, #3308,
4
+ * ADR-3180 §8.1).
5
+ *
6
+ * Composed EXCLUSIVELY from the already-consolidated §7 owners
7
+ * (`getMilestoneInfo`, `listMilestonePhaseDirs`, `isPhaseComplete`,
8
+ * `scanPhasePlans`, `stateFieldValue`, `planningPaths`) plus the frozen
9
+ * `SCOPE` enum. This module introduces no new semantic derivation — it
10
+ * introduces exactly one new thing: `worstScope`, a way to combine several
11
+ * independently-scoped owner answers into one composite record without
12
+ * letting a caller treat a non-answer as data.
13
+ *
14
+ * `buildPlanningSnapshot(cwd)` is the sole export consumers reach for;
15
+ * `worstScope` is exported alongside it for direct unit coverage.
16
+ *
17
+ * Design: .gsd/phase/refactor-3308-planning-snapshot-parsed-projection/40-design.md
18
+ *
19
+ * ADR-457 build-at-publish: source in src/planning-snapshot.cts, compiled to
20
+ * gsd-core/bin/lib/planning-snapshot.cjs (gitignored).
21
+ */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ const node_fs_1 = __importDefault(require("node:fs"));
26
+ const node_path_1 = __importDefault(require("node:path"));
27
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
28
+ const roadmapParserMod = require("./roadmap-parser.cjs");
29
+ const { getMilestoneInfo, extractCurrentMilestone } = roadmapParserMod;
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
31
+ const phaseLocatorMod = require("./phase-locator.cjs");
32
+ const { listMilestonePhaseDirs } = phaseLocatorMod;
33
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
34
+ const verificationMod = require("./verification.cjs");
35
+ const { isPhaseComplete } = verificationMod;
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
37
+ const scanPhasePlans = require("./plan-scan.cjs");
38
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
39
+ const planningWorkspace = require("./planning-workspace.cjs");
40
+ const { planningPaths, planningRoot } = planningWorkspace;
41
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const frontmatterMod = require("./frontmatter.cjs");
44
+ const { extractFrontmatter, stripFrontmatter } = frontmatterMod;
45
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
46
+ const coreUtilsMod = require("./core-utils.cjs");
47
+ const { findOrphanSummaries } = coreUtilsMod;
48
+ const state_document_cjs_1 = require("./state-document.cjs");
49
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
50
+ const unusableInputMod = require("./unusable-input.cjs");
51
+ const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
52
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
53
+ const planningScopeMod = require("./planning-scope.cjs");
54
+ const { SCOPE } = planningScopeMod;
55
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
56
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module
57
+ const agentInstallCheckMod = require("./agent-install-check.cjs");
58
+ const { checkAgentsInstalled } = agentInstallCheckMod;
59
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- worktree-safety.cjs is an export= CommonJS module
60
+ const worktreeSafetyMod = require("./worktree-safety.cjs");
61
+ const { inspectWorktreeHealth } = worktreeSafetyMod;
62
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
63
+ const configLoaderMod = require("./config-loader.cjs");
64
+ const { isGitIgnored } = configLoaderMod;
65
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
66
+ const phaseIdMod = require("./phase-id.cjs");
67
+ const { PHASE_NUMBER_TOKEN_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, stripProjectCodePrefix, scopeToPhase } = phaseIdMod;
68
+ const validate_cjs_1 = require("./validate.cjs");
69
+ // ─── worstScope — the one new piece of coordination logic ───────────────────
70
+ /**
71
+ * Severity ordering (`UNREADABLE` worst, `COMPLETE` best) is a genuine design
72
+ * choice, not inherited from anywhere — see the design doc's "Scope
73
+ * combination" section. `TRUNCATED` vs `UNSCOPED` are not ranked against each
74
+ * other by any upstream decision; this ordering exists only so a future
75
+ * diagnostic rule can name which failure was worse when several compound.
76
+ */
77
+ const SCOPE_SEVERITY = {
78
+ [SCOPE.COMPLETE]: 0,
79
+ [SCOPE.TRUNCATED]: 1,
80
+ [SCOPE.UNSCOPED]: 2,
81
+ [SCOPE.UNREADABLE]: 3,
82
+ };
83
+ /**
84
+ * Combine several independently-scoped owner answers into the single worst
85
+ * (most severe) `Scope` among them. Pure, no I/O. Not a re-derivation of any
86
+ * §7 owner — it folds together already-final `scope` outputs, which is new
87
+ * coordination logic no single owner has visibility to express itself.
88
+ */
89
+ function worstScope(...scopes) {
90
+ return scopes.reduce((worst, s) => (SCOPE_SEVERITY[s] > SCOPE_SEVERITY[worst] ? s : worst));
91
+ }
92
+ /**
93
+ * Build one `PhaseSnapshot` for a single already-enumerated phase directory
94
+ * name. `isPhaseComplete` and `scanPhasePlans` each perform their own raw
95
+ * `readdirSync` against `fullPhaseDir` and can independently degrade — see
96
+ * the design doc's "Scope combination" section for why the two are genuinely
97
+ * uncorrelated (isPhaseComplete's readability check never re-derives or
98
+ * requires scanPhasePlans, and vice versa).
99
+ */
100
+ function buildPhaseSnapshot(phasesDir, dir) {
101
+ const fullPhaseDir = node_path_1.default.join(phasesDir, dir);
102
+ const completionResult = isPhaseComplete(fullPhaseDir);
103
+ const scanResult = scanPhasePlans(fullPhaseDir);
104
+ return {
105
+ dir,
106
+ complete: completionResult.value.complete,
107
+ verificationStatus: completionResult.value.verification.status,
108
+ planCount: scanResult.planCount,
109
+ summaryCount: scanResult.summaryCount,
110
+ scope: worstScope(completionResult.scope, scanResult.scope),
111
+ };
112
+ }
113
+ /**
114
+ * Resolve every STATE.md-sourced field in one place: `currentPhaseLabel` (the
115
+ * raw `Phase:` field under `## Current Position`, e.g. `"3 of 8 (User
116
+ * Auth)"`, not a normalized phase-directory id — see the design doc's Known
117
+ * limits), `statePhaseTokens` (Phase 11, #3309 — every phase-number-shaped
118
+ * token found anywhere in STATE.md's raw text, backs W002), and `stateStatus`
119
+ * (Phase 11, #3309 — the `status`/`Status` field, backs W011).
120
+ *
121
+ * Phase 10 shipped `currentPhaseLabel` as its own single-purpose reader
122
+ * (`buildCurrentPhaseLabel(statePath)`); this phase folds two more STATE.md
123
+ * derivations in rather than reading and parsing the same file three times
124
+ * per `buildPlanningSnapshot` call — the read, `extractFrontmatter`, and
125
+ * `stripFrontmatter` are genuinely shared inputs for all three, and sharing
126
+ * them means `warnUnusableInput(STATE_UNREADABLE)` also stays a single call
127
+ * site instead of a risk of tripling on one degraded read.
128
+ *
129
+ * This module performs the one STATE.md read no §7 owner does, mirroring
130
+ * every existing STATE.md caller (`cmdStateSnapshot`, `cmdStatePrune`):
131
+ * `platformReadSync` + `extractFrontmatter` + `stripFrontmatter`.
132
+ *
133
+ * - STATE.md absent (ENOENT, `platformReadSync` returns `null`) is a real
134
+ * non-answer, NOT corruption — a project that never ran `state.init`
135
+ * legitimately has no STATE.md yet. `warnUnusableInput` is NOT called.
136
+ * - STATE.md present but unreadable (any other read error, e.g. EISDIR) is
137
+ * corruption — `warnUnusableInput(STATE_UNREADABLE)` fires exactly once,
138
+ * and all three fields degrade to their UNREADABLE non-answer together.
139
+ * - An unterminated frontmatter fence is reported by `extractFrontmatter`
140
+ * itself (`FRONTMATTER_UNTERMINATED`) — this function does not duplicate
141
+ * that diagnostic; it still attempts a body-only field read on whatever
142
+ * `stripFrontmatter` leaves behind.
143
+ * - `currentPhaseLabel`/`stateStatus` both live under `## Current Position`
144
+ * (`gsd-core/templates/state.md`) and both use `stateFieldValue`
145
+ * (`state-document.cts:296`) the exact way `smart-entry.cts:448`/
146
+ * `state.cts:1561,3273` already call it for `'status'`/`'Status'` — so a
147
+ * missing `## Current Position` section degrades BOTH to `TRUNCATED` with
148
+ * a whole-body fallback, together.
149
+ * - `statePhaseTokens` scans the WHOLE document (`verify.cts`'s exact
150
+ * `PHASE_NUMBER_TOKEN_SOURCE` regex, relocated verbatim from
151
+ * `verify.cts:1731-1735`), not just the Current Position section, so it is
152
+ * NOT degraded to `TRUNCATED` by a missing section header — it stays
153
+ * `COMPLETE` whenever the file itself was read successfully.
154
+ */
155
+ function buildStateFields(statePath) {
156
+ let content;
157
+ try {
158
+ content = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
159
+ }
160
+ catch {
161
+ warnUnusableInput({ reason: UNUSABLE_REASON.STATE_UNREADABLE, source: statePath });
162
+ return {
163
+ currentPhaseLabel: { value: null, scope: SCOPE.UNREADABLE },
164
+ statePhaseTokens: { value: [], scope: SCOPE.UNREADABLE },
165
+ stateStatus: { value: null, scope: SCOPE.UNREADABLE },
166
+ };
167
+ }
168
+ if (content === null) {
169
+ return {
170
+ currentPhaseLabel: { value: null, scope: SCOPE.UNREADABLE },
171
+ statePhaseTokens: { value: [], scope: SCOPE.UNREADABLE },
172
+ stateStatus: { value: null, scope: SCOPE.UNREADABLE },
173
+ };
174
+ }
175
+ const frontmatter = extractFrontmatter(content, statePath);
176
+ const body = stripFrontmatter(content);
177
+ const section = (0, state_document_cjs_1.stateCurrentPositionSlice)(body);
178
+ const currentPositionScope = section === null ? SCOPE.TRUNCATED : SCOPE.COMPLETE;
179
+ // #1760 fallback ladder — now a full mirror of `state.cts`'s
180
+ // `resolveStatePhase` (its three-source ladder at `state.cts:1494-1516`),
181
+ // including the frontmatter step that ladder leads with:
182
+ // 1. frontmatter `current_phase` scalar — the machine-readable key
183
+ // `gsd-tools state update` / `state begin-phase` persist via
184
+ // `syncStateFrontmatter` (`state.cts:2023`), so it takes PRIORITY over
185
+ // any body field (#3280: a body-only ladder left W011 structurally
186
+ // blind to the one format the product itself writes — a stale body
187
+ // `Phase:` remnant even SHADOWED the current frontmatter value).
188
+ // 2. the legacy bold `**Current Phase:**` field (what `verify.cts:2109-
189
+ // 2111` originally matched, and what pre-template-migration STATE.md
190
+ // fixtures still use).
191
+ // 3. the current template's bare `Phase: [X] of [Y]` field.
192
+ // A document carrying several is read the same way `resolveStatePhase`
193
+ // reads it elsewhere — frontmatter first, then body, in that order.
194
+ const frontmatterCurrentPhase = (0, state_document_cjs_1.stateFieldValue)(frontmatter, body, 'current_phase', null);
195
+ const legacyCurrentPhaseLabel = (0, state_document_cjs_1.stateFieldValue)(frontmatter, section ?? body, null, 'Current Phase', {
196
+ scope: currentPositionScope,
197
+ });
198
+ const templateCurrentPhaseLabel = (0, state_document_cjs_1.stateFieldValue)(frontmatter, section ?? body, null, 'Phase', {
199
+ scope: currentPositionScope,
200
+ });
201
+ const currentPhaseLabel = {
202
+ value: frontmatterCurrentPhase.value ?? legacyCurrentPhaseLabel.value ?? templateCurrentPhaseLabel.value,
203
+ scope: frontmatterCurrentPhase.value !== null
204
+ ? frontmatterCurrentPhase.scope
205
+ : legacyCurrentPhaseLabel.value !== null
206
+ ? legacyCurrentPhaseLabel.scope
207
+ : templateCurrentPhaseLabel.scope,
208
+ };
209
+ const stateStatus = (0, state_document_cjs_1.stateFieldValue)(frontmatter, section ?? body, 'status', 'Status', {
210
+ scope: currentPositionScope,
211
+ });
212
+ const statePhaseTokens = {
213
+ value: [...content.matchAll(new RegExp(`[Pp]hase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'g'))].map((m) => m[1]),
214
+ scope: SCOPE.COMPLETE,
215
+ };
216
+ return { currentPhaseLabel, statePhaseTokens, stateStatus };
217
+ }
218
+ /**
219
+ * Resolve `config` — the parsed `.planning/config.json`, preserving the same
220
+ * three-way distinction `cmdValidateHealth` (`src/verify.cts` W003/E005)
221
+ * already makes without going through `loadConfig` (which collapses that
222
+ * distinction): absent is a real non-answer — `{value: null, scope:
223
+ * UNREADABLE, exists: false}`, no `warnUnusableInput` call, mirrors
224
+ * `buildCurrentPhaseLabel`'s treatment of an absent STATE.md; present but
225
+ * unparseable JSON IS corruption — `{value: null, scope: UNREADABLE, exists:
226
+ * true}`, `warnUnusableInput(CONFIG_UNREADABLE)` fires exactly once, so a
227
+ * later health-diagnostic rule can tell "config.json not found" (W003,
228
+ * repairable via `createConfig`) apart from "config.json: JSON parse error"
229
+ * (E005, repairable via `resetConfig`) — the `exists` flag is exactly that
230
+ * discriminator. `config.json` is root-scoped (`planningRoot`), NOT
231
+ * workstream-scoped (`planningPaths(cwd).config` would resolve under
232
+ * `.planning/workstreams/<ws>/` instead) — see verify.cts's own
233
+ * rootBase-vs-wsBase split at cmdValidateHealth's top.
234
+ */
235
+ function buildConfigField(cwd) {
236
+ const configPath = node_path_1.default.join(planningRoot(cwd), 'config.json');
237
+ if (!node_fs_1.default.existsSync(configPath)) {
238
+ return { value: null, scope: SCOPE.UNREADABLE, exists: false };
239
+ }
240
+ try {
241
+ const raw = node_fs_1.default.readFileSync(configPath, 'utf-8');
242
+ const parsed = JSON.parse(raw);
243
+ return { value: parsed, scope: SCOPE.COMPLETE, exists: true };
244
+ }
245
+ catch {
246
+ warnUnusableInput({ reason: UNUSABLE_REASON.CONFIG_UNREADABLE, source: configPath });
247
+ return { value: null, scope: SCOPE.UNREADABLE, exists: true };
248
+ }
249
+ }
250
+ /**
251
+ * Resolve `agentInstall` — wraps `checkAgentsInstalled(runtime, cwd)` with
252
+ * the same `runtime` `cmdValidateHealth` resolves (`resolveRuntime(cwd)`,
253
+ * its `_slashRuntime`). Not `.planning/`-sourced (see design doc). `scope`
254
+ * is `COMPLETE` whenever the scan itself ran, even when it reports missing
255
+ * or incomplete agents — that is a real answer, not a non-answer.
256
+ * `UNREADABLE` only if the scan itself throws, mirroring cmdValidateHealth's
257
+ * own try/catch around this same call (there, the exception is swallowed as
258
+ * "non-blocking"; here it is surfaced via `scope` instead of silently
259
+ * dropped, since a snapshot field has nowhere else to carry that fact).
260
+ */
261
+ function buildAgentInstallField(cwd) {
262
+ const runtime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
263
+ try {
264
+ return { value: checkAgentsInstalled(runtime, cwd), scope: SCOPE.COMPLETE };
265
+ }
266
+ catch {
267
+ return {
268
+ value: {
269
+ agents_installed: false,
270
+ missing_agents: [],
271
+ installed_agents: [],
272
+ incomplete_agents: [],
273
+ agents_dir: '',
274
+ agent_runtime: runtime,
275
+ },
276
+ scope: SCOPE.UNREADABLE,
277
+ };
278
+ }
279
+ }
280
+ /**
281
+ * Resolve `worktreeHealth` — wraps `inspectWorktreeHealth(cwd, { staleAfterMs
282
+ * }, deps)` with the exact same arguments `cmdValidateHealth` passes
283
+ * (`src/verify.cts` W017/W020/W027 call sites): a 1-hour staleness window,
284
+ * and the raw `execGit`/`fs.existsSync`/`fs.statSync` seam (not
285
+ * `worktree-safety.cts`'s own `execGitDefault` wrapper). Not
286
+ * `.planning/`-sourced (see design doc). `scope` is `COMPLETE` only when the
287
+ * underlying `git worktree list` scan itself succeeded (`ok: true`) — a
288
+ * timed-out or failed scan (`ok: false`, mirroring W020's degraded-check
289
+ * report) or a thrown exception (mirrors cmdValidateHealth's own
290
+ * "git worktree not available or not a git repo — skip silently" catch)
291
+ * both degrade to `UNREADABLE` with an empty findings array, since neither
292
+ * case has real per-worktree data to report. `reason` carries
293
+ * `inspectWorktreeHealth`'s own discriminator ('ok' | 'git_timed_out' |
294
+ * 'git_list_failed' | 'not_a_git_repo') straight through — NOT discarded —
295
+ * so `checkW020` (`src/health-diagnostic-rules/worktree-health.cts`) can
296
+ * reproduce `verify.cts:2202-2217`'s exact branching: it warns on
297
+ * 'git_timed_out' or 'git_list_failed' but stays silent on 'not_a_git_repo'
298
+ * (a `.planning/`-only fixture/tmp dir with no git repo at all is not a
299
+ * degraded scan). A thrown exception reports 'exception', which also stays
300
+ * silent, matching the original's catch-all "skip silently" comment.
301
+ */
302
+ function buildWorktreeHealthField(cwd) {
303
+ try {
304
+ const result = inspectWorktreeHealth(cwd, { staleAfterMs: 60 * 60 * 1000 }, { execGit: shell_command_projection_cjs_1.execGit, existsSync: node_fs_1.default.existsSync, statSync: node_fs_1.default.statSync });
305
+ if (!result.ok) {
306
+ return { value: [], scope: SCOPE.UNREADABLE, reason: result.reason };
307
+ }
308
+ return { value: result.findings, scope: SCOPE.COMPLETE, reason: result.reason };
309
+ }
310
+ catch {
311
+ return { value: [], scope: SCOPE.UNREADABLE, reason: 'exception' };
312
+ }
313
+ }
314
+ // #3586 (Phase 2, epic #2292): matches `execGit`'s own default timeout
315
+ // (`shell-command-projection.cts:628`, also `10_000`) — kept as an explicit
316
+ // named constant here (rather than omitting `timeout` and relying on that
317
+ // default silently) so this call site's bound is self-documenting; generous
318
+ // enough for a normal repo, bounded enough to degrade rather than stall
319
+ // `buildPlanningSnapshot`, which every health path calls.
320
+ const PLANNING_TRACKED_GIT_TIMEOUT_MS = 10_000;
321
+ /**
322
+ * Resolve `planningTracked` — whether `.planning/` matches a gitignore rule
323
+ * AND whether at least one path under it is still tracked by git (#3586,
324
+ * Phase 2 of epic #2292). `.gitignore` has no effect on files git already
325
+ * tracks, so a project that committed `.planning/` before ignoring it keeps
326
+ * staging those files forever — while `commit_docs` auto-resolves `false`
327
+ * (`isGitIgnored`, reused below, is exactly what that resolution consults),
328
+ * which is what makes the contradiction invisible. Backs W029
329
+ * (`src/health-diagnostic-rules/config-validation.cts`).
330
+ *
331
+ * Modeled directly on `buildWorktreeHealthField` above (ADR-3180 §8.1 rule 1:
332
+ * a `Rule.check(snapshot)` may perform no ambient I/O, so this `git ls-files`
333
+ * probe lives here, in the snapshot builder, not the rule).
334
+ *
335
+ * `tracked` runs `git ls-files -- .planning` through the module's own
336
+ * injected `execGit` seam: non-empty stdout means at least one path under
337
+ * `.planning/` is in the INDEX — worktree presence is irrelevant (a path
338
+ * tracked in the index but deleted on disk still counts; that is what "the
339
+ * index is what matters" means for this probe).
340
+ *
341
+ * `ignored` reuses `isGitIgnored` (`config-loader.cjs`) — the SAME
342
+ * `git check-ignore -q --no-index` resolution `commit_docs` auto-resolution
343
+ * already calls (`config-loader.cts:824`) — rather than a second,
344
+ * independently-drifting `check-ignore` invocation. Only computed once the
345
+ * `ls-files` probe itself succeeded; a probe that could not run has no
346
+ * grounds to ask a second question either.
347
+ *
348
+ * Degradation mirrors `buildWorktreeHealthField` exactly: not a git repo →
349
+ * `SCOPE.UNREADABLE` + reason `not_a_git_repo` (silent downstream — matches
350
+ * the sibling builder's deliberate treatment of a `.planning/`-only
351
+ * fixture/tmp dir with no git repo at all); a timed-out `ls-files` →
352
+ * `git_timed_out`; any other non-zero exit → `git_list_failed`; a thrown
353
+ * exception → `exception`. Never throws out of the builder.
354
+ *
355
+ * Repo-presence is determined STRUCTURALLY, not by reading `ls-files`'s
356
+ * stderr prose (#3586): git localizes its error text (`LANG`/`LC_ALL`), so a
357
+ * regex matching the English "not a git repository" string silently
358
+ * misclassifies `not_a_git_repo` as `git_list_failed` under any non-English
359
+ * locale — this is exactly the "raw text matching on subprocess output"
360
+ * `CONTRIBUTING.md` bans, applied to production code rather than a test.
361
+ * Instead, on the `ls-files` failure path ONLY (never on the happy path —
362
+ * `buildPlanningSnapshot` runs on every health invocation, and the happy
363
+ * path must stay a single subprocess call), this asks git a structural
364
+ * yes/no question via `git rev-parse --is-inside-work-tree`: exit 0 means we
365
+ * ARE inside a work tree, so the `ls-files` failure was something else →
366
+ * `git_list_failed`; a non-zero exit means we are NOT → `not_a_git_repo`.
367
+ * If that probe itself times out, `result.timedOut` (the shared
368
+ * `isSpawnTimeout` predicate) reports `git_timed_out` — not a hand-rolled
369
+ * timeout check.
370
+ *
371
+ * `ENOBUFS` overflow (#3586 review F2): `execGit` sets no `maxBuffer`, so
372
+ * `spawnSync`'s Node-default 1MB cap applies to `ls-files`' stdout. A
373
+ * `.planning/` tree with enough tracked paths to exceed 1MB makes
374
+ * `spawnSync` report `error.code === 'ENOBUFS'` — exactly the large-tracked-
375
+ * history case this probe exists to catch, and exactly the case most likely
376
+ * to legitimately overflow the buffer. Falling into the generic
377
+ * non-zero-exit path here would misclassify it as `git_list_failed` →
378
+ * `SCOPE.UNREADABLE`, silently dropping the finding in precisely the
379
+ * scenario where it matters most. Overflow is therefore treated as PROOF OF
380
+ * TRACKING, not as a degraded read: `ls-files` only overflows because it had
381
+ * non-empty output to begin with, so `tracked` is unconditionally `true` —
382
+ * detected BEFORE the generic `exitCode !== 0` branch below, so this case
383
+ * never reaches (and never pays for) the `rev-parse` structural probe.
384
+ */
385
+ function buildPlanningTrackedField(cwd) {
386
+ try {
387
+ const result = (0, shell_command_projection_cjs_1.execGit)(['ls-files', '--', '.planning'], { cwd, timeout: PLANNING_TRACKED_GIT_TIMEOUT_MS });
388
+ if (result.timedOut) {
389
+ return { value: { tracked: false, ignored: false }, scope: SCOPE.UNREADABLE, reason: 'git_timed_out' };
390
+ }
391
+ if (result.error?.code === 'ENOBUFS') {
392
+ const ignored = isGitIgnored(cwd, '.planning/');
393
+ return { value: { tracked: true, ignored }, scope: SCOPE.COMPLETE, reason: 'ok_truncated' };
394
+ }
395
+ if (result.exitCode !== 0) {
396
+ const probe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--is-inside-work-tree'], { cwd, timeout: PLANNING_TRACKED_GIT_TIMEOUT_MS });
397
+ if (probe.timedOut) {
398
+ return { value: { tracked: false, ignored: false }, scope: SCOPE.UNREADABLE, reason: 'git_timed_out' };
399
+ }
400
+ const reason = probe.exitCode === 0 ? 'git_list_failed' : 'not_a_git_repo';
401
+ return { value: { tracked: false, ignored: false }, scope: SCOPE.UNREADABLE, reason };
402
+ }
403
+ const tracked = result.stdout.trim().length > 0;
404
+ const ignored = isGitIgnored(cwd, '.planning/');
405
+ return { value: { tracked, ignored }, scope: SCOPE.COMPLETE, reason: 'ok' };
406
+ }
407
+ catch {
408
+ return { value: { tracked: false, ignored: false }, scope: SCOPE.UNREADABLE, reason: 'exception' };
409
+ }
410
+ }
411
+ // ─── Phase 11 (#3309) "Rule table organization" builders ────────────────────
412
+ // Each relocates (not reinvents) an existing `verify.cts` derivation. See the
413
+ // design doc's "Rule table organization" table for the exact source lines.
414
+ /**
415
+ * Resolve `projectSections` — the `##`-level section headings actually
416
+ * present in `.planning/PROJECT.md`, as a plain list (NOT filtered against a
417
+ * required-sections list — the caller, the future W001/E002 rules, do that
418
+ * comparison). Relocates the read+parse half of `verify.cts:1681-1691`
419
+ * (E002/W001), generalized from "does the file include these three fixed
420
+ * strings" to "what headings does the file actually have."
421
+ *
422
+ * PROJECT.md is root-scoped (`planningRoot(cwd)`), NOT workstream-scoped —
423
+ * mirrors `cmdValidateHealth`'s own `projectPath = path.join(rootBase,
424
+ * 'PROJECT.md')` (`verify.cts:1649`), the same root-vs-workstream split
425
+ * `buildConfigField` already documents for config.json.
426
+ *
427
+ * Same `exists`-discriminator shape as `config`: absent file is a real
428
+ * non-answer (`{value: null, scope: UNREADABLE, exists: false}`, no
429
+ * `warnUnusableInput`); present but unreadable IS corruption —
430
+ * `{value: null, scope: UNREADABLE, exists: true}`,
431
+ * `warnUnusableInput(PROJECT_UNREADABLE)` fires exactly once, mirroring
432
+ * `buildConfigField`'s treatment of a present-but-unparseable config.json.
433
+ */
434
+ function buildProjectSectionsField(cwd) {
435
+ const projectPath = node_path_1.default.join(planningRoot(cwd), 'PROJECT.md');
436
+ if (!node_fs_1.default.existsSync(projectPath)) {
437
+ return { value: null, scope: SCOPE.UNREADABLE, exists: false };
438
+ }
439
+ let content;
440
+ try {
441
+ content = node_fs_1.default.readFileSync(projectPath, 'utf-8');
442
+ }
443
+ catch {
444
+ warnUnusableInput({ reason: UNUSABLE_REASON.PROJECT_UNREADABLE, source: projectPath });
445
+ return { value: null, scope: SCOPE.UNREADABLE, exists: true };
446
+ }
447
+ const value = [...content.matchAll(/^##\s+(.+)$/gm)].map((m) => m[1].trim());
448
+ return { value, scope: SCOPE.COMPLETE, exists: true };
449
+ }
450
+ /**
451
+ * Resolve `roadmapDeclaredPhases` — every phase id ROADMAP.md declares
452
+ * (heading-style AND checklist-style, not filtered to disk presence), each
453
+ * paired with the milestone-version section it was found under (`null` when
454
+ * found outside any versioned section). Backs W006/W007 (declared-phase
455
+ * half) and W021(2288)/W026(2392) (milestone-attribution half).
456
+ *
457
+ * The declared-phase-id half reuses `buildRoadmapPhaseVariants`
458
+ * (`validate.cts:136`, already imported by `verify.cts:12` — genuine existing
459
+ * reuse). The milestone-attribution half relocates
460
+ * `checkMilestonePrefixMismatches`'s `sectionRx`-based section walk
461
+ * (`verify.cts:1429-1459`, local/unexported there), generalized from "record
462
+ * only the mismatches" to "record every attribution" — this field exposes
463
+ * the parsed fact; the future W021/W026 rules make the mismatch judgment.
464
+ */
465
+ function buildRoadmapDeclaredPhasesField(roadmapPath) {
466
+ if (!node_fs_1.default.existsSync(roadmapPath)) {
467
+ return { value: [], scope: SCOPE.UNREADABLE };
468
+ }
469
+ let content;
470
+ try {
471
+ content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
472
+ }
473
+ catch {
474
+ return { value: [], scope: SCOPE.UNREADABLE };
475
+ }
476
+ const { roadmapPhases } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(content);
477
+ const milestoneByPhase = new Map();
478
+ const sectionRx = /^#{1,3}\s+(?:\[[^\]]{1,200}\]\s*)?.*v(\d+\.\d+)/gim;
479
+ const sections = [];
480
+ let sm;
481
+ while ((sm = sectionRx.exec(content)) !== null) {
482
+ if (sections.length > 0)
483
+ sections[sections.length - 1].end = sm.index;
484
+ sections.push({ version: `v${sm[1]}`, start: sm.index, end: content.length });
485
+ }
486
+ const phaseRx = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/gi;
487
+ for (const section of sections) {
488
+ const sectionContent = content.slice(section.start, section.end);
489
+ phaseRx.lastIndex = 0;
490
+ let pm;
491
+ while ((pm = phaseRx.exec(sectionContent)) !== null) {
492
+ if (!milestoneByPhase.has(pm[1]))
493
+ milestoneByPhase.set(pm[1], section.version);
494
+ }
495
+ }
496
+ const value = [...roadmapPhases].map((phaseId) => ({
497
+ phaseId,
498
+ milestone: milestoneByPhase.get(phaseId) ?? null,
499
+ }));
500
+ return { value, scope: SCOPE.COMPLETE };
501
+ }
502
+ /**
503
+ * Resolve `roadmapPhaseCheckboxes` — parsed `[x]`/`[ ]` checkbox state per
504
+ * phase from ROADMAP.md's progress-table region, keyed by phase id. Backs
505
+ * W011.
506
+ *
507
+ * Relocates and generalizes `verify.cts`'s W011 block (`verify.cts:2104-
508
+ * 2134`): that call site builds ONE hardcoded `phaseCheckboxRe` testing a
509
+ * single target phase id (STATE's current phase) for a `[x]` match. This
510
+ * builder is the same regex shape, generalized to CAPTURE both the check
511
+ * character and the phase id instead of interpolating one fixed target, so
512
+ * every declared checkbox is recorded, not just one.
513
+ *
514
+ * NOT a re-derivation of `isPhaseComplete` (`verification.cts:557`, ADR-3180
515
+ * §7.4, disk-strict): that owner explicitly refuses to consult the ROADMAP
516
+ * checkbox at all when DECIDING phase completion (`verification.cts:536-
517
+ * 537`). This field only exposes what the checkbox literally says, for a
518
+ * diagnostic (W011) whose entire purpose is flagging when the two DISAGREE —
519
+ * reading the data is not re-litigating who is authoritative.
520
+ */
521
+ function buildRoadmapPhaseCheckboxesField(roadmapPath) {
522
+ if (!node_fs_1.default.existsSync(roadmapPath)) {
523
+ return { value: {}, scope: SCOPE.UNREADABLE };
524
+ }
525
+ let content;
526
+ try {
527
+ content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
528
+ }
529
+ catch {
530
+ return { value: {}, scope: SCOPE.UNREADABLE };
531
+ }
532
+ const checkboxRe = new RegExp(`-\\s*\\[([xX ])\\].*?Phase\\s+0*(${PHASE_NUMBER_TOKEN_SOURCE})${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'gi');
533
+ const value = {};
534
+ let m;
535
+ while ((m = checkboxRe.exec(content)) !== null) {
536
+ value[m[2]] = m[1].toLowerCase() === 'x';
537
+ }
538
+ return { value, scope: SCOPE.COMPLETE };
539
+ }
540
+ /**
541
+ * Resolve `researchValidationStatus` — per phase directory, whether its
542
+ * `*-RESEARCH.md` contains the literal heading `## Validation Architecture`,
543
+ * and whether a `*-VALIDATION.md` file exists in the same directory. Backs
544
+ * W009.
545
+ *
546
+ * Relocates the file-naming convention `verify.cts:1967-1990` (W009) uses to
547
+ * find "the" RESEARCH.md / VALIDATION.md in a phase dir: a flat,
548
+ * non-recursive `readdirSync` of the phase dir, then the first entry whose
549
+ * name ends `-RESEARCH.md` / any entry ending `-VALIDATION.md`. Computed for
550
+ * EVERY phase dir unconditionally (verify.cts's W009 only reads RESEARCH.md
551
+ * when `hasResearch && !hasValidation`; this field exposes both booleans
552
+ * regardless, so the future W009 rule does its own `hasResearch &&
553
+ * hasValidationArchitecture && !hasValidationMd` check against parsed data,
554
+ * not raw text).
555
+ *
556
+ * `scope` mirrors `phaseDirs.scope` (the caller-supplied enumeration): a
557
+ * per-directory read failure degrades that single entry's booleans to
558
+ * `false` and is silently skipped, mirroring `verify.cts`'s own
559
+ * `catch { intentionally empty }` around this exact read — this is a
560
+ * deliberate fail-open match to the pre-migration behavior, not a scope
561
+ * degradation, since the original never surfaced these failures either.
562
+ */
563
+ function buildResearchValidationStatusField(phasesDir, phaseDirNames, enumerationScope) {
564
+ const value = phaseDirNames.map((dir) => {
565
+ const fullPhaseDir = node_path_1.default.join(phasesDir, dir);
566
+ let files;
567
+ try {
568
+ files = node_fs_1.default.readdirSync(fullPhaseDir);
569
+ }
570
+ catch {
571
+ return { dir, hasValidationArchitecture: false, hasValidationMd: false };
572
+ }
573
+ // #3511: scope the raw listing to this phase dir before the two
574
+ // phase-numbered-artifact predicates, so a stray cross-phase
575
+ // -RESEARCH.md/-VALIDATION.md sitting in the wrong directory cannot flip
576
+ // this phase's flags — mirrors core-utils.cts's getPhaseFileStats.
577
+ const scopedFiles = scopeToPhase(files, dir);
578
+ const researchFile = scopedFiles.find((f) => f.endsWith('-RESEARCH.md'));
579
+ const hasValidationMd = scopedFiles.some((f) => f.endsWith('-VALIDATION.md'));
580
+ let hasValidationArchitecture = false;
581
+ if (researchFile) {
582
+ try {
583
+ const researchContent = node_fs_1.default.readFileSync(node_path_1.default.join(fullPhaseDir, researchFile), 'utf-8');
584
+ hasValidationArchitecture = researchContent.includes('## Validation Architecture');
585
+ }
586
+ catch {
587
+ /* intentionally empty — mirrors verify.cts:1986-1988's own silent skip */
588
+ }
589
+ }
590
+ return { dir, hasValidationArchitecture, hasValidationMd };
591
+ });
592
+ return { value, scope: enumerationScope };
593
+ }
594
+ /**
595
+ * Resolve `milestoneArchiveStatus` — `archivedVersions` (versions with a
596
+ * `milestones/<ver>-ROADMAP.md` snapshot file present) and `documentedVersions`
597
+ * (`## <version>` headings already present in MILESTONES.md). Backs W018.
598
+ *
599
+ * Relocates `verify.cts:2301-2335` (W018)'s directory-scan glob
600
+ * (`^(v\d+\.\d+(?:\.\d+)?)-ROADMAP\.md$` against a flat, non-recursive
601
+ * `readdirSync` of `.planning/milestones/`) and its MILESTONES.md
602
+ * heading-membership check, generalized from "is THIS archived version's
603
+ * heading present" to "list every `## <version>` heading MILESTONES.md has."
604
+ *
605
+ * Confirmed NOT a fit for `listArchiveVersionDirs`
606
+ * (`phase-locator.cts:127`): that function scans `milestones/*-phases/`
607
+ * DIRECTORIES, a different target than this field's `milestones/*-ROADMAP.md`
608
+ * FILES — reusing it here would silently answer the wrong question.
609
+ *
610
+ * Root-scoped (`planningRoot(cwd)`), matching `verify.cts`'s own
611
+ * `rootBase`-based `milestonesPath`/`milestonesArchiveDir`.
612
+ */
613
+ function buildMilestoneArchiveStatusField(cwd) {
614
+ const rootBase = planningRoot(cwd);
615
+ const milestonesArchiveDir = node_path_1.default.join(rootBase, 'milestones');
616
+ const milestonesPath = node_path_1.default.join(rootBase, 'MILESTONES.md');
617
+ let archivedVersions = [];
618
+ let scope = SCOPE.COMPLETE;
619
+ if (node_fs_1.default.existsSync(milestonesArchiveDir)) {
620
+ try {
621
+ const archiveFiles = node_fs_1.default.readdirSync(milestonesArchiveDir);
622
+ archivedVersions = archiveFiles
623
+ .map((f) => f.match(/^(v\d+\.\d+(?:\.\d+)?)-ROADMAP\.md$/))
624
+ .filter((m) => m !== null)
625
+ .map((m) => m[1]);
626
+ }
627
+ catch {
628
+ scope = SCOPE.UNREADABLE;
629
+ }
630
+ }
631
+ let documentedVersions = [];
632
+ if (node_fs_1.default.existsSync(milestonesPath)) {
633
+ try {
634
+ const registryContent = node_fs_1.default.readFileSync(milestonesPath, 'utf-8');
635
+ documentedVersions = [...registryContent.matchAll(/^##\s+(v\d+\.\d+(?:\.\d+)?)/gm)].map((m) => m[1]);
636
+ }
637
+ catch {
638
+ scope = worstScope(scope, SCOPE.UNREADABLE);
639
+ }
640
+ }
641
+ return { value: { archivedVersions, documentedVersions }, scope };
642
+ }
643
+ /**
644
+ * Resolve `planningRootFiles` — plain listing of file (not directory) names
645
+ * directly under `.planning/` root. Backs W019.
646
+ *
647
+ * Pairs with the existing exported `isCanonicalPlanningFile` predicate
648
+ * (`artifacts.cts:43`) — but per the design doc, that predicate is called by
649
+ * the future W019 RULE per filename, not by this builder; this field only
650
+ * needs to BE the raw filename list.
651
+ */
652
+ function buildPlanningRootFilesField(cwd) {
653
+ try {
654
+ const entries = node_fs_1.default.readdirSync(planningRoot(cwd), { withFileTypes: true });
655
+ return { value: entries.filter((e) => e.isFile()).map((e) => e.name), scope: SCOPE.COMPLETE };
656
+ }
657
+ catch {
658
+ return { value: [], scope: SCOPE.UNREADABLE };
659
+ }
660
+ }
661
+ /**
662
+ * Resolve `allPhaseDirNames` — every directory name directly under the
663
+ * active `phases/` root, UNFILTERED by `listMilestonePhaseDirs`'s
664
+ * current-milestone-window membership test (unlike `phaseDirs`). Backs
665
+ * W007 (see the field's own doc comment on `PlanningSnapshot` for why
666
+ * `phaseDirs` cannot). An absent `phases/` root is a real empty, not a
667
+ * failure (mirrors `listMilestonePhaseDirs`'s own treatment); a present but
668
+ * unreadable root degrades to `UNREADABLE` with an empty list.
669
+ */
670
+ function buildAllPhaseDirNamesField(phasesDir) {
671
+ if (!node_fs_1.default.existsSync(phasesDir))
672
+ return { value: [], scope: SCOPE.COMPLETE };
673
+ try {
674
+ const value = node_fs_1.default
675
+ .readdirSync(phasesDir, { withFileTypes: true })
676
+ .filter((e) => e.isDirectory())
677
+ .map((e) => e.name)
678
+ .sort();
679
+ return { value, scope: SCOPE.COMPLETE };
680
+ }
681
+ catch {
682
+ return { value: [], scope: SCOPE.UNREADABLE };
683
+ }
684
+ }
685
+ /**
686
+ * Resolve `archivedPhaseTokens` — every phase-number token belonging to a
687
+ * directory directly under any `.planning/milestones/*-phases/` archive.
688
+ * Backs W002's archived-phase exemption (#3652); see the field's own doc
689
+ * comment on `PlanningSnapshot`. Mirrors `verify.cts`'s
690
+ * `forEachArchivedPhaseToken` + `listMilestoneArchiveDirs` exactly — same
691
+ * `MILESTONE_ARCHIVE_DIR_RE` archive-dir filter, same `PHASE_TOKEN_FROM_DIR_RE`
692
+ * per-entry match, same `stripProjectCodePrefix` normalization — just
693
+ * collecting into a value array instead of an `onPhase` callback. An absent
694
+ * `milestones/` dir is a real empty (no archives yet), not a failure; a
695
+ * present-but-unreadable per-archive-dir entry is silently skipped, mirroring
696
+ * `forEachArchivedPhaseToken`'s own per-directory `catch { /* absent/unreadable *\/ }`.
697
+ */
698
+ function buildArchivedPhaseTokensField(planBase) {
699
+ const milestonesDir = node_path_1.default.join(planBase, 'milestones');
700
+ let archiveDirs;
701
+ try {
702
+ archiveDirs = node_fs_1.default
703
+ .readdirSync(milestonesDir, { withFileTypes: true })
704
+ .filter((e) => e.isDirectory() && validate_cjs_1.MILESTONE_ARCHIVE_DIR_RE.test(e.name))
705
+ .map((e) => node_path_1.default.join(milestonesDir, e.name));
706
+ }
707
+ catch (err) {
708
+ if (err.code === 'ENOENT')
709
+ return { value: [], scope: SCOPE.COMPLETE };
710
+ return { value: [], scope: SCOPE.UNREADABLE };
711
+ }
712
+ const value = [];
713
+ for (const archiveDir of archiveDirs) {
714
+ try {
715
+ const entries = node_fs_1.default.readdirSync(archiveDir, { withFileTypes: true });
716
+ for (const e of entries) {
717
+ if (!e.isDirectory())
718
+ continue;
719
+ const m = e.name.match(validate_cjs_1.PHASE_TOKEN_FROM_DIR_RE);
720
+ if (m)
721
+ value.push(stripProjectCodePrefix(m[1]));
722
+ }
723
+ }
724
+ catch {
725
+ /* archive dir absent/unreadable — mirrors forEachArchivedPhaseToken */
726
+ }
727
+ }
728
+ return { value, scope: SCOPE.COMPLETE };
729
+ }
730
+ /**
731
+ * Resolve `currentMilestoneRoadmapPhaseIds` — every phase-number token found
732
+ * in ROADMAP.md's content once scoped to the CURRENT milestone via
733
+ * `extractCurrentMilestone(content, cwd)`. Backs W026's archive-tolerant
734
+ * unstarted-phase scan; see the field's own doc comment on `PlanningSnapshot`
735
+ * for why `roadmapDeclaredPhases` cannot serve this. An absent/unreadable
736
+ * ROADMAP.md degrades to an empty list, mirroring every other
737
+ * ROADMAP-sourced field's absent-file handling.
738
+ */
739
+ function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath) {
740
+ if (!node_fs_1.default.existsSync(roadmapPath))
741
+ return { value: [], scope: SCOPE.UNREADABLE };
742
+ let content;
743
+ try {
744
+ content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
745
+ }
746
+ catch {
747
+ return { value: [], scope: SCOPE.UNREADABLE };
748
+ }
749
+ const scoped = extractCurrentMilestone(content, cwd);
750
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal
751
+ // mirror of OPTIONAL_PHASE_TAG_SOURCE) — verbatim from `verify.cts:2366`.
752
+ const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
753
+ const value = [...scoped.matchAll(phasePattern)].map((m) => m[1]);
754
+ return { value, scope: SCOPE.COMPLETE };
755
+ }
756
+ /**
757
+ * Resolve `perPhasePlanNumbering`/`perPhaseOrphanSummaries`/
758
+ * `perPhaseWaveMissingPlans` — Phase 12 (#3310, ADR-3180 §8.4), backing
759
+ * C002/C003/C004. One shared per-phase-directory scan serves all three
760
+ * fields (mirrors `buildStateFields`'s "one builder, several named outputs"
761
+ * convention above): each of the three questions below reads the exact same
762
+ * `scanPhasePlans(fullPhaseDir)` result, so scanning each phase directory
763
+ * three separate times (one function per field) would triple the
764
+ * `readdirSync`/frontmatter-read cost for zero behavioral gain — the three
765
+ * subjects are independent QUESTIONS, not independent SCANS.
766
+ *
767
+ * Enumerated over `allPhaseDirNames`, NOT `phaseDirs` (the
768
+ * current-milestone-windowed twin): the pre-migration `cmdValidateConsistency`
769
+ * (`verify.cts:1521-1608`) walks `collectPhaseRoots(planBase)`'s flat
770
+ * `phases/` root via a plain, unfiltered `readdirSync` — every phase
771
+ * directory on disk, not just the ones the current milestone window
772
+ * resolves as "in scope" — exactly the un-windowed shape `allPhaseDirNames`
773
+ * already exposes for W007 (see that field's own doc comment). Using the
774
+ * windowed `phaseDirs` here would silently narrow C002/C003/C004's coverage
775
+ * relative to the behavior being relocated. Disclosed fidelity note: this
776
+ * does NOT walk `collectPhaseRoots`'s second root (an active archived
777
+ * milestone's `<ver>-phases/` directory) — `allPhaseDirNames` is scoped to
778
+ * the flat `phases/` root only, the same scope every other
779
+ * `allPhaseDirNames`-sourced field already carries.
780
+ *
781
+ * QUESTION 1 — `perPhasePlanNumbering`: the sorted list of `-NN-PLAN.md`
782
+ * sequence numbers physically present (superseded or not — a retired plan
783
+ * still occupied a number), from `allPlanFiles` via the exact
784
+ * `/-(\d{2})-PLAN\.md$/` regex `verify.cts:1558` already uses. This field
785
+ * exposes the raw per-phase number list only; the future C002 rule computes
786
+ * the gap itself.
787
+ *
788
+ * QUESTION 2 — `perPhaseOrphanSummaries`: every SUMMARY.md with no matching
789
+ * LIVE PLAN.md, via `findOrphanSummaries(planFiles, summaryFiles)`
790
+ * (`core-utils.cjs`, `verify.cts:1584` — the same owner
791
+ * `src/health-diagnostic-rules/phase-structure.cts`'s I001 rule already
792
+ * consumes indirectly via `PhaseSnapshot.planCount`/`summaryCount`, for the
793
+ * INVERSE question). Uses the live (superseded-excluded) `planFiles`, not
794
+ * `allPlanFiles` — a superseded plan's summary is still an orphan.
795
+ *
796
+ * QUESTION 3 — `perPhaseWaveMissingPlans`: every LIVE plan (`planFiles`,
797
+ * same live set as Question 2 — a superseded plan legitimately carries no
798
+ * `wave`) whose frontmatter has no `wave` key, via `extractFrontmatter`,
799
+ * mirroring `verify.cts:1596-1603` exactly. A plan file that cannot be read
800
+ * is silently skipped, mirroring `cmdValidateConsistency`'s own outer
801
+ * `catch { intentionally empty }` (`verify.cts:1605-1607`) around this exact
802
+ * loop — a fail-open match to the pre-migration behavior, not a new scope
803
+ * degradation.
804
+ */
805
+ function buildPerPhasePlanScanFields(phasesDir, phaseDirNames, enumerationScope) {
806
+ const planNumbering = [];
807
+ const orphanSummaries = [];
808
+ const waveMissingPlans = [];
809
+ for (const phaseDir of phaseDirNames) {
810
+ const fullPhaseDir = node_path_1.default.join(phasesDir, phaseDir);
811
+ const { allPlanFiles, planFiles, summaryFiles } = scanPhasePlans(fullPhaseDir);
812
+ const planNums = allPlanFiles
813
+ .map((p) => {
814
+ const m = p.match(/-(\d{2})-PLAN\.md$/);
815
+ return m ? parseInt(m[1], 10) : null;
816
+ })
817
+ .filter((n) => n !== null)
818
+ .sort((a, b) => a - b);
819
+ planNumbering.push({ phaseDir, planNums });
820
+ for (const orphan of findOrphanSummaries(planFiles, summaryFiles)) {
821
+ orphanSummaries.push({ phaseDir, orphanSummary: orphan });
822
+ }
823
+ for (const plan of planFiles) {
824
+ try {
825
+ const planFilePath = node_path_1.default.join(fullPhaseDir, plan);
826
+ const content = node_fs_1.default.readFileSync(planFilePath, 'utf-8');
827
+ const fmData = extractFrontmatter(content, planFilePath);
828
+ if (!fmData['wave'])
829
+ waveMissingPlans.push({ phaseDir, plan });
830
+ }
831
+ catch {
832
+ /* unreadable plan file — mirrors verify.cts:1605-1607's own silent skip */
833
+ }
834
+ }
835
+ }
836
+ return {
837
+ perPhasePlanNumbering: { value: planNumbering, scope: enumerationScope },
838
+ perPhaseOrphanSummaries: { value: orphanSummaries, scope: enumerationScope },
839
+ perPhaseWaveMissingPlans: { value: waveMissingPlans, scope: enumerationScope },
840
+ };
841
+ }
842
+ /**
843
+ * Build the full `.planning/` projection for `cwd`. Composes the six §7
844
+ * owners named in the design doc's "Owners consumed" table, plus (Phase 11,
845
+ * #3309) the three additive subject-surface fields `config`/`agentInstall`/
846
+ * `worktreeHealth` — no re-derivation, no new semantic answer beyond what
847
+ * their respective owners already compute. See the design doc for the
848
+ * behavior table and rejected alternatives.
849
+ */
850
+ function buildPlanningSnapshot(cwd) {
851
+ const paths = planningPaths(cwd);
852
+ const milestone = getMilestoneInfo(cwd);
853
+ const phaseDirs = listMilestonePhaseDirs(paths.phases, { cwd });
854
+ const phasesValue = phaseDirs.value.map((dir) => buildPhaseSnapshot(paths.phases, dir));
855
+ const stateFields = buildStateFields(paths.state);
856
+ const allPhaseDirNames = buildAllPhaseDirNamesField(paths.phases);
857
+ const perPhasePlanScanFields = buildPerPhasePlanScanFields(paths.phases, allPhaseDirNames.value, allPhaseDirNames.scope);
858
+ return {
859
+ cwd: node_path_1.default.resolve(cwd),
860
+ milestone,
861
+ phaseDirs,
862
+ phases: {
863
+ value: phasesValue,
864
+ scope: worstScope(phaseDirs.scope, ...phasesValue.map((p) => p.scope)),
865
+ },
866
+ currentPhaseLabel: stateFields.currentPhaseLabel,
867
+ config: buildConfigField(cwd),
868
+ agentInstall: buildAgentInstallField(cwd),
869
+ worktreeHealth: buildWorktreeHealthField(cwd),
870
+ planningTracked: buildPlanningTrackedField(cwd),
871
+ projectSections: buildProjectSectionsField(cwd),
872
+ statePhaseTokens: stateFields.statePhaseTokens,
873
+ stateStatus: stateFields.stateStatus,
874
+ roadmapDeclaredPhases: buildRoadmapDeclaredPhasesField(paths.roadmap),
875
+ roadmapPhaseCheckboxes: buildRoadmapPhaseCheckboxesField(paths.roadmap),
876
+ researchValidationStatus: buildResearchValidationStatusField(paths.phases, phaseDirs.value, phaseDirs.scope),
877
+ milestoneArchiveStatus: buildMilestoneArchiveStatusField(cwd),
878
+ planningRootFiles: buildPlanningRootFilesField(cwd),
879
+ allPhaseDirNames,
880
+ archivedPhaseTokens: buildArchivedPhaseTokensField(paths.planning),
881
+ currentMilestoneRoadmapPhaseIds: buildCurrentMilestoneRoadmapPhaseIdsField(cwd, paths.roadmap),
882
+ perPhasePlanNumbering: perPhasePlanScanFields.perPhasePlanNumbering,
883
+ perPhaseOrphanSummaries: perPhasePlanScanFields.perPhaseOrphanSummaries,
884
+ perPhaseWaveMissingPlans: perPhasePlanScanFields.perPhaseWaveMissingPlans,
885
+ };
886
+ }
887
+ module.exports = {
888
+ buildPlanningSnapshot,
889
+ worstScope,
890
+ };