@opengsd/gsd-core 1.3.0-rc.1 → 1.3.0

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