@opengsd/gsd-core 1.2.0-rc.1

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 (503) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja-JP.md +870 -0
  3. package/README.ko-KR.md +861 -0
  4. package/README.md +301 -0
  5. package/README.pt-BR.md +492 -0
  6. package/README.zh-CN.md +842 -0
  7. package/agents/gsd-advisor-researcher.md +127 -0
  8. package/agents/gsd-ai-researcher.md +133 -0
  9. package/agents/gsd-assumptions-analyzer.md +105 -0
  10. package/agents/gsd-code-fixer.md +668 -0
  11. package/agents/gsd-code-reviewer.md +387 -0
  12. package/agents/gsd-codebase-mapper.md +853 -0
  13. package/agents/gsd-debug-session-manager.md +314 -0
  14. package/agents/gsd-debugger.md +1452 -0
  15. package/agents/gsd-doc-classifier.md +168 -0
  16. package/agents/gsd-doc-synthesizer.md +204 -0
  17. package/agents/gsd-doc-verifier.md +217 -0
  18. package/agents/gsd-doc-writer.md +615 -0
  19. package/agents/gsd-domain-researcher.md +153 -0
  20. package/agents/gsd-eval-auditor.md +191 -0
  21. package/agents/gsd-eval-planner.md +154 -0
  22. package/agents/gsd-executor.md +772 -0
  23. package/agents/gsd-framework-selector.md +160 -0
  24. package/agents/gsd-integration-checker.md +470 -0
  25. package/agents/gsd-intel-updater.md +342 -0
  26. package/agents/gsd-nyquist-auditor.md +203 -0
  27. package/agents/gsd-pattern-mapper.md +335 -0
  28. package/agents/gsd-phase-researcher.md +928 -0
  29. package/agents/gsd-plan-checker.md +978 -0
  30. package/agents/gsd-planner.md +1218 -0
  31. package/agents/gsd-project-researcher.md +677 -0
  32. package/agents/gsd-research-synthesizer.md +255 -0
  33. package/agents/gsd-roadmapper.md +688 -0
  34. package/agents/gsd-security-auditor.md +155 -0
  35. package/agents/gsd-ui-auditor.md +495 -0
  36. package/agents/gsd-ui-checker.md +309 -0
  37. package/agents/gsd-ui-researcher.md +380 -0
  38. package/agents/gsd-user-profiler.md +171 -0
  39. package/agents/gsd-verifier.md +917 -0
  40. package/bin/install.js +10936 -0
  41. package/bin/lib/ui-safety-gate.cjs +107 -0
  42. package/commands/gsd/add-tests.md +42 -0
  43. package/commands/gsd/ai-integration-phase.md +37 -0
  44. package/commands/gsd/audit-fix.md +34 -0
  45. package/commands/gsd/audit-milestone.md +37 -0
  46. package/commands/gsd/audit-uat.md +24 -0
  47. package/commands/gsd/autonomous.md +46 -0
  48. package/commands/gsd/capture.md +62 -0
  49. package/commands/gsd/cleanup.md +24 -0
  50. package/commands/gsd/code-review.md +59 -0
  51. package/commands/gsd/complete-milestone.md +143 -0
  52. package/commands/gsd/config.md +56 -0
  53. package/commands/gsd/debug.md +52 -0
  54. package/commands/gsd/discuss-phase.md +76 -0
  55. package/commands/gsd/docs-update.md +49 -0
  56. package/commands/gsd/eval-review.md +33 -0
  57. package/commands/gsd/execute-phase.md +64 -0
  58. package/commands/gsd/explore.md +27 -0
  59. package/commands/gsd/extract-learnings.md +23 -0
  60. package/commands/gsd/fast.md +31 -0
  61. package/commands/gsd/forensics.md +57 -0
  62. package/commands/gsd/graphify.md +199 -0
  63. package/commands/gsd/health.md +31 -0
  64. package/commands/gsd/help.md +28 -0
  65. package/commands/gsd/import.md +41 -0
  66. package/commands/gsd/inbox.md +39 -0
  67. package/commands/gsd/ingest-docs.md +42 -0
  68. package/commands/gsd/manager.md +45 -0
  69. package/commands/gsd/map-codebase.md +83 -0
  70. package/commands/gsd/milestone-summary.md +51 -0
  71. package/commands/gsd/mvp-phase.md +45 -0
  72. package/commands/gsd/new-milestone.md +45 -0
  73. package/commands/gsd/new-project.md +47 -0
  74. package/commands/gsd/ns-context.md +23 -0
  75. package/commands/gsd/ns-ideate.md +24 -0
  76. package/commands/gsd/ns-manage.md +29 -0
  77. package/commands/gsd/ns-project.md +22 -0
  78. package/commands/gsd/ns-review.md +26 -0
  79. package/commands/gsd/ns-workflow.md +28 -0
  80. package/commands/gsd/pause-work.md +43 -0
  81. package/commands/gsd/phase.md +56 -0
  82. package/commands/gsd/plan-phase.md +62 -0
  83. package/commands/gsd/plan-review-convergence.md +59 -0
  84. package/commands/gsd/pr-branch.md +26 -0
  85. package/commands/gsd/profile-user.md +46 -0
  86. package/commands/gsd/progress.md +47 -0
  87. package/commands/gsd/quick.md +174 -0
  88. package/commands/gsd/resume-work.md +30 -0
  89. package/commands/gsd/review-backlog.md +63 -0
  90. package/commands/gsd/review.md +41 -0
  91. package/commands/gsd/secure-phase.md +36 -0
  92. package/commands/gsd/settings.md +29 -0
  93. package/commands/gsd/ship.md +24 -0
  94. package/commands/gsd/sketch.md +60 -0
  95. package/commands/gsd/spec-phase.md +63 -0
  96. package/commands/gsd/spike.md +57 -0
  97. package/commands/gsd/stats.md +19 -0
  98. package/commands/gsd/surface.md +155 -0
  99. package/commands/gsd/thread.md +24 -0
  100. package/commands/gsd/ui-phase.md +35 -0
  101. package/commands/gsd/ui-review.md +33 -0
  102. package/commands/gsd/ultraplan-phase.md +34 -0
  103. package/commands/gsd/undo.md +35 -0
  104. package/commands/gsd/update.md +48 -0
  105. package/commands/gsd/validate-phase.md +36 -0
  106. package/commands/gsd/verify-work.md +39 -0
  107. package/commands/gsd/workspace.md +52 -0
  108. package/commands/gsd/workstreams.md +70 -0
  109. package/get-shit-done/bin/check-latest-version.cjs +106 -0
  110. package/get-shit-done/bin/gsd-tools.cjs +1676 -0
  111. package/get-shit-done/bin/lib/active-workstream-store.cjs +302 -0
  112. package/get-shit-done/bin/lib/adr-parser.cjs +394 -0
  113. package/get-shit-done/bin/lib/agent-command-router.cjs +65 -0
  114. package/get-shit-done/bin/lib/artifacts.cjs +53 -0
  115. package/get-shit-done/bin/lib/audit.cjs +755 -0
  116. package/get-shit-done/bin/lib/check-command-router.cjs +333 -0
  117. package/get-shit-done/bin/lib/cjs-command-router-adapter.cjs +118 -0
  118. package/get-shit-done/bin/lib/clock.cjs +96 -0
  119. package/get-shit-done/bin/lib/clusters.cjs +135 -0
  120. package/get-shit-done/bin/lib/code-review-flags.cjs +74 -0
  121. package/get-shit-done/bin/lib/command-aliases.cjs +815 -0
  122. package/get-shit-done/bin/lib/command-arg-projection.cjs +62 -0
  123. package/get-shit-done/bin/lib/command-routing-hub.cjs +388 -0
  124. package/get-shit-done/bin/lib/commands.cjs +1188 -0
  125. package/get-shit-done/bin/lib/config-schema.cjs +31 -0
  126. package/get-shit-done/bin/lib/config.cjs +728 -0
  127. package/get-shit-done/bin/lib/configuration.cjs +248 -0
  128. package/get-shit-done/bin/lib/context-utilization.cjs +47 -0
  129. package/get-shit-done/bin/lib/core.cjs +2121 -0
  130. package/get-shit-done/bin/lib/decisions.cjs +116 -0
  131. package/get-shit-done/bin/lib/docs.cjs +270 -0
  132. package/get-shit-done/bin/lib/drift.cjs +388 -0
  133. package/get-shit-done/bin/lib/fallow-runner.cjs +109 -0
  134. package/get-shit-done/bin/lib/frontmatter.cjs +389 -0
  135. package/get-shit-done/bin/lib/gap-checker.cjs +205 -0
  136. package/get-shit-done/bin/lib/graphify.cjs +592 -0
  137. package/get-shit-done/bin/lib/gsd2-import.cjs +514 -0
  138. package/get-shit-done/bin/lib/init-command-router.cjs +58 -0
  139. package/get-shit-done/bin/lib/init.cjs +2112 -0
  140. package/get-shit-done/bin/lib/install-profiles.cjs +603 -0
  141. package/get-shit-done/bin/lib/installer-migration-authoring.cjs +117 -0
  142. package/get-shit-done/bin/lib/installer-migration-report.cjs +354 -0
  143. package/get-shit-done/bin/lib/installer-migrations/000-first-time-baseline.cjs +220 -0
  144. package/get-shit-done/bin/lib/installer-migrations/001-legacy-orphan-files.cjs +41 -0
  145. package/get-shit-done/bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs +80 -0
  146. package/get-shit-done/bin/lib/installer-migrations.cjs +778 -0
  147. package/get-shit-done/bin/lib/intel.cjs +708 -0
  148. package/get-shit-done/bin/lib/learnings.cjs +421 -0
  149. package/get-shit-done/bin/lib/milestone.cjs +314 -0
  150. package/get-shit-done/bin/lib/model-catalog.cjs +212 -0
  151. package/get-shit-done/bin/lib/model-profiles.cjs +31 -0
  152. package/get-shit-done/bin/lib/observability/event.cjs +82 -0
  153. package/get-shit-done/bin/lib/observability/logger.cjs +174 -0
  154. package/get-shit-done/bin/lib/observability/redaction.cjs +50 -0
  155. package/get-shit-done/bin/lib/package-identity.cjs +31 -0
  156. package/get-shit-done/bin/lib/phase-command-router.cjs +191 -0
  157. package/get-shit-done/bin/lib/phase-lifecycle.cjs +80 -0
  158. package/get-shit-done/bin/lib/phase.cjs +1607 -0
  159. package/get-shit-done/bin/lib/phases-command-router.cjs +39 -0
  160. package/get-shit-done/bin/lib/plan-scan.cjs +97 -0
  161. package/get-shit-done/bin/lib/planning-workspace.cjs +238 -0
  162. package/get-shit-done/bin/lib/profile-output.cjs +1141 -0
  163. package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
  164. package/get-shit-done/bin/lib/project-root.cjs +112 -0
  165. package/get-shit-done/bin/lib/prompt-budget.cjs +399 -0
  166. package/get-shit-done/bin/lib/review-reviewer-selection.cjs +125 -0
  167. package/get-shit-done/bin/lib/roadmap-command-router.cjs +28 -0
  168. package/get-shit-done/bin/lib/roadmap.cjs +650 -0
  169. package/get-shit-done/bin/lib/runtime-artifact-layout.cjs +301 -0
  170. package/get-shit-done/bin/lib/runtime-homes.cjs +222 -0
  171. package/get-shit-done/bin/lib/runtime-name-policy.cjs +83 -0
  172. package/get-shit-done/bin/lib/runtime-slash.cjs +112 -0
  173. package/get-shit-done/bin/lib/schema-detect.cjs +165 -0
  174. package/get-shit-done/bin/lib/secrets.cjs +32 -0
  175. package/get-shit-done/bin/lib/security.cjs +600 -0
  176. package/get-shit-done/bin/lib/semver-compare.cjs +35 -0
  177. package/get-shit-done/bin/lib/shell-command-projection.cjs +500 -0
  178. package/get-shit-done/bin/lib/state-command-router.cjs +252 -0
  179. package/get-shit-done/bin/lib/state-document.cjs +263 -0
  180. package/get-shit-done/bin/lib/state.cjs +2038 -0
  181. package/get-shit-done/bin/lib/surface.cjs +470 -0
  182. package/get-shit-done/bin/lib/task-command-router.cjs +81 -0
  183. package/get-shit-done/bin/lib/template.cjs +228 -0
  184. package/get-shit-done/bin/lib/uat.cjs +289 -0
  185. package/get-shit-done/bin/lib/update-context.cjs +209 -0
  186. package/get-shit-done/bin/lib/validate-command-router.cjs +83 -0
  187. package/get-shit-done/bin/lib/validate.cjs +92 -0
  188. package/get-shit-done/bin/lib/verify-command-router.cjs +40 -0
  189. package/get-shit-done/bin/lib/verify.cjs +1511 -0
  190. package/get-shit-done/bin/lib/workstream-inventory-builder.cjs +74 -0
  191. package/get-shit-done/bin/lib/workstream-inventory.cjs +146 -0
  192. package/get-shit-done/bin/lib/workstream-name-policy.cjs +94 -0
  193. package/get-shit-done/bin/lib/workstream.cjs +389 -0
  194. package/get-shit-done/bin/lib/worktree-safety.cjs +985 -0
  195. package/get-shit-done/bin/shared/config-defaults.manifest.json +97 -0
  196. package/get-shit-done/bin/shared/config-schema.manifest.json +175 -0
  197. package/get-shit-done/bin/shared/model-catalog.json +122 -0
  198. package/get-shit-done/bin/shared/runtime-aliases.manifest.json +75 -0
  199. package/get-shit-done/bin/verify-reapply-patches.cjs +352 -0
  200. package/get-shit-done/contexts/dev.md +21 -0
  201. package/get-shit-done/contexts/research.md +22 -0
  202. package/get-shit-done/contexts/review.md +23 -0
  203. package/get-shit-done/references/agent-contracts.md +79 -0
  204. package/get-shit-done/references/ai-evals.md +156 -0
  205. package/get-shit-done/references/ai-frameworks.md +186 -0
  206. package/get-shit-done/references/artifact-types.md +131 -0
  207. package/get-shit-done/references/autonomous-smart-discuss.md +277 -0
  208. package/get-shit-done/references/checkpoints.md +814 -0
  209. package/get-shit-done/references/common-bug-patterns.md +114 -0
  210. package/get-shit-done/references/context-budget.md +85 -0
  211. package/get-shit-done/references/continuation-format.md +253 -0
  212. package/get-shit-done/references/debugger-philosophy.md +76 -0
  213. package/get-shit-done/references/decimal-phase-calculation.md +64 -0
  214. package/get-shit-done/references/doc-conflict-engine.md +91 -0
  215. package/get-shit-done/references/domain-probes.md +125 -0
  216. package/get-shit-done/references/execute-mvp-tdd.md +81 -0
  217. package/get-shit-done/references/executor-examples.md +110 -0
  218. package/get-shit-done/references/few-shot-examples/plan-checker.md +73 -0
  219. package/get-shit-done/references/few-shot-examples/verifier.md +109 -0
  220. package/get-shit-done/references/gate-prompts.md +100 -0
  221. package/get-shit-done/references/gates.md +70 -0
  222. package/get-shit-done/references/git-integration.md +298 -0
  223. package/get-shit-done/references/git-planning-commit.md +40 -0
  224. package/get-shit-done/references/ios-scaffold.md +123 -0
  225. package/get-shit-done/references/mandatory-initial-read.md +2 -0
  226. package/get-shit-done/references/model-profile-resolution.md +38 -0
  227. package/get-shit-done/references/model-profiles.md +245 -0
  228. package/get-shit-done/references/mvp-concepts.md +49 -0
  229. package/get-shit-done/references/phase-argument-parsing.md +61 -0
  230. package/get-shit-done/references/planner-antipatterns.md +89 -0
  231. package/get-shit-done/references/planner-chunked.md +49 -0
  232. package/get-shit-done/references/planner-gap-closure.md +62 -0
  233. package/get-shit-done/references/planner-graphify-auto-update.md +67 -0
  234. package/get-shit-done/references/planner-human-verify-mode.md +57 -0
  235. package/get-shit-done/references/planner-interface-context.md +62 -0
  236. package/get-shit-done/references/planner-mvp-mode.md +53 -0
  237. package/get-shit-done/references/planner-reviews.md +39 -0
  238. package/get-shit-done/references/planner-revision.md +87 -0
  239. package/get-shit-done/references/planner-source-audit.md +73 -0
  240. package/get-shit-done/references/planning-config.md +471 -0
  241. package/get-shit-done/references/project-skills-discovery.md +19 -0
  242. package/get-shit-done/references/questioning.md +162 -0
  243. package/get-shit-done/references/revision-loop.md +97 -0
  244. package/get-shit-done/references/scout-codebase.md +51 -0
  245. package/get-shit-done/references/skeleton-template.md +48 -0
  246. package/get-shit-done/references/sketch-interactivity.md +41 -0
  247. package/get-shit-done/references/sketch-theme-system.md +94 -0
  248. package/get-shit-done/references/sketch-tooling.md +45 -0
  249. package/get-shit-done/references/sketch-variant-patterns.md +81 -0
  250. package/get-shit-done/references/spidr-splitting.md +69 -0
  251. package/get-shit-done/references/tdd.md +330 -0
  252. package/get-shit-done/references/thinking-models-debug.md +44 -0
  253. package/get-shit-done/references/thinking-models-execution.md +50 -0
  254. package/get-shit-done/references/thinking-models-planning.md +62 -0
  255. package/get-shit-done/references/thinking-models-research.md +50 -0
  256. package/get-shit-done/references/thinking-models-verification.md +55 -0
  257. package/get-shit-done/references/thinking-partner.md +96 -0
  258. package/get-shit-done/references/ui-brand.md +160 -0
  259. package/get-shit-done/references/universal-anti-patterns.md +63 -0
  260. package/get-shit-done/references/user-profiling.md +681 -0
  261. package/get-shit-done/references/user-story-template.md +58 -0
  262. package/get-shit-done/references/verification-overrides.md +227 -0
  263. package/get-shit-done/references/verification-patterns.md +612 -0
  264. package/get-shit-done/references/verify-mvp-mode.md +85 -0
  265. package/get-shit-done/references/workstream-flag.md +111 -0
  266. package/get-shit-done/references/worktree-path-safety.md +89 -0
  267. package/get-shit-done/templates/AI-SPEC.md +246 -0
  268. package/get-shit-done/templates/DEBUG.md +169 -0
  269. package/get-shit-done/templates/README.md +77 -0
  270. package/get-shit-done/templates/SECURITY.md +61 -0
  271. package/get-shit-done/templates/UAT.md +265 -0
  272. package/get-shit-done/templates/UI-SPEC.md +100 -0
  273. package/get-shit-done/templates/VALIDATION.md +76 -0
  274. package/get-shit-done/templates/claude-md.md +145 -0
  275. package/get-shit-done/templates/codebase/architecture.md +255 -0
  276. package/get-shit-done/templates/codebase/concerns.md +310 -0
  277. package/get-shit-done/templates/codebase/conventions.md +307 -0
  278. package/get-shit-done/templates/codebase/integrations.md +280 -0
  279. package/get-shit-done/templates/codebase/stack.md +186 -0
  280. package/get-shit-done/templates/codebase/structure.md +285 -0
  281. package/get-shit-done/templates/codebase/testing.md +480 -0
  282. package/get-shit-done/templates/config.json +62 -0
  283. package/get-shit-done/templates/context.md +352 -0
  284. package/get-shit-done/templates/continue-here.md +78 -0
  285. package/get-shit-done/templates/copilot-instructions.md +7 -0
  286. package/get-shit-done/templates/debug-subagent-prompt.md +91 -0
  287. package/get-shit-done/templates/dev-preferences.md +21 -0
  288. package/get-shit-done/templates/discovery.md +146 -0
  289. package/get-shit-done/templates/discussion-log.md +63 -0
  290. package/get-shit-done/templates/milestone-archive.md +123 -0
  291. package/get-shit-done/templates/milestone.md +115 -0
  292. package/get-shit-done/templates/phase-prompt.md +610 -0
  293. package/get-shit-done/templates/planner-subagent-prompt.md +117 -0
  294. package/get-shit-done/templates/project.md +186 -0
  295. package/get-shit-done/templates/requirements.md +231 -0
  296. package/get-shit-done/templates/research-project/ARCHITECTURE.md +204 -0
  297. package/get-shit-done/templates/research-project/FEATURES.md +147 -0
  298. package/get-shit-done/templates/research-project/PITFALLS.md +200 -0
  299. package/get-shit-done/templates/research-project/STACK.md +120 -0
  300. package/get-shit-done/templates/research-project/SUMMARY.md +170 -0
  301. package/get-shit-done/templates/research.md +592 -0
  302. package/get-shit-done/templates/retrospective.md +54 -0
  303. package/get-shit-done/templates/roadmap.md +202 -0
  304. package/get-shit-done/templates/spec.md +307 -0
  305. package/get-shit-done/templates/state.md +195 -0
  306. package/get-shit-done/templates/summary-complex.md +59 -0
  307. package/get-shit-done/templates/summary-minimal.md +41 -0
  308. package/get-shit-done/templates/summary-standard.md +48 -0
  309. package/get-shit-done/templates/summary.md +248 -0
  310. package/get-shit-done/templates/user-profile.md +146 -0
  311. package/get-shit-done/templates/user-setup.md +311 -0
  312. package/get-shit-done/templates/verification-report.md +322 -0
  313. package/get-shit-done/workflows/_runtime-launcher.snippet.sh +1 -0
  314. package/get-shit-done/workflows/add-backlog.md +91 -0
  315. package/get-shit-done/workflows/add-phase.md +113 -0
  316. package/get-shit-done/workflows/add-tests.md +355 -0
  317. package/get-shit-done/workflows/add-todo.md +161 -0
  318. package/get-shit-done/workflows/ai-integration-phase.md +295 -0
  319. package/get-shit-done/workflows/analyze-dependencies.md +96 -0
  320. package/get-shit-done/workflows/audit-fix.md +178 -0
  321. package/get-shit-done/workflows/audit-milestone.md +358 -0
  322. package/get-shit-done/workflows/audit-uat.md +110 -0
  323. package/get-shit-done/workflows/autonomous.md +795 -0
  324. package/get-shit-done/workflows/check-todos.md +180 -0
  325. package/get-shit-done/workflows/cleanup.md +155 -0
  326. package/get-shit-done/workflows/code-review-fix.md +502 -0
  327. package/get-shit-done/workflows/code-review.md +656 -0
  328. package/get-shit-done/workflows/complete-milestone.md +855 -0
  329. package/get-shit-done/workflows/debug.md +232 -0
  330. package/get-shit-done/workflows/diagnose-issues.md +241 -0
  331. package/get-shit-done/workflows/discovery-phase.md +291 -0
  332. package/get-shit-done/workflows/discuss-phase/modes/advisor.md +176 -0
  333. package/get-shit-done/workflows/discuss-phase/modes/all.md +28 -0
  334. package/get-shit-done/workflows/discuss-phase/modes/analyze.md +44 -0
  335. package/get-shit-done/workflows/discuss-phase/modes/auto.md +57 -0
  336. package/get-shit-done/workflows/discuss-phase/modes/batch.md +52 -0
  337. package/get-shit-done/workflows/discuss-phase/modes/chain.md +98 -0
  338. package/get-shit-done/workflows/discuss-phase/modes/default.md +141 -0
  339. package/get-shit-done/workflows/discuss-phase/modes/power.md +44 -0
  340. package/get-shit-done/workflows/discuss-phase/modes/text.md +55 -0
  341. package/get-shit-done/workflows/discuss-phase/templates/checkpoint.json +18 -0
  342. package/get-shit-done/workflows/discuss-phase/templates/context.md +136 -0
  343. package/get-shit-done/workflows/discuss-phase/templates/discussion-log.md +50 -0
  344. package/get-shit-done/workflows/discuss-phase-assumptions.md +675 -0
  345. package/get-shit-done/workflows/discuss-phase-power.md +291 -0
  346. package/get-shit-done/workflows/discuss-phase.md +499 -0
  347. package/get-shit-done/workflows/do.md +111 -0
  348. package/get-shit-done/workflows/docs-update.md +1162 -0
  349. package/get-shit-done/workflows/edit-phase.md +295 -0
  350. package/get-shit-done/workflows/eval-review.md +156 -0
  351. package/get-shit-done/workflows/execute-phase/steps/codebase-drift-gate.md +82 -0
  352. package/get-shit-done/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  353. package/get-shit-done/workflows/execute-phase/steps/post-merge-gate.md +117 -0
  354. package/get-shit-done/workflows/execute-phase.md +1709 -0
  355. package/get-shit-done/workflows/execute-plan.md +526 -0
  356. package/get-shit-done/workflows/explore.md +144 -0
  357. package/get-shit-done/workflows/extract-learnings.md +243 -0
  358. package/get-shit-done/workflows/fast.md +124 -0
  359. package/get-shit-done/workflows/forensics.md +279 -0
  360. package/get-shit-done/workflows/graduation.md +196 -0
  361. package/get-shit-done/workflows/health.md +224 -0
  362. package/get-shit-done/workflows/help/modes/brief.md +22 -0
  363. package/get-shit-done/workflows/help/modes/default.md +50 -0
  364. package/get-shit-done/workflows/help/modes/full.md +784 -0
  365. package/get-shit-done/workflows/help/modes/topic.md +74 -0
  366. package/get-shit-done/workflows/help.md +24 -0
  367. package/get-shit-done/workflows/import.md +254 -0
  368. package/get-shit-done/workflows/inbox.md +387 -0
  369. package/get-shit-done/workflows/ingest-docs.md +339 -0
  370. package/get-shit-done/workflows/insert-phase.md +152 -0
  371. package/get-shit-done/workflows/list-phase-assumptions.md +178 -0
  372. package/get-shit-done/workflows/list-workspaces.md +57 -0
  373. package/get-shit-done/workflows/manager.md +393 -0
  374. package/get-shit-done/workflows/map-codebase.md +444 -0
  375. package/get-shit-done/workflows/milestone-summary.md +224 -0
  376. package/get-shit-done/workflows/mvp-phase.md +222 -0
  377. package/get-shit-done/workflows/new-milestone.md +635 -0
  378. package/get-shit-done/workflows/new-project.md +1555 -0
  379. package/get-shit-done/workflows/new-workspace.md +240 -0
  380. package/get-shit-done/workflows/next.md +299 -0
  381. package/get-shit-done/workflows/node-repair.md +92 -0
  382. package/get-shit-done/workflows/note.md +158 -0
  383. package/get-shit-done/workflows/pause-work.md +244 -0
  384. package/get-shit-done/workflows/plan-milestone-gaps.md +281 -0
  385. package/get-shit-done/workflows/plan-phase.md +1809 -0
  386. package/get-shit-done/workflows/plan-review-convergence.md +346 -0
  387. package/get-shit-done/workflows/plant-seed.md +230 -0
  388. package/get-shit-done/workflows/pr-branch.md +157 -0
  389. package/get-shit-done/workflows/profile-user.md +453 -0
  390. package/get-shit-done/workflows/progress.md +699 -0
  391. package/get-shit-done/workflows/quick.md +1039 -0
  392. package/get-shit-done/workflows/reapply-patches.md +426 -0
  393. package/get-shit-done/workflows/remove-phase.md +156 -0
  394. package/get-shit-done/workflows/remove-workspace.md +108 -0
  395. package/get-shit-done/workflows/resume-project.md +332 -0
  396. package/get-shit-done/workflows/review.md +623 -0
  397. package/get-shit-done/workflows/scan.md +105 -0
  398. package/get-shit-done/workflows/secure-phase.md +180 -0
  399. package/get-shit-done/workflows/session-report.md +146 -0
  400. package/get-shit-done/workflows/settings-advanced.md +620 -0
  401. package/get-shit-done/workflows/settings-integrations.md +312 -0
  402. package/get-shit-done/workflows/settings.md +552 -0
  403. package/get-shit-done/workflows/ship.md +356 -0
  404. package/get-shit-done/workflows/sketch-wrap-up.md +286 -0
  405. package/get-shit-done/workflows/sketch.md +361 -0
  406. package/get-shit-done/workflows/spec-phase.md +262 -0
  407. package/get-shit-done/workflows/spike-wrap-up.md +307 -0
  408. package/get-shit-done/workflows/spike.md +453 -0
  409. package/get-shit-done/workflows/stats.md +80 -0
  410. package/get-shit-done/workflows/sync-skills.md +182 -0
  411. package/get-shit-done/workflows/thread.md +222 -0
  412. package/get-shit-done/workflows/transition.md +694 -0
  413. package/get-shit-done/workflows/ui-phase.md +328 -0
  414. package/get-shit-done/workflows/ui-review.md +193 -0
  415. package/get-shit-done/workflows/ultraplan-phase.md +199 -0
  416. package/get-shit-done/workflows/undo.md +314 -0
  417. package/get-shit-done/workflows/update.md +443 -0
  418. package/get-shit-done/workflows/validate-phase.md +179 -0
  419. package/get-shit-done/workflows/verify-phase.md +544 -0
  420. package/get-shit-done/workflows/verify-work.md +781 -0
  421. package/hooks/dist/gsd-check-update-worker.js +95 -0
  422. package/hooks/dist/gsd-check-update.js +64 -0
  423. package/hooks/dist/gsd-context-monitor.js +195 -0
  424. package/hooks/dist/gsd-graphify-update.sh +158 -0
  425. package/hooks/dist/gsd-phase-boundary.sh +47 -0
  426. package/hooks/dist/gsd-prompt-guard.js +97 -0
  427. package/hooks/dist/gsd-read-guard.js +101 -0
  428. package/hooks/dist/gsd-read-injection-scanner.js +203 -0
  429. package/hooks/dist/gsd-session-state.sh +59 -0
  430. package/hooks/dist/gsd-statusline.js +548 -0
  431. package/hooks/dist/gsd-update-banner.js +134 -0
  432. package/hooks/dist/gsd-validate-commit.sh +57 -0
  433. package/hooks/dist/gsd-workflow-guard.js +166 -0
  434. package/hooks/dist/lib/git-cmd.js +150 -0
  435. package/hooks/dist/lib/gsd-graphify-rebuild.sh +65 -0
  436. package/hooks/gsd-check-update-worker.js +95 -0
  437. package/hooks/gsd-check-update.js +64 -0
  438. package/hooks/gsd-context-monitor.js +195 -0
  439. package/hooks/gsd-graphify-update.sh +158 -0
  440. package/hooks/gsd-phase-boundary.sh +47 -0
  441. package/hooks/gsd-prompt-guard.js +97 -0
  442. package/hooks/gsd-read-guard.js +101 -0
  443. package/hooks/gsd-read-injection-scanner.js +203 -0
  444. package/hooks/gsd-session-state.sh +59 -0
  445. package/hooks/gsd-statusline.js +548 -0
  446. package/hooks/gsd-update-banner.js +134 -0
  447. package/hooks/gsd-validate-commit.sh +57 -0
  448. package/hooks/gsd-workflow-guard.js +166 -0
  449. package/hooks/lib/git-cmd.js +150 -0
  450. package/hooks/lib/gsd-graphify-rebuild.sh +65 -0
  451. package/hooks/managed-hooks-registry.cjs +34 -0
  452. package/package.json +102 -0
  453. package/scripts/affected-tests-lib.cjs +541 -0
  454. package/scripts/audit-workflow-script-paths.cjs +73 -0
  455. package/scripts/base64-scan.sh +339 -0
  456. package/scripts/build-hooks.js +236 -0
  457. package/scripts/changeset/README.md +129 -0
  458. package/scripts/changeset/cli.cjs +392 -0
  459. package/scripts/changeset/github-release-notes.cjs +199 -0
  460. package/scripts/changeset/lint.cjs +110 -0
  461. package/scripts/changeset/new.cjs +137 -0
  462. package/scripts/changeset/parse.cjs +114 -0
  463. package/scripts/changeset/render.cjs +34 -0
  464. package/scripts/changeset/serialize.cjs +130 -0
  465. package/scripts/check-alias-drift.cjs +108 -0
  466. package/scripts/check-env.cjs +302 -0
  467. package/scripts/check-npm-integrity.cjs +209 -0
  468. package/scripts/ci-guard-runner.cjs +16 -0
  469. package/scripts/ci-prepare-test-scope.cjs +46 -0
  470. package/scripts/ci-rebase-check.cjs +85 -0
  471. package/scripts/ci-test-scope.cjs +302 -0
  472. package/scripts/command-contract-helpers.cjs +64 -0
  473. package/scripts/diff-touches-shipped-paths.cjs +147 -0
  474. package/scripts/fix-slash-commands.cjs +147 -0
  475. package/scripts/gen-inventory-manifest.cjs +109 -0
  476. package/scripts/generate-package-identity.cjs +104 -0
  477. package/scripts/lint-command-contract.cjs +108 -0
  478. package/scripts/lint-descriptions.cjs +83 -0
  479. package/scripts/lint-docs-required.cjs +222 -0
  480. package/scripts/lint-no-source-grep-extras.cjs +81 -0
  481. package/scripts/lint-no-source-grep.cjs +174 -0
  482. package/scripts/lint-package-identity-drift.cjs +141 -0
  483. package/scripts/lint-pr-check-project-dir.cjs +98 -0
  484. package/scripts/lint-shared-module-handsync.cjs +388 -0
  485. package/scripts/lint-shell-command-projection-drift.cjs +57 -0
  486. package/scripts/lint-skill-deps.cjs +180 -0
  487. package/scripts/lint-test-file-count.allowlist.json +36 -0
  488. package/scripts/lint-test-file-count.cjs +190 -0
  489. package/scripts/pr-template-policy.cjs +268 -0
  490. package/scripts/prompt-injection-scan.sh +203 -0
  491. package/scripts/release-tarball-smoke.cjs +627 -0
  492. package/scripts/run-affected-tests.cjs +6 -0
  493. package/scripts/run-cross-platform-tests.cjs +63 -0
  494. package/scripts/run-tests.cjs +282 -0
  495. package/scripts/secret-scan-lint.sh +231 -0
  496. package/scripts/secret-scan.sh +358 -0
  497. package/scripts/setup-branch-protection.sh +236 -0
  498. package/scripts/shared-module-handsync-allowlist.json +183 -0
  499. package/scripts/strip-prose-atrefs.cjs +106 -0
  500. package/scripts/sync-rulesets.sh +34 -0
  501. package/scripts/sync-runtime-launcher.cjs +402 -0
  502. package/scripts/test-failure-reasons.cjs +34 -0
  503. package/scripts/workflow-policy.cjs +450 -0
@@ -0,0 +1,1511 @@
1
+ /**
2
+ * Verify — Verification suite, consistency, and health validation
3
+ */
4
+
5
+ const {
6
+ // Issue #6 exports (W006/W007 phase variant helpers)
7
+ phaseVariants, buildRoadmapPhaseVariants, buildNotStartedPhaseVariants,
8
+ // Issue #26 exports (W005 regex, W006-archived regex constants, I001 helper)
9
+ phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem,
10
+ } = require('./validate.cjs');
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const { loadConfig, normalizePhaseName, escapeRegex, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, output, error, checkAgentsInstalled, CONFIG_DEFAULTS, inspectWorktreeHealth } = require('./core.cjs');
16
+ const { execGit, platformReadSync: safeReadFile, platformWriteSync } = require('./shell-command-projection.cjs');
17
+ const { PACKAGE_NAME } = require('./package-identity.cjs');
18
+ const { planningDir } = require('./planning-workspace.cjs');
19
+ const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
20
+ const { writeStateMd } = require('./state.cjs');
21
+ const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs');
22
+
23
+ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
24
+ if (!summaryPath) {
25
+ error('summary-path required');
26
+ }
27
+
28
+ const fullPath = path.join(cwd, summaryPath);
29
+ const checkCount = checkFileCount || 2;
30
+
31
+ // Check 1: Summary exists
32
+ if (!fs.existsSync(fullPath)) {
33
+ const result = {
34
+ passed: false,
35
+ checks: {
36
+ summary_exists: false,
37
+ files_created: { checked: 0, found: 0, missing: [] },
38
+ commits_exist: false,
39
+ self_check: 'not_found',
40
+ },
41
+ errors: ['SUMMARY.md not found'],
42
+ };
43
+ output(result, raw, 'failed');
44
+ return;
45
+ }
46
+
47
+ const content = fs.readFileSync(fullPath, 'utf-8');
48
+ const errors = [];
49
+
50
+ // Check 2: Spot-check files mentioned in summary
51
+ const mentionedFiles = new Set();
52
+ const patterns = [
53
+ /`([^`]+\.[a-zA-Z]+)`/g,
54
+ /(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`]+\.[a-zA-Z]+)`?/gi,
55
+ ];
56
+
57
+ for (const pattern of patterns) {
58
+ let m;
59
+ while ((m = pattern.exec(content)) !== null) {
60
+ const filePath = m[1];
61
+ if (filePath && !filePath.startsWith('http') && filePath.includes('/')) {
62
+ mentionedFiles.add(filePath);
63
+ }
64
+ }
65
+ }
66
+
67
+ const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
68
+ const missing = [];
69
+ for (const file of filesToCheck) {
70
+ if (!fs.existsSync(path.join(cwd, file))) {
71
+ missing.push(file);
72
+ }
73
+ }
74
+
75
+ // Check 3: Commits exist
76
+ const commitHashPattern = /\b[0-9a-f]{7,40}\b/g;
77
+ const hashes = content.match(commitHashPattern) || [];
78
+ let commitsExist = false;
79
+ if (hashes.length > 0) {
80
+ for (const hash of hashes.slice(0, 3)) {
81
+ const result = execGit(['cat-file', '-t', hash], { cwd });
82
+ if (result.exitCode === 0 && result.stdout.trim() === 'commit') {
83
+ commitsExist = true;
84
+ break;
85
+ }
86
+ }
87
+ }
88
+
89
+ // Check 4: Self-check section
90
+ let selfCheck = 'not_found';
91
+ const selfCheckPattern = /##\s*(?:Self[- ]?Check|Verification|Quality Check)/i;
92
+ if (selfCheckPattern.test(content)) {
93
+ const passPattern = /(?:all\s+)?(?:pass|✓|✅|complete|succeeded)/i;
94
+ const failPattern = /(?:fail|✗|❌|incomplete|blocked)/i;
95
+ const checkSection = content.slice(content.search(selfCheckPattern));
96
+ if (failPattern.test(checkSection)) {
97
+ selfCheck = 'failed';
98
+ } else if (passPattern.test(checkSection)) {
99
+ selfCheck = 'passed';
100
+ }
101
+ }
102
+
103
+ if (missing.length > 0) errors.push('Missing files: ' + missing.join(', '));
104
+ if (!commitsExist && hashes.length > 0) errors.push('Referenced commit hashes not found in git history');
105
+ if (selfCheck === 'failed') errors.push('Self-check section indicates failure');
106
+
107
+ const checks = {
108
+ summary_exists: true,
109
+ files_created: { checked: filesToCheck.length, found: filesToCheck.length - missing.length, missing },
110
+ commits_exist: commitsExist,
111
+ self_check: selfCheck,
112
+ };
113
+
114
+ const passed = missing.length === 0 && selfCheck !== 'failed';
115
+ const result = { passed, checks, errors };
116
+ output(result, raw, passed ? 'passed' : 'failed');
117
+ }
118
+
119
+ function cmdVerifyPlanStructure(cwd, filePath, raw) {
120
+ if (!filePath) { error('file path required'); }
121
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
122
+ const content = safeReadFile(fullPath);
123
+ if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
124
+
125
+ const fm = extractFrontmatter(content);
126
+ const errors = [];
127
+ const warnings = [];
128
+
129
+ // Check required frontmatter fields
130
+ const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
131
+ for (const field of required) {
132
+ if (fm[field] === undefined) errors.push(`Missing required frontmatter field: ${field}`);
133
+ }
134
+
135
+ // Parse and check task elements
136
+ const taskPattern = /<task[^>]*>([\s\S]*?)<\/task>/g;
137
+ const tasks = [];
138
+ let taskMatch;
139
+ while ((taskMatch = taskPattern.exec(content)) !== null) {
140
+ const taskContent = taskMatch[1];
141
+ const nameMatch = taskContent.match(/<name>([\s\S]*?)<\/name>/);
142
+ const taskName = nameMatch ? nameMatch[1].trim() : 'unnamed';
143
+ const hasFiles = /<files>/.test(taskContent);
144
+ const hasAction = /<action>/.test(taskContent);
145
+ const hasVerify = /<verify>/.test(taskContent);
146
+ const hasDone = /<done>/.test(taskContent);
147
+
148
+ if (!nameMatch) errors.push('Task missing <name> element');
149
+ if (!hasAction) errors.push(`Task '${taskName}' missing <action>`);
150
+ if (!hasVerify) warnings.push(`Task '${taskName}' missing <verify>`);
151
+ if (!hasDone) warnings.push(`Task '${taskName}' missing <done>`);
152
+ if (!hasFiles) warnings.push(`Task '${taskName}' missing <files>`);
153
+
154
+ tasks.push({ name: taskName, hasFiles, hasAction, hasVerify, hasDone });
155
+ }
156
+
157
+ if (tasks.length === 0) warnings.push('No <task> elements found');
158
+
159
+ // Wave/depends_on consistency
160
+ if (fm.wave && parseInt(fm.wave) > 1 && (!fm.depends_on || (Array.isArray(fm.depends_on) && fm.depends_on.length === 0))) {
161
+ warnings.push('Wave > 1 but depends_on is empty');
162
+ }
163
+
164
+ // Autonomous/checkpoint consistency
165
+ const hasCheckpoints = /<task\s+type=["']?checkpoint/.test(content);
166
+ if (hasCheckpoints && fm.autonomous !== 'false' && fm.autonomous !== false) {
167
+ errors.push('Has checkpoint tasks but autonomous is not false');
168
+ }
169
+
170
+ output({
171
+ valid: errors.length === 0,
172
+ errors,
173
+ warnings,
174
+ task_count: tasks.length,
175
+ tasks,
176
+ frontmatter_fields: Object.keys(fm),
177
+ }, raw, errors.length === 0 ? 'valid' : 'invalid');
178
+ }
179
+
180
+ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
181
+ if (!phase) { error('phase required'); }
182
+ const phaseInfo = findPhaseInternal(cwd, phase);
183
+ if (!phaseInfo || !phaseInfo.found) {
184
+ output({ error: 'Phase not found', phase }, raw);
185
+ return;
186
+ }
187
+
188
+ const errors = [];
189
+ const warnings = [];
190
+ const phaseDir = path.join(cwd, phaseInfo.directory);
191
+
192
+ // List plans and summaries
193
+ let files;
194
+ try { files = fs.readdirSync(phaseDir); } catch { output({ error: 'Cannot read phase directory' }, raw); return; }
195
+
196
+ const plans = files.filter(f => f.match(/-PLAN\.md$/i));
197
+ const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i));
198
+
199
+ // Extract plan IDs (everything before -PLAN.md)
200
+ const planIds = new Set(plans.map(p => p.replace(/-PLAN\.md$/i, '')));
201
+ const summaryIds = new Set(summaries.map(s => s.replace(/-SUMMARY\.md$/i, '')));
202
+
203
+ // Plans without summaries
204
+ const incompletePlans = [...planIds].filter(id => !summaryIds.has(id));
205
+ if (incompletePlans.length > 0) {
206
+ errors.push(`Plans without summaries: ${incompletePlans.join(', ')}`);
207
+ }
208
+
209
+ // Summaries without plans (orphans)
210
+ const orphanSummaries = [...summaryIds].filter(id => !planIds.has(id));
211
+ if (orphanSummaries.length > 0) {
212
+ warnings.push(`Summaries without plans: ${orphanSummaries.join(', ')}`);
213
+ }
214
+
215
+ output({
216
+ complete: errors.length === 0,
217
+ phase: phaseInfo.phase_number,
218
+ plan_count: plans.length,
219
+ summary_count: summaries.length,
220
+ incomplete_plans: incompletePlans,
221
+ orphan_summaries: orphanSummaries,
222
+ errors,
223
+ warnings,
224
+ }, raw, errors.length === 0 ? 'complete' : 'incomplete');
225
+ }
226
+
227
+ function cmdVerifyReferences(cwd, filePath, raw) {
228
+ if (!filePath) { error('file path required'); }
229
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
230
+ const content = safeReadFile(fullPath);
231
+ if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
232
+
233
+ const found = [];
234
+ const missing = [];
235
+
236
+ // Find @-references: @path/to/file (must contain / to be a file path)
237
+ const atRefs = content.match(/@([^\s\n,)]+\/[^\s\n,)]+)/g) || [];
238
+ for (const ref of atRefs) {
239
+ const cleanRef = ref.slice(1); // remove @
240
+ const resolved = cleanRef.startsWith('~/')
241
+ ? path.join(process.env.HOME || '', cleanRef.slice(2))
242
+ : path.join(cwd, cleanRef);
243
+ if (fs.existsSync(resolved)) {
244
+ found.push(cleanRef);
245
+ } else {
246
+ missing.push(cleanRef);
247
+ }
248
+ }
249
+
250
+ // Find backtick file paths that look like real paths (contain / and have extension)
251
+ const backtickRefs = content.match(/`([^`]+\/[^`]+\.[a-zA-Z]{1,10})`/g) || [];
252
+ for (const ref of backtickRefs) {
253
+ const cleanRef = ref.slice(1, -1); // remove backticks
254
+ if (cleanRef.startsWith('http') || cleanRef.includes('${') || cleanRef.includes('{{')) continue;
255
+ if (found.includes(cleanRef) || missing.includes(cleanRef)) continue; // dedup
256
+ const resolved = path.join(cwd, cleanRef);
257
+ if (fs.existsSync(resolved)) {
258
+ found.push(cleanRef);
259
+ } else {
260
+ missing.push(cleanRef);
261
+ }
262
+ }
263
+
264
+ output({
265
+ valid: missing.length === 0,
266
+ found: found.length,
267
+ missing,
268
+ total: found.length + missing.length,
269
+ }, raw, missing.length === 0 ? 'valid' : 'invalid');
270
+ }
271
+
272
+ function cmdVerifyCommits(cwd, hashes, raw) {
273
+ if (!hashes || hashes.length === 0) { error('At least one commit hash required'); }
274
+
275
+ const valid = [];
276
+ const invalid = [];
277
+ for (const hash of hashes) {
278
+ const result = execGit(['cat-file', '-t', hash], { cwd });
279
+ if (result.exitCode === 0 && result.stdout.trim() === 'commit') {
280
+ valid.push(hash);
281
+ } else {
282
+ invalid.push(hash);
283
+ }
284
+ }
285
+
286
+ output({
287
+ all_valid: invalid.length === 0,
288
+ valid,
289
+ invalid,
290
+ total: hashes.length,
291
+ }, raw, invalid.length === 0 ? 'valid' : 'invalid');
292
+ }
293
+
294
+ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
295
+ if (!planFilePath) { error('plan file path required'); }
296
+ const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
297
+ const content = safeReadFile(fullPath);
298
+ if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
299
+
300
+ const artifacts = parseMustHavesBlock(content, 'artifacts');
301
+ if (artifacts.length === 0) {
302
+ output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
303
+ return;
304
+ }
305
+
306
+ const results = [];
307
+ for (const artifact of artifacts) {
308
+ if (typeof artifact === 'string') continue; // skip simple string items
309
+ const artPath = artifact.path;
310
+ if (!artPath) continue;
311
+
312
+ const artFullPath = path.join(cwd, artPath);
313
+ const exists = fs.existsSync(artFullPath);
314
+ const check = { path: artPath, exists, issues: [], passed: false };
315
+
316
+ if (exists) {
317
+ const fileContent = safeReadFile(artFullPath) || '';
318
+ const lineCount = fileContent.split('\n').length;
319
+
320
+ if (artifact.min_lines && lineCount < artifact.min_lines) {
321
+ check.issues.push(`Only ${lineCount} lines, need ${artifact.min_lines}`);
322
+ }
323
+ if (artifact.contains && !fileContent.includes(artifact.contains)) {
324
+ check.issues.push(`Missing pattern: ${artifact.contains}`);
325
+ }
326
+ if (artifact.exports) {
327
+ const exports = Array.isArray(artifact.exports) ? artifact.exports : [artifact.exports];
328
+ for (const exp of exports) {
329
+ if (!fileContent.includes(exp)) check.issues.push(`Missing export: ${exp}`);
330
+ }
331
+ }
332
+ check.passed = check.issues.length === 0;
333
+ } else {
334
+ check.issues.push('File not found');
335
+ }
336
+
337
+ results.push(check);
338
+ }
339
+
340
+ const passed = results.filter(r => r.passed).length;
341
+ output({
342
+ all_passed: passed === results.length,
343
+ passed,
344
+ total: results.length,
345
+ artifacts: results,
346
+ }, raw, passed === results.length ? 'valid' : 'invalid');
347
+ }
348
+
349
+ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
350
+ if (!planFilePath) { error('plan file path required'); }
351
+ const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
352
+ const content = safeReadFile(fullPath);
353
+ if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
354
+
355
+ const keyLinks = parseMustHavesBlock(content, 'key_links');
356
+ if (keyLinks.length === 0) {
357
+ output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
358
+ return;
359
+ }
360
+
361
+ const results = [];
362
+ for (const link of keyLinks) {
363
+ if (typeof link === 'string') continue;
364
+ const check = { from: link.from, to: link.to, via: link.via || '', verified: false, detail: '' };
365
+
366
+ const sourceContent = safeReadFile(path.join(cwd, link.from || ''));
367
+ if (!sourceContent) {
368
+ check.detail = 'Source file not found';
369
+ } else if (link.pattern) {
370
+ try {
371
+ const regex = new RegExp(link.pattern);
372
+ if (regex.test(sourceContent)) {
373
+ check.verified = true;
374
+ check.detail = 'Pattern found in source';
375
+ } else {
376
+ const targetContent = safeReadFile(path.join(cwd, link.to || ''));
377
+ if (targetContent && regex.test(targetContent)) {
378
+ check.verified = true;
379
+ check.detail = 'Pattern found in target';
380
+ } else {
381
+ check.detail = `Pattern "${link.pattern}" not found in source or target`;
382
+ }
383
+ }
384
+ } catch {
385
+ check.detail = `Invalid regex pattern: ${link.pattern}`;
386
+ }
387
+ } else {
388
+ // No pattern: just check source references target
389
+ if (sourceContent.includes(link.to || '')) {
390
+ check.verified = true;
391
+ check.detail = 'Target referenced in source';
392
+ } else {
393
+ check.detail = 'Target not referenced in source';
394
+ }
395
+ }
396
+
397
+ results.push(check);
398
+ }
399
+
400
+ const verified = results.filter(r => r.verified).length;
401
+ output({
402
+ all_verified: verified === results.length,
403
+ verified,
404
+ total: results.length,
405
+ links: results,
406
+ }, raw, verified === results.length ? 'valid' : 'invalid');
407
+ }
408
+
409
+ // PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE are sourced from
410
+ // validate.generated.cjs (issue #26, ADR-3524). No inline copies.
411
+
412
+ function listMilestoneArchiveDirs(planBase) {
413
+ const milestonesDir = path.join(planBase, 'milestones');
414
+ try {
415
+ return fs.readdirSync(milestonesDir, { withFileTypes: true })
416
+ .filter((e) => e.isDirectory() && MILESTONE_ARCHIVE_DIR_RE.test(e.name))
417
+ .map((e) => path.join(milestonesDir, e.name))
418
+ .sort((a, b) => path.basename(a).localeCompare(path.basename(b), undefined, { numeric: true }));
419
+ } catch {
420
+ return [];
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Walk every milestone archive directory and call `onPhase` with the phase
426
+ * token (e.g. `64`, `64A`, `64.1`) extracted from each archived phase dir's
427
+ * name. Mirrors `forEachArchivedPhaseToken` in sdk/src/query/validate.ts so
428
+ * Check 4 (W002) on the CJS side has the same archive-walking primitive.
429
+ * Bug #3652.
430
+ */
431
+ function forEachArchivedPhaseToken(planBase, onPhase) {
432
+ for (const archiveDir of listMilestoneArchiveDirs(planBase)) {
433
+ try {
434
+ const entries = fs.readdirSync(archiveDir, { withFileTypes: true });
435
+ for (const e of entries) {
436
+ if (!e.isDirectory()) continue;
437
+ const m = e.name.match(PHASE_TOKEN_FROM_DIR_RE);
438
+ if (m) onPhase(m[1]);
439
+ }
440
+ } catch { /* archive dir absent/unreadable */ }
441
+ }
442
+ }
443
+
444
+ function getActiveMilestoneArchiveDir(planBase) {
445
+ // Knuth invariant: the resolver answers exactly one question —
446
+ // "what archive directory holds the active milestone's phases?"
447
+ // Answer space: <dir> | null.
448
+ //
449
+ // When STATE.md is present and names a milestone:
450
+ // - If a matching milestones/<vX.Y>-phases/ directory exists → return it.
451
+ // - If no matching directory exists → return null. The active milestone
452
+ // has no archive yet (phases live in flat phases/). Falling through to
453
+ // an older milestone's archive is wrong and produces W007 false positives.
454
+ //
455
+ // The version-sort fallback to the newest archive fires ONLY when STATE.md is
456
+ // absent or unparseable — not when it cleanly names an unarchived milestone.
457
+
458
+ const archiveDirs = listMilestoneArchiveDirs(planBase);
459
+ if (archiveDirs.length === 0) return null;
460
+
461
+ // STATE.md present and parseable: match wins, no-match returns null.
462
+ try {
463
+ const statePath = path.join(planBase, 'STATE.md');
464
+ if (fs.existsSync(statePath)) {
465
+ const state = fs.readFileSync(statePath, 'utf-8');
466
+ const m = state.match(/^\s*(?:\*\*)?milestone(?:\*\*)?:\s*\*{0,2}\s*([^\s*\r\n#][^\s\r\n#]*)/mi);
467
+ if (m && m[1]) {
468
+ const milestone = m[1].trim();
469
+ const candidate = path.join(planBase, 'milestones', `${milestone}-phases`);
470
+ // Return the matching archive, or null if the active milestone has no archive yet.
471
+ return archiveDirs.includes(candidate) ? candidate : null;
472
+ }
473
+ }
474
+ } catch { /* intentionally empty — fall through to version-sort below */ }
475
+
476
+ // Fallback: STATE.md is absent or unparseable — highest (most recent) archive by version-ish name.
477
+ return archiveDirs[archiveDirs.length - 1];
478
+ }
479
+
480
+ function collectPhaseRoots(planBase) {
481
+ const roots = [];
482
+ const flatPhasesDir = path.join(planBase, 'phases');
483
+ if (fs.existsSync(flatPhasesDir)) roots.push(flatPhasesDir);
484
+ const activeArchive = getActiveMilestoneArchiveDir(planBase);
485
+ if (activeArchive) roots.push(activeArchive);
486
+ return roots;
487
+ }
488
+
489
+ // Returns a Set of phase numbers found on disk across active phase roots.
490
+ function collectDiskPhases(planBase) {
491
+ const diskPhases = new Set();
492
+ const phaseRoots = collectPhaseRoots(planBase);
493
+ const scanDir = (dir) => {
494
+ try {
495
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
496
+ for (const e of entries) {
497
+ if (e.isDirectory()) {
498
+ const m = e.name.match(PHASE_TOKEN_FROM_DIR_RE);
499
+ if (m) diskPhases.add(m[1]);
500
+ }
501
+ }
502
+ } catch { /* dir absent */ }
503
+ };
504
+
505
+ for (const root of phaseRoots) scanDir(root);
506
+
507
+ return diskPhases;
508
+ }
509
+
510
+ function cmdValidateConsistency(cwd, raw) {
511
+ const planBase = planningDir(cwd);
512
+ const roadmapPath = path.join(planBase, 'ROADMAP.md');
513
+ const errors = [];
514
+ const warnings = [];
515
+
516
+ // Check for ROADMAP
517
+ if (!fs.existsSync(roadmapPath)) {
518
+ errors.push('ROADMAP.md not found');
519
+ output({ passed: false, errors, warnings }, raw, 'failed');
520
+ return;
521
+ }
522
+
523
+ const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
524
+ const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
525
+
526
+ // Extract phases from the ACTIVE-milestone scope (archived milestones already
527
+ // stripped). Used for the "in ROADMAP but not on disk" check — we only require
528
+ // disk dirs for the active milestone's phases.
529
+ const roadmapPhases = new Set();
530
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
531
+ let m;
532
+ while ((m = phasePattern.exec(roadmapContent)) !== null) {
533
+ roadmapPhases.add(m[1]);
534
+ }
535
+
536
+ // Extract phases from the FULL ROADMAP (every milestone). Used for the
537
+ // "on disk but not in ROADMAP" orphan check: a phase dir belonging to a
538
+ // shipped milestone is expected to exist on disk and is NOT an orphan, even
539
+ // though it is absent from the active-milestone scope. Without this, narrowing
540
+ // the scope (#501) would flag every shipped phase dir as a spurious orphan.
541
+ const fullRoadmapPhases = new Set();
542
+ const fullPhasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
543
+ let fm;
544
+ while ((fm = fullPhasePattern.exec(roadmapContentRaw)) !== null) {
545
+ fullRoadmapPhases.add(fm[1]);
546
+ }
547
+
548
+ // Get phases on disk (flat layout + milestone-archive layout)
549
+ const diskPhases = collectDiskPhases(planBase);
550
+
551
+ // Check: phases in ROADMAP but not on disk (active-milestone scope)
552
+ for (const p of roadmapPhases) {
553
+ if (!diskPhases.has(p) && !diskPhases.has(normalizePhaseName(p))) {
554
+ warnings.push(`Phase ${p} in ROADMAP.md but no directory on disk`);
555
+ }
556
+ }
557
+
558
+ // Check: phases on disk but not in ROADMAP (compared against the FULL roadmap
559
+ // so shipped-milestone phase dirs are not flagged as orphans — #501)
560
+ for (const p of diskPhases) {
561
+ const unpadded = String(parseInt(p, 10));
562
+ if (!fullRoadmapPhases.has(p) && !fullRoadmapPhases.has(unpadded)) {
563
+ warnings.push(`Phase ${p} exists on disk but not in ROADMAP.md`);
564
+ }
565
+ }
566
+
567
+ // Check: sequential phase numbers (integers only, skip in custom naming mode)
568
+ const config = loadConfig(cwd);
569
+ if (config.phase_naming !== 'custom') {
570
+ const integerPhases = [...diskPhases]
571
+ .filter(p => !p.includes('.'))
572
+ .map(p => parseInt(p, 10))
573
+ .sort((a, b) => a - b);
574
+
575
+ for (let i = 1; i < integerPhases.length; i++) {
576
+ if (integerPhases[i] !== integerPhases[i - 1] + 1) {
577
+ warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
578
+ }
579
+ }
580
+ }
581
+
582
+ const phaseRoots = collectPhaseRoots(planBase);
583
+ for (const phaseRoot of phaseRoots) {
584
+ try {
585
+ const entries = fs.readdirSync(phaseRoot, { withFileTypes: true });
586
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
587
+
588
+ for (const dir of dirs) {
589
+ const phasePath = path.join(phaseRoot, dir);
590
+ const phaseLabel = path.relative(planBase, phasePath).replace(/\\/g, '/');
591
+ const phaseFiles = fs.readdirSync(phasePath);
592
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md')).sort();
593
+
594
+ // Extract plan numbers
595
+ const planNums = plans.map(p => {
596
+ const pm = p.match(/-(\d{2})-PLAN\.md$/);
597
+ return pm ? parseInt(pm[1], 10) : null;
598
+ }).filter(n => n !== null);
599
+
600
+ for (let i = 1; i < planNums.length; i++) {
601
+ if (planNums[i] !== planNums[i - 1] + 1) {
602
+ warnings.push(`Gap in plan numbering in ${phaseLabel}: plan ${planNums[i - 1]} → ${planNums[i]}`);
603
+ }
604
+ }
605
+
606
+ // Check: plans without summaries (completed plans)
607
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md'));
608
+ const planIds = new Set(plans.map(p => p.replace('-PLAN.md', '')));
609
+ const summaryIds = new Set(summaries.map(s => s.replace('-SUMMARY.md', '')));
610
+
611
+ // Summary without matching plan is suspicious
612
+ for (const sid of summaryIds) {
613
+ if (!planIds.has(sid)) {
614
+ warnings.push(`Summary ${sid}-SUMMARY.md in ${phaseLabel} has no matching PLAN.md`);
615
+ }
616
+ }
617
+
618
+ // Check: frontmatter in plans has required fields
619
+ for (const plan of plans) {
620
+ const content = fs.readFileSync(path.join(phasePath, plan), 'utf-8');
621
+ const fm = extractFrontmatter(content);
622
+ if (!fm.wave) {
623
+ warnings.push(`${phaseLabel}/${plan}: missing 'wave' in frontmatter`);
624
+ }
625
+ }
626
+ }
627
+ } catch { /* intentionally empty */ }
628
+ }
629
+
630
+ const passed = errors.length === 0;
631
+ output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
632
+ }
633
+
634
+ // canonicalPlanStem is sourced from validate.generated.cjs (issue #26, ADR-3524).
635
+ // No inline copy — see top-of-file require() for the import.
636
+
637
+ function cmdValidateHealth(cwd, options, raw) {
638
+ // Guard: detect if CWD is the home directory (likely accidental)
639
+ const resolved = path.resolve(cwd);
640
+ if (resolved === os.homedir()) {
641
+ output({
642
+ status: 'error',
643
+ errors: [{ code: 'E010', message: `CWD is home directory (${resolved}) — health check would read the wrong .planning/ directory. Run from your project root instead.`, fix: 'cd into your project directory and retry' }],
644
+ warnings: [],
645
+ info: [{ code: 'I010', message: `Resolved CWD: ${resolved}` }],
646
+ repairable_count: 0,
647
+ }, raw);
648
+ return;
649
+ }
650
+
651
+ const planBase = planningDir(cwd);
652
+ const projectPath = path.join(planBase, 'PROJECT.md');
653
+ const roadmapPath = path.join(planBase, 'ROADMAP.md');
654
+ const statePath = path.join(planBase, 'STATE.md');
655
+ const configPath = path.join(planBase, 'config.json');
656
+ const phasesDir = path.join(planBase, 'phases');
657
+ // Resolve runtime once so every emitted fix hint uses the routable slash
658
+ // form for this install (#3584).
659
+ const _slashRuntime = resolveRuntime(cwd);
660
+ const slash = (name) => formatGsdSlash(name, _slashRuntime);
661
+
662
+ const errors = [];
663
+ const warnings = [];
664
+ const info = [];
665
+ const repairs = [];
666
+
667
+ // Helper to add issue
668
+ const addIssue = (severity, code, message, fix, repairable = false) => {
669
+ const issue = { code, message, fix, repairable };
670
+ if (severity === 'error') errors.push(issue);
671
+ else if (severity === 'warning') warnings.push(issue);
672
+ else info.push(issue);
673
+ };
674
+
675
+ // ─── Check 1: .planning/ exists ───────────────────────────────────────────
676
+ if (!fs.existsSync(planBase)) {
677
+ addIssue('error', 'E001', '.planning/ directory not found', `Run ${slash('new-project')} to initialize`);
678
+ output({
679
+ status: 'broken',
680
+ errors,
681
+ warnings,
682
+ info,
683
+ repairable_count: 0,
684
+ }, raw);
685
+ return;
686
+ }
687
+
688
+ // ─── Check 2: PROJECT.md exists and has required sections ─────────────────
689
+ if (!fs.existsSync(projectPath)) {
690
+ addIssue('error', 'E002', 'PROJECT.md not found', `Run ${slash('new-project')} to create`);
691
+ } else {
692
+ const content = fs.readFileSync(projectPath, 'utf-8');
693
+ const requiredSections = ['## What This Is', '## Core Value', '## Requirements'];
694
+ for (const section of requiredSections) {
695
+ if (!content.includes(section)) {
696
+ addIssue('warning', 'W001', `PROJECT.md missing section: ${section}`, 'Add section manually');
697
+ }
698
+ }
699
+ }
700
+
701
+ // ─── Check 3: ROADMAP.md exists ───────────────────────────────────────────
702
+ if (!fs.existsSync(roadmapPath)) {
703
+ addIssue('error', 'E003', 'ROADMAP.md not found', `Run ${slash('new-milestone')} to create roadmap`);
704
+ }
705
+
706
+ // ─── Check 4: STATE.md exists and references valid phases ─────────────────
707
+ if (!fs.existsSync(statePath)) {
708
+ addIssue('error', 'E004', 'STATE.md not found', `Run ${slash('health')} --repair to regenerate`, true);
709
+ repairs.push('regenerateState');
710
+ } else {
711
+ const stateContent = fs.readFileSync(statePath, 'utf-8');
712
+ // Extract phase references from STATE.md
713
+ const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+[A-Z]?(?:\.\d+)*)/g)].map(m => m[1]);
714
+ // Bug #2633 — ROADMAP.md is the authority for which phases are valid.
715
+ // STATE.md may legitimately reference current-milestone future phases
716
+ // (not yet materialized on disk) and shipped-milestone history phases
717
+ // (archived / cleared off disk). Matching only against on-disk dirs
718
+ // produces false W002 warnings in both cases.
719
+ const validPhases = collectDiskPhases(planBase);
720
+ // Union in every phase declared anywhere in ROADMAP.md (current + shipped + backlog).
721
+ try {
722
+ if (fs.existsSync(roadmapPath)) {
723
+ const roadmapRaw = fs.readFileSync(roadmapPath, 'utf-8');
724
+ const all = [...roadmapRaw.matchAll(/#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi)];
725
+ for (const m of all) validPhases.add(m[1]);
726
+ }
727
+ } catch { /* intentionally empty */ }
728
+ // Bug #3652 — also union phases from every milestone archive, not only
729
+ // the active one. After /gsd:complete-milestone, historical phase dirs
730
+ // live under milestones/vX.Y-phases/ and their `#### Phase N:` headings
731
+ // get collapsed inside <details> blocks (which the heading regex above
732
+ // misses). collectDiskPhases() only scans the active archive, so
733
+ // without this step STATE.md's narrative references to older shipped
734
+ // phases fire false W002.
735
+ forEachArchivedPhaseToken(planBase, (token) => validPhases.add(token));
736
+ // Compare canonical full phase tokens. Also accept a leading-zero variant
737
+ // on the integer prefix only (e.g. "03" matching "3", "03.1" matching
738
+ // "3.1") so historic STATE.md formatting still validates. Suffix tokens
739
+ // like "3A" must match exactly — never collapsed to "3".
740
+ const normalizedValid = new Set();
741
+ for (const p of validPhases) {
742
+ normalizedValid.add(p);
743
+ const dotIdx = p.indexOf('.');
744
+ const head = dotIdx === -1 ? p : p.slice(0, dotIdx);
745
+ const tail = dotIdx === -1 ? '' : p.slice(dotIdx);
746
+ if (/^\d+$/.test(head)) {
747
+ normalizedValid.add(head.padStart(2, '0') + tail);
748
+ }
749
+ }
750
+ // Check for invalid references
751
+ for (const ref of phaseRefs) {
752
+ const dotIdx = ref.indexOf('.');
753
+ const head = dotIdx === -1 ? ref : ref.slice(0, dotIdx);
754
+ const tail = dotIdx === -1 ? '' : ref.slice(dotIdx);
755
+ const padded = /^\d+$/.test(head) ? head.padStart(2, '0') + tail : ref;
756
+ if (!normalizedValid.has(ref) && !normalizedValid.has(padded)) {
757
+ // Only warn if we know any valid phases (not just an empty project)
758
+ if (normalizedValid.size > 0) {
759
+ addIssue(
760
+ 'warning',
761
+ 'W002',
762
+ `STATE.md references phase ${ref}, but only phases ${[...validPhases].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).join(', ')} are declared`,
763
+ `Review STATE.md manually before changing it; ${slash('health')} --repair will not overwrite an existing STATE.md for phase mismatches`
764
+ );
765
+ }
766
+ }
767
+ }
768
+ }
769
+
770
+ // ─── Check 5: config.json valid JSON + valid schema ───────────────────────
771
+ if (!fs.existsSync(configPath)) {
772
+ addIssue('warning', 'W003', 'config.json not found', `Run ${slash('health')} --repair to create with defaults`, true);
773
+ repairs.push('createConfig');
774
+ } else {
775
+ try {
776
+ const raw = fs.readFileSync(configPath, 'utf-8');
777
+ const parsed = JSON.parse(raw);
778
+ // Validate known fields
779
+ const validProfiles = ['quality', 'balanced', 'budget', 'inherit'];
780
+ if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) {
781
+ addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
782
+ }
783
+ } catch (err) {
784
+ addIssue('error', 'E005', `config.json: JSON parse error - ${err.message}`, `Run ${slash('health')} --repair to reset to defaults`, true);
785
+ repairs.push('resetConfig');
786
+ }
787
+ }
788
+
789
+ // ─── Check 5b: Nyquist validation key presence ──────────────────────────
790
+ if (fs.existsSync(configPath)) {
791
+ try {
792
+ const configRaw = fs.readFileSync(configPath, 'utf-8');
793
+ const configParsed = JSON.parse(configRaw);
794
+ if (configParsed.workflow && configParsed.workflow.nyquist_validation === undefined) {
795
+ addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', `Run ${slash('health')} --repair to add key`, true);
796
+ if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey');
797
+ }
798
+ if (configParsed.workflow && configParsed.workflow.ai_integration_phase === undefined) {
799
+ addIssue('warning', 'W016', `config.json: workflow.ai_integration_phase absent (defaults to enabled — run ${slash('ai-integration-phase')} before planning AI system phases)`, `Run ${slash('health')} --repair to add key`, true);
800
+ if (!repairs.includes('addAiIntegrationPhaseKey')) repairs.push('addAiIntegrationPhaseKey');
801
+ }
802
+ } catch { /* intentionally empty */ }
803
+ }
804
+
805
+ // ─── Read phase directories once for checks 6, 7, 7b, and 8 (#1973) ──────
806
+ let phaseDirEntries = [];
807
+ const phaseDirFiles = new Map(); // phase dir name → file list
808
+ try {
809
+ phaseDirEntries = fs.readdirSync(phasesDir, { withFileTypes: true }).filter(e => e.isDirectory());
810
+ for (const e of phaseDirEntries) {
811
+ try {
812
+ phaseDirFiles.set(e.name, fs.readdirSync(path.join(phasesDir, e.name)));
813
+ } catch { phaseDirFiles.set(e.name, []); }
814
+ }
815
+ } catch { /* intentionally empty */ }
816
+
817
+ // ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
818
+ // phaseDirNameRe sourced from validate.generated.cjs (issue #26, ADR-3524).
819
+ for (const e of phaseDirEntries) {
820
+ if (!e.name.match(phaseDirNameRe)) {
821
+ addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
822
+ }
823
+ }
824
+
825
+ // ─── Check 7: Orphaned plans (PLAN without SUMMARY) ───────────────────────
826
+ for (const e of phaseDirEntries) {
827
+ const phaseFiles = phaseDirFiles.get(e.name) || [];
828
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
829
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
830
+ const summaryBases = new Set();
831
+ for (const s of summaries) {
832
+ const summaryBase = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
833
+ summaryBases.add(summaryBase);
834
+ summaryBases.add(canonicalPlanStem(summaryBase));
835
+ }
836
+
837
+ for (const plan of plans) {
838
+ const planBase = plan.replace('-PLAN.md', '').replace('PLAN.md', '');
839
+ const canonicalBase = canonicalPlanStem(planBase);
840
+ if (!summaryBases.has(planBase) && !summaryBases.has(canonicalBase)) {
841
+ addIssue('info', 'I001', `${e.name}/${plan} has no SUMMARY.md`, 'May be in progress');
842
+ }
843
+ }
844
+ }
845
+
846
+ // ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
847
+ for (const e of phaseDirEntries) {
848
+ const phaseFiles = phaseDirFiles.get(e.name) || [];
849
+ const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md'));
850
+ const hasValidation = phaseFiles.some(f => f.endsWith('-VALIDATION.md'));
851
+ if (hasResearch && !hasValidation) {
852
+ const researchFile = phaseFiles.find(f => f.endsWith('-RESEARCH.md'));
853
+ try {
854
+ const researchContent = fs.readFileSync(path.join(phasesDir, e.name, researchFile), 'utf-8');
855
+ if (researchContent.includes('## Validation Architecture')) {
856
+ addIssue('warning', 'W009', `Phase ${e.name}: has Validation Architecture in RESEARCH.md but no VALIDATION.md`, `Re-run ${slash('plan-phase')} with --research to regenerate`);
857
+ }
858
+ } catch { /* intentionally empty */ }
859
+ }
860
+ }
861
+
862
+ // ─── Check 7c: Agent installation (#1371) ──────────────────────────────────
863
+ // Verify GSD agents are installed. Missing agents cause Task(subagent_type=...)
864
+ // to silently fall back to general-purpose, losing specialized instructions.
865
+ try {
866
+ const agentStatus = checkAgentsInstalled();
867
+ if (!agentStatus.agents_installed) {
868
+ if (agentStatus.installed_agents.length === 0) {
869
+ addIssue('warning', 'W010',
870
+ `No GSD agents found in ${agentStatus.agents_dir} — Task(subagent_type="gsd-*") will fall back to general-purpose`,
871
+ `Run the GSD installer: npx ${PACKAGE_NAME}@latest`);
872
+ } else {
873
+ addIssue('warning', 'W010',
874
+ `Missing ${agentStatus.missing_agents.length} GSD agents: ${agentStatus.missing_agents.join(', ')} — affected workflows will fall back to general-purpose`,
875
+ `Run the GSD installer: npx ${PACKAGE_NAME}@latest`);
876
+ }
877
+ }
878
+ } catch { /* intentionally empty — agent check is non-blocking */ }
879
+
880
+ // ─── Check 8: Run existing consistency checks ─────────────────────────────
881
+ // Inline subset of cmdValidateConsistency. Unlike Check 4 (W002), this
882
+ // check filters ROADMAP.md through extractCurrentMilestone first — shipped
883
+ // milestones are stripped before the heading scan. However, a phase can
884
+ // appear in the CURRENT milestone AND have its directory inside a milestone
885
+ // archive (completed + archived). forEachArchivedPhaseToken is therefore
886
+ // called below to add archived dirs to diskPhases so W006 does not fire
887
+ // for them. (#3652, #3806)
888
+ //
889
+ // Fix #6 (three drift items vs sdk/src/query/validate.ts Check 8):
890
+ // 1. activeDiskPhases: separate from diskPhases; only active phasesDir phases.
891
+ // W007 uses activeDiskPhases so archived phases don't produce false W007.
892
+ // 2. phaseVariants() + buildRoadmapPhaseVariants(): W006 disk-existence check
893
+ // and W007 roadmap-membership check now use full variant sets, fixing
894
+ // false W006/W007 for letter-suffix phases with padding mismatch.
895
+ // 3. buildNotStartedPhaseVariants(): replaces raw+parseInt-padded notStartedPhases
896
+ // with phaseVariants() expansion, fixing W006 unchecked-phase skip for
897
+ // zero-padded letter-suffix forms.
898
+ if (fs.existsSync(roadmapPath)) {
899
+ const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
900
+ const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
901
+
902
+ // roadmapPhases (active-milestone scope): used for the W006 disk-existence
903
+ // check (preserve original for message). W006 stays scoped to the active
904
+ // milestone — we only require disk dirs for the current milestone's phases.
905
+ const { roadmapPhases } = buildRoadmapPhaseVariants(roadmapContent);
906
+
907
+ // W007 (on-disk-but-not-in-roadmap) must check membership against the FULL
908
+ // roadmap (every milestone), not just the active-milestone scope. A phase dir
909
+ // belonging to a shipped milestone is expected on disk; flagging it as a W007
910
+ // orphan once the scope is narrowed (#501) would be spurious noise.
911
+ const { roadmapPhaseVariants: fullRoadmapPhaseVariants } =
912
+ buildRoadmapPhaseVariants(roadmapContentRaw);
913
+
914
+ // diskPhases: active phasesDir + archived milestone dirs (for W006 — archived phases
915
+ // are valid on-disk locations for historical ROADMAP phases).
916
+ const diskPhases = collectDiskPhases(planBase);
917
+ // Include archived milestone phase directories as valid on-disk locations.
918
+ // Mirrors forEachArchivedPhaseToken call in sdk/src/query/validate.ts Check 8. (#3806)
919
+ forEachArchivedPhaseToken(planBase, (token) => diskPhases.add(token));
920
+
921
+ // activeDiskPhases: only phases from the active phasesDir (NOT archived).
922
+ // Used for W007: archived phases should not trigger W007 even if absent from
923
+ // current ROADMAP (they were shipped in a prior milestone). (#6 drift item 1)
924
+ const activeDiskPhases = collectDiskPhases(planBase);
925
+
926
+ // Build a set of all variants of phases explicitly marked not-yet-started in
927
+ // the ROADMAP summary list (- [ ] **Phase N:**). These phases are intentionally
928
+ // absent from disk — W006 must not fire for them. (#2009, #6 drift item 3)
929
+ // buildNotStartedPhaseVariants() uses phaseVariants() so zero-padded letter-suffix
930
+ // forms like "03B" are recognized even when the unchecked entry says "3B".
931
+ const notStartedPhases = buildNotStartedPhaseVariants(roadmapContent);
932
+
933
+ // Phases in ROADMAP but not on disk (W006)
934
+ // Uses phaseVariants() for disk-existence check so "3B" matches disk dir "03B-foo".
935
+ for (const p of roadmapPhases) {
936
+ const variants = phaseVariants(p);
937
+ const existsOnDisk = [...variants].some((v) => diskPhases.has(v));
938
+ if (!existsOnDisk) {
939
+ // Skip phases explicitly flagged as not-yet-started in the summary list
940
+ const isNotStarted = [...variants].some((v) => notStartedPhases.has(v));
941
+ if (isNotStarted) continue;
942
+ addIssue('warning', 'W006', `Phase ${p} in ROADMAP.md but no directory on disk`, 'Create phase directory or remove from roadmap');
943
+ }
944
+ }
945
+
946
+ // Phases on disk but not in ROADMAP (W007)
947
+ // Uses activeDiskPhases (no archived) and roadmapPhaseVariants (all variants)
948
+ // so neither archived phases nor padding-mismatch phases trigger false W007.
949
+ for (const p of activeDiskPhases) {
950
+ const variants = phaseVariants(p);
951
+ if (![...variants].some((v) => fullRoadmapPhaseVariants.has(v))) {
952
+ addIssue('warning', 'W007', `Phase ${p} exists on disk but not in ROADMAP.md`, 'Add to roadmap or remove directory');
953
+ }
954
+ }
955
+ }
956
+
957
+ // ─── Check 9: STATE.md / ROADMAP.md cross-validation ─────────────────────
958
+ if (fs.existsSync(statePath) && fs.existsSync(roadmapPath)) {
959
+ try {
960
+ const stateContent = fs.readFileSync(statePath, 'utf-8');
961
+ const roadmapContentFull = fs.readFileSync(roadmapPath, 'utf-8');
962
+
963
+ // Extract current phase from STATE.md
964
+ const currentPhaseMatch = stateContent.match(/\*\*Current Phase:\*\*\s*(\S+)/i) ||
965
+ stateContent.match(/Current Phase:\s*(\S+)/i);
966
+ if (currentPhaseMatch) {
967
+ const statePhase = currentPhaseMatch[1].replace(/^0+/, '');
968
+ // Check if ROADMAP shows this phase as already complete
969
+ const phaseCheckboxRe = new RegExp(`-\\s*\\[x\\].*Phase\\s+0*${escapeRegex(statePhase)}[:\\s]`, 'i');
970
+ if (phaseCheckboxRe.test(roadmapContentFull)) {
971
+ // STATE says "current" but ROADMAP says "complete" — divergence
972
+ const stateStatus = stateContent.match(/\*\*Status:\*\*\s*(.+)/i);
973
+ const statusVal = stateStatus ? stateStatus[1].trim().toLowerCase() : '';
974
+ if (statusVal !== 'complete' && statusVal !== 'done') {
975
+ addIssue('warning', 'W011',
976
+ `STATE.md says current phase is ${statePhase} (status: ${statusVal || 'unknown'}) but ROADMAP.md shows it as [x] complete — state files may be out of sync`,
977
+ `Run ${slash('progress')} to re-derive current position, or manually update STATE.md`);
978
+ }
979
+ }
980
+ }
981
+ } catch { /* intentionally empty — cross-validation is advisory */ }
982
+ }
983
+
984
+ // ─── Check 10: Config field validation ────────────────────────────────────
985
+ if (fs.existsSync(configPath)) {
986
+ try {
987
+ const configRaw = fs.readFileSync(configPath, 'utf-8');
988
+ const configParsed = JSON.parse(configRaw);
989
+
990
+ // Validate branching_strategy
991
+ const validStrategies = ['none', 'phase', 'milestone'];
992
+ if (configParsed.branching_strategy && !validStrategies.includes(configParsed.branching_strategy)) {
993
+ addIssue('warning', 'W012',
994
+ `config.json: invalid branching_strategy "${configParsed.branching_strategy}"`,
995
+ `Valid values: ${validStrategies.join(', ')}`);
996
+ }
997
+
998
+ // Validate context_window is a positive integer
999
+ if (configParsed.context_window !== undefined) {
1000
+ const cw = configParsed.context_window;
1001
+ if (typeof cw !== 'number' || cw <= 0 || !Number.isInteger(cw)) {
1002
+ addIssue('warning', 'W013',
1003
+ `config.json: context_window should be a positive integer, got "${cw}"`,
1004
+ 'Set to 200000 (default) or 1000000 (for 1M models)');
1005
+ }
1006
+ }
1007
+
1008
+ // Validate branch templates have required placeholders
1009
+ if (configParsed.phase_branch_template && !configParsed.phase_branch_template.includes('{phase}')) {
1010
+ addIssue('warning', 'W014',
1011
+ 'config.json: phase_branch_template missing {phase} placeholder',
1012
+ 'Template must include {phase} for phase number substitution');
1013
+ }
1014
+ if (configParsed.milestone_branch_template && !configParsed.milestone_branch_template.includes('{milestone}')) {
1015
+ addIssue('warning', 'W015',
1016
+ 'config.json: milestone_branch_template missing {milestone} placeholder',
1017
+ 'Template must include {milestone} for version substitution');
1018
+ }
1019
+ } catch { /* parse error already caught in Check 5 */ }
1020
+ }
1021
+
1022
+ // ─── Check 11: Stale / orphan git worktrees (#2167) ────────────────────────
1023
+ try {
1024
+ const worktreeHealth = inspectWorktreeHealth(
1025
+ cwd,
1026
+ { staleAfterMs: 60 * 60 * 1000 },
1027
+ { execGit, existsSync: fs.existsSync, statSync: fs.statSync }
1028
+ );
1029
+ if (!worktreeHealth.ok) {
1030
+ // AC2 / AC3: surface degraded-git state as a structured warning instead
1031
+ // of silently suppressing it (PRED.k302 — error-swallowing-empty-sentinel).
1032
+ if (worktreeHealth.reason === 'git_timed_out') {
1033
+ addIssue('warning', 'W020',
1034
+ 'Worktree health check degraded: git worktree list timed out after 10s — orphan/stale worktrees could not be inspected',
1035
+ 'Run: git worktree list --porcelain to diagnose; check for .git/index.lock or a hung git process');
1036
+ }
1037
+ if (worktreeHealth.reason === 'git_list_failed') {
1038
+ addIssue('warning', 'W020',
1039
+ 'Worktree health check degraded: git worktree list failed — orphan/stale worktrees could not be inspected',
1040
+ 'Run: git worktree list --porcelain to diagnose; check git repository state and permissions');
1041
+ }
1042
+ // Other non-ok reasons (not_a_git_repo) are silent — not meaningful for
1043
+ // users who have no git repo.
1044
+ } else {
1045
+ for (const finding of worktreeHealth.findings) {
1046
+ if (finding.kind === 'orphan') {
1047
+ addIssue('warning', 'W017',
1048
+ `Orphan git worktree: ${finding.path} (path no longer exists on disk)`,
1049
+ 'Run: git worktree prune');
1050
+ continue;
1051
+ }
1052
+
1053
+ if (finding.kind === 'stale') {
1054
+ addIssue('warning', 'W017',
1055
+ `Stale git worktree: ${finding.path} (last modified ${finding.ageMinutes} minutes ago)`,
1056
+ `Run: git worktree remove ${finding.path} --force`);
1057
+ }
1058
+ }
1059
+ }
1060
+ } catch { /* git worktree not available or not a git repo — skip silently */ }
1061
+
1062
+ // ─── Check 12: MILESTONES.md / archive snapshot drift (#2446) ─────────────
1063
+ const milestonesPath = path.join(planBase, 'MILESTONES.md');
1064
+ const milestonesArchiveDir = path.join(planBase, 'milestones');
1065
+ const missingFromRegistry = [];
1066
+ try {
1067
+ if (fs.existsSync(milestonesArchiveDir)) {
1068
+ const archiveFiles = fs.readdirSync(milestonesArchiveDir);
1069
+ const archivedVersions = archiveFiles
1070
+ .map(f => f.match(/^(v\d+\.\d+(?:\.\d+)?)-ROADMAP\.md$/))
1071
+ .filter(Boolean)
1072
+ .map(m => m[1]);
1073
+
1074
+ if (archivedVersions.length > 0) {
1075
+ const registryContent = fs.existsSync(milestonesPath)
1076
+ ? fs.readFileSync(milestonesPath, 'utf-8')
1077
+ : '';
1078
+ for (const ver of archivedVersions) {
1079
+ if (!registryContent.includes(`## ${ver}`)) {
1080
+ missingFromRegistry.push(ver);
1081
+ }
1082
+ }
1083
+ if (missingFromRegistry.length > 0) {
1084
+ addIssue('warning', 'W018',
1085
+ `MILESTONES.md missing ${missingFromRegistry.length} archived milestone(s): ${missingFromRegistry.join(', ')}`,
1086
+ `Run ${slash('health')} --backfill to synthesize missing entries from archive snapshots`,
1087
+ true);
1088
+ repairs.push('backfillMilestones');
1089
+ }
1090
+ }
1091
+ }
1092
+ } catch { /* intentionally empty — milestone sync check is advisory */ }
1093
+
1094
+ // ─── Check 13: Unrecognized .planning/ root files (W019) ──────────────────
1095
+ try {
1096
+ const { isCanonicalPlanningFile } = require('./artifacts.cjs');
1097
+ const entries = fs.readdirSync(planBase, { withFileTypes: true });
1098
+ for (const entry of entries) {
1099
+ if (!entry.isFile()) continue;
1100
+ if (!entry.name.endsWith('.md')) continue;
1101
+ if (!isCanonicalPlanningFile(entry.name)) {
1102
+ addIssue('warning', 'W019',
1103
+ `Unrecognized .planning/ file: ${entry.name} — not a canonical GSD artifact`,
1104
+ 'Move to .planning/milestones/ archive subdir or delete if stale. See templates/README.md for the canonical artifact list.',
1105
+ false);
1106
+ }
1107
+ }
1108
+ } catch { /* artifact check is advisory — skip on error */ }
1109
+
1110
+ // ─── Perform repairs if requested ─────────────────────────────────────────
1111
+ const repairActions = [];
1112
+ if (options.repair && repairs.length > 0) {
1113
+ for (const repair of repairs) {
1114
+ try {
1115
+ switch (repair) {
1116
+ case 'createConfig':
1117
+ case 'resetConfig': {
1118
+ const defaults = {
1119
+ model_profile: CONFIG_DEFAULTS.model_profile,
1120
+ commit_docs: CONFIG_DEFAULTS.commit_docs,
1121
+ search_gitignored: CONFIG_DEFAULTS.search_gitignored,
1122
+ branching_strategy: CONFIG_DEFAULTS.branching_strategy,
1123
+ phase_branch_template: CONFIG_DEFAULTS.phase_branch_template,
1124
+ milestone_branch_template: CONFIG_DEFAULTS.milestone_branch_template,
1125
+ quick_branch_template: CONFIG_DEFAULTS.quick_branch_template,
1126
+ workflow: {
1127
+ research: CONFIG_DEFAULTS.research,
1128
+ plan_check: CONFIG_DEFAULTS.plan_checker,
1129
+ verifier: CONFIG_DEFAULTS.verifier,
1130
+ nyquist_validation: CONFIG_DEFAULTS.nyquist_validation,
1131
+ },
1132
+ parallelization: CONFIG_DEFAULTS.parallelization,
1133
+ brave_search: CONFIG_DEFAULTS.brave_search,
1134
+ };
1135
+ platformWriteSync(configPath, JSON.stringify(defaults, null, 2));
1136
+ repairActions.push({ action: repair, success: true, path: 'config.json' });
1137
+ break;
1138
+ }
1139
+ case 'regenerateState': {
1140
+ // Create timestamped backup before overwriting
1141
+ if (fs.existsSync(statePath)) {
1142
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
1143
+ const backupPath = `${statePath}.bak-${timestamp}`;
1144
+ fs.copyFileSync(statePath, backupPath);
1145
+ repairActions.push({ action: 'backupState', success: true, path: backupPath });
1146
+ }
1147
+ // Generate minimal STATE.md from ROADMAP.md structure
1148
+ const milestone = getMilestoneInfo(cwd);
1149
+ const projectRef = path
1150
+ .relative(cwd, path.join(planningDir(cwd), 'PROJECT.md'))
1151
+ .split(path.sep).join('/');
1152
+ let stateContent = `# Session State\n\n`;
1153
+ stateContent += `## Project Reference\n\n`;
1154
+ stateContent += `See: ${projectRef}\n\n`;
1155
+ stateContent += `## Position\n\n`;
1156
+ stateContent += `**Milestone:** ${milestone.version} ${milestone.name}\n`;
1157
+ stateContent += `**Current phase:** (determining...)\n`;
1158
+ stateContent += `**Status:** Resuming\n\n`;
1159
+ stateContent += `## Session Log\n\n`;
1160
+ stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by ${slash('health')} --repair\n`;
1161
+ writeStateMd(statePath, stateContent, cwd);
1162
+ repairActions.push({ action: repair, success: true, path: 'STATE.md' });
1163
+ break;
1164
+ }
1165
+ case 'addNyquistKey': {
1166
+ if (fs.existsSync(configPath)) {
1167
+ try {
1168
+ const configRaw = fs.readFileSync(configPath, 'utf-8');
1169
+ const configParsed = JSON.parse(configRaw);
1170
+ if (!configParsed.workflow) configParsed.workflow = {};
1171
+ if (configParsed.workflow.nyquist_validation === undefined) {
1172
+ configParsed.workflow.nyquist_validation = true;
1173
+ platformWriteSync(configPath, JSON.stringify(configParsed, null, 2));
1174
+ }
1175
+ repairActions.push({ action: repair, success: true, path: 'config.json' });
1176
+ } catch (err) {
1177
+ repairActions.push({ action: repair, success: false, error: err.message });
1178
+ }
1179
+ }
1180
+ break;
1181
+ }
1182
+ case 'addAiIntegrationPhaseKey': {
1183
+ if (fs.existsSync(configPath)) {
1184
+ try {
1185
+ const configRaw = fs.readFileSync(configPath, 'utf-8');
1186
+ const configParsed = JSON.parse(configRaw);
1187
+ if (!configParsed.workflow) configParsed.workflow = {};
1188
+ if (configParsed.workflow.ai_integration_phase === undefined) {
1189
+ configParsed.workflow.ai_integration_phase = true;
1190
+ platformWriteSync(configPath, JSON.stringify(configParsed, null, 2));
1191
+ }
1192
+ repairActions.push({ action: repair, success: true, path: 'config.json' });
1193
+ } catch (err) {
1194
+ repairActions.push({ action: repair, success: false, error: err.message });
1195
+ }
1196
+ }
1197
+ break;
1198
+ }
1199
+ case 'backfillMilestones': {
1200
+ if (!options.backfill && !options.repair) break;
1201
+ const today = new Date().toISOString().split('T')[0];
1202
+ let backfilled = 0;
1203
+ for (const ver of missingFromRegistry) {
1204
+ try {
1205
+ const snapshotPath = path.join(milestonesArchiveDir, `${ver}-ROADMAP.md`);
1206
+ const snapshot = safeReadFile(snapshotPath);
1207
+ // Build minimal entry from snapshot title or version
1208
+ const titleMatch = snapshot && snapshot.match(/^#\s+(.+)$/m);
1209
+ const milestoneName = titleMatch ? titleMatch[1].replace(/^Milestone\s+/i, '').replace(/^v[\d.]+\s*/, '').trim() : ver;
1210
+ const entry = `## ${ver}${milestoneName && milestoneName !== ver ? ` ${milestoneName}` : ''} (Backfilled: ${today})\n\n**Note:** Synthesized from archive snapshot by \`${slash('health')} --backfill\`. Original completion date unknown.\n\n---\n\n`;
1211
+ const milestonesContent = fs.existsSync(milestonesPath)
1212
+ ? fs.readFileSync(milestonesPath, 'utf-8')
1213
+ : '';
1214
+ if (!milestonesContent.trim()) {
1215
+ platformWriteSync(milestonesPath, `# Milestones\n\n${entry}`);
1216
+ } else {
1217
+ const headerMatch = milestonesContent.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
1218
+ if (headerMatch) {
1219
+ const header = headerMatch[1];
1220
+ const rest = milestonesContent.slice(header.length);
1221
+ platformWriteSync(milestonesPath, header + entry + rest);
1222
+ } else {
1223
+ platformWriteSync(milestonesPath, entry + milestonesContent);
1224
+ }
1225
+ }
1226
+ backfilled++;
1227
+ } catch { /* intentionally empty — partial backfill is acceptable */ }
1228
+ }
1229
+ repairActions.push({ action: repair, success: true, detail: `Backfilled ${backfilled} milestone(s) into MILESTONES.md` });
1230
+ break;
1231
+ }
1232
+ }
1233
+ } catch (err) {
1234
+ repairActions.push({ action: repair, success: false, error: err.message });
1235
+ }
1236
+ }
1237
+ }
1238
+
1239
+ // ─── Determine overall status ─────────────────────────────────────────────
1240
+ let status;
1241
+ if (errors.length > 0) {
1242
+ status = 'broken';
1243
+ } else if (warnings.length > 0) {
1244
+ status = 'degraded';
1245
+ } else {
1246
+ status = 'healthy';
1247
+ }
1248
+
1249
+ const repairableCount = errors.filter(e => e.repairable).length +
1250
+ warnings.filter(w => w.repairable).length;
1251
+
1252
+ const result = {
1253
+ status,
1254
+ errors,
1255
+ warnings,
1256
+ info,
1257
+ repairable_count: repairableCount,
1258
+ repairs_performed: repairActions.length > 0 ? repairActions : undefined,
1259
+ };
1260
+ output(result, raw);
1261
+ return result;
1262
+ }
1263
+
1264
+ /**
1265
+ * Validate agent installation status (#1371).
1266
+ * Returns detailed information about which agents are installed and which are missing.
1267
+ */
1268
+ function cmdValidateAgents(cwd, raw) {
1269
+ const { MODEL_PROFILES } = require('./model-profiles.cjs');
1270
+ const agentStatus = checkAgentsInstalled();
1271
+ const expected = Object.keys(MODEL_PROFILES);
1272
+
1273
+ output({
1274
+ agents_dir: agentStatus.agents_dir,
1275
+ agents_found: agentStatus.agents_installed,
1276
+ installed: agentStatus.installed_agents,
1277
+ missing: agentStatus.missing_agents,
1278
+ expected,
1279
+ }, raw);
1280
+ }
1281
+
1282
+ // ─── Schema Drift Detection ──────────────────────────────────────────────────
1283
+
1284
+ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
1285
+ const { detectSchemaFiles, checkSchemaDrift } = require('./schema-detect.cjs');
1286
+
1287
+ if (!phaseArg) {
1288
+ error('Usage: verify schema-drift <phase> [--skip]');
1289
+ return;
1290
+ }
1291
+
1292
+ // Find phase directory
1293
+ const pDir = planningDir(cwd);
1294
+ const phasesDir = path.join(pDir, 'phases');
1295
+ if (!fs.existsSync(phasesDir)) {
1296
+ output({ drift_detected: false, blocking: false, message: 'No phases directory' }, raw);
1297
+ return;
1298
+ }
1299
+
1300
+ // Find matching phase directory
1301
+ let phaseDir = null;
1302
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
1303
+ for (const entry of entries) {
1304
+ if (entry.isDirectory() && entry.name.includes(phaseArg)) {
1305
+ phaseDir = path.join(phasesDir, entry.name);
1306
+ break;
1307
+ }
1308
+ }
1309
+
1310
+ // Also try exact match
1311
+ if (!phaseDir) {
1312
+ const exact = path.join(phasesDir, phaseArg);
1313
+ if (fs.existsSync(exact)) phaseDir = exact;
1314
+ }
1315
+
1316
+ if (!phaseDir) {
1317
+ output({ drift_detected: false, blocking: false, message: `Phase directory not found: ${phaseArg}` }, raw);
1318
+ return;
1319
+ }
1320
+
1321
+ // Collect files_modified from all PLAN.md files in the phase
1322
+ const allFiles = [];
1323
+ const planFiles = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
1324
+ for (const pf of planFiles) {
1325
+ const content = fs.readFileSync(path.join(phaseDir, pf), 'utf-8');
1326
+ // Extract files_modified from frontmatter
1327
+ const fmMatch = content.match(/files_modified:\s*\[([^\]]*)\]/);
1328
+ if (fmMatch) {
1329
+ const files = fmMatch[1].split(',').map(f => f.trim()).filter(Boolean);
1330
+ allFiles.push(...files);
1331
+ }
1332
+ }
1333
+
1334
+ // Collect execution log from SUMMARY.md files
1335
+ let executionLog = '';
1336
+ const summaryFiles = fs.readdirSync(phaseDir).filter(f => f.endsWith('-SUMMARY.md'));
1337
+ for (const sf of summaryFiles) {
1338
+ executionLog += fs.readFileSync(path.join(phaseDir, sf), 'utf-8') + '\n';
1339
+ }
1340
+
1341
+ // Also check git commit messages for push evidence
1342
+ const gitLog = execGit(['log', '--oneline', '--all', '-50'], { cwd });
1343
+ if (gitLog.exitCode === 0) {
1344
+ executionLog += '\n' + gitLog.stdout;
1345
+ }
1346
+
1347
+ const result = checkSchemaDrift(allFiles, executionLog, { skipCheck: !!skipFlag });
1348
+
1349
+ output({
1350
+ drift_detected: result.driftDetected,
1351
+ blocking: result.blocking,
1352
+ schema_files: result.schemaFiles,
1353
+ orms: result.orms,
1354
+ unpushed_orms: result.unpushedOrms,
1355
+ message: result.message,
1356
+ skipped: result.skipped || false,
1357
+ }, raw);
1358
+ }
1359
+
1360
+ // ─── Codebase Drift Detection (#2003) ────────────────────────────────────────
1361
+
1362
+ /**
1363
+ * Detect structural drift between the committed tree and
1364
+ * `.planning/codebase/STRUCTURE.md`. Non-blocking: any failure returns a
1365
+ * `{ skipped: true }` JSON result with a reason; the command never exits
1366
+ * non-zero so `execute-phase`'s drift gate cannot fail the phase.
1367
+ */
1368
+ function cmdVerifyCodebaseDrift(cwd, raw) {
1369
+ const drift = require('./drift.cjs');
1370
+
1371
+ const emit = (payload) => output(payload, raw);
1372
+
1373
+ try {
1374
+ const codebaseDir = path.join(planningDir(cwd), 'codebase');
1375
+ const structurePath = path.join(codebaseDir, 'STRUCTURE.md');
1376
+ if (!fs.existsSync(structurePath)) {
1377
+ emit({
1378
+ skipped: true,
1379
+ reason: 'no-structure-md',
1380
+ action_required: false,
1381
+ directive: 'none',
1382
+ elements: [],
1383
+ });
1384
+ return;
1385
+ }
1386
+
1387
+ let structureMd;
1388
+ try {
1389
+ structureMd = fs.readFileSync(structurePath, 'utf-8');
1390
+ } catch (err) {
1391
+ emit({
1392
+ skipped: true,
1393
+ reason: 'cannot-read-structure-md: ' + err.message,
1394
+ action_required: false,
1395
+ directive: 'none',
1396
+ elements: [],
1397
+ });
1398
+ return;
1399
+ }
1400
+
1401
+ const lastMapped = drift.readMappedCommit(structurePath);
1402
+
1403
+ // Verify we're inside a git repo and resolve the diff range.
1404
+ const revProbe = execGit(['rev-parse', 'HEAD'], { cwd });
1405
+ if (revProbe.exitCode !== 0) {
1406
+ emit({
1407
+ skipped: true,
1408
+ reason: 'not-a-git-repo',
1409
+ action_required: false,
1410
+ directive: 'none',
1411
+ elements: [],
1412
+ });
1413
+ return;
1414
+ }
1415
+
1416
+ // Empty-tree SHA is a stable fallback when no mapping commit is recorded.
1417
+ const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
1418
+ let base = lastMapped;
1419
+ if (!base) {
1420
+ base = EMPTY_TREE;
1421
+ } else {
1422
+ // Verify the commit is reachable; if not, fall back to EMPTY_TREE.
1423
+ const verify = execGit(['cat-file', '-t', base], { cwd });
1424
+ if (verify.exitCode !== 0) base = EMPTY_TREE;
1425
+ }
1426
+
1427
+ const diff = execGit(['diff', '--name-status', base, 'HEAD'], { cwd });
1428
+ if (diff.exitCode !== 0) {
1429
+ emit({
1430
+ skipped: true,
1431
+ reason: 'git-diff-failed',
1432
+ action_required: false,
1433
+ directive: 'none',
1434
+ elements: [],
1435
+ });
1436
+ return;
1437
+ }
1438
+
1439
+ const added = [];
1440
+ const modified = [];
1441
+ const deleted = [];
1442
+ for (const line of diff.stdout.split(/\r?\n/)) {
1443
+ if (!line.trim()) continue;
1444
+ const m = line.match(/^([A-Z])\d*\t(.+?)(?:\t(.+))?$/);
1445
+ if (!m) continue;
1446
+ const status = m[1];
1447
+ // For renames (R), use the new path (m[3] if present, else m[2]).
1448
+ const file = m[3] || m[2];
1449
+ if (status === 'A' || status === 'R' || status === 'C') added.push(file);
1450
+ else if (status === 'M') modified.push(file);
1451
+ else if (status === 'D') deleted.push(file);
1452
+ }
1453
+
1454
+ // Threshold and action read from config, with defaults.
1455
+ const config = loadConfig(cwd);
1456
+ const threshold = Number.isInteger(config?.workflow?.drift_threshold) && config.workflow.drift_threshold >= 1
1457
+ ? config.workflow.drift_threshold
1458
+ : 3;
1459
+ const action = config?.workflow?.drift_action === 'auto-remap' ? 'auto-remap' : 'warn';
1460
+
1461
+ const result = drift.detectDrift({
1462
+ addedFiles: added,
1463
+ modifiedFiles: modified,
1464
+ deletedFiles: deleted,
1465
+ structureMd,
1466
+ threshold,
1467
+ action,
1468
+ // #3584: keep drift.cjs a pure library — resolve the runtime here and
1469
+ // pass the literal name in so drift never touches env/config itself.
1470
+ runtime: resolveRuntime(cwd),
1471
+ });
1472
+
1473
+ emit({
1474
+ skipped: !!result.skipped,
1475
+ reason: result.reason || null,
1476
+ action_required: !!result.actionRequired,
1477
+ directive: result.directive,
1478
+ spawn_mapper: !!result.spawnMapper,
1479
+ affected_paths: result.affectedPaths || [],
1480
+ elements: result.elements || [],
1481
+ threshold,
1482
+ action,
1483
+ last_mapped_commit: lastMapped,
1484
+ message: result.message || '',
1485
+ });
1486
+ } catch (err) {
1487
+ // Non-blocking: never bubble up an exception.
1488
+ emit({
1489
+ skipped: true,
1490
+ reason: 'exception: ' + (err && err.message ? err.message : String(err)),
1491
+ action_required: false,
1492
+ directive: 'none',
1493
+ elements: [],
1494
+ });
1495
+ }
1496
+ }
1497
+
1498
+ module.exports = {
1499
+ cmdVerifySummary,
1500
+ cmdVerifyPlanStructure,
1501
+ cmdVerifyPhaseCompleteness,
1502
+ cmdVerifyReferences,
1503
+ cmdVerifyCommits,
1504
+ cmdVerifyArtifacts,
1505
+ cmdVerifyKeyLinks,
1506
+ cmdValidateConsistency,
1507
+ cmdValidateHealth,
1508
+ cmdValidateAgents,
1509
+ cmdVerifySchemaDrift,
1510
+ cmdVerifyCodebaseDrift,
1511
+ };