@howlil/ez-agents 3.5.0 → 4.0.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 (382) hide show
  1. package/README.md +735 -537
  2. package/agents/ez-architect-agent.md +267 -0
  3. package/agents/ez-backend-agent.md +303 -0
  4. package/agents/ez-chief-strategist.md +271 -0
  5. package/agents/ez-codebase-mapper.md +770 -770
  6. package/agents/ez-context-manager.md +319 -0
  7. package/agents/ez-debugger.md +1255 -1255
  8. package/agents/ez-design-expert.md +347 -0
  9. package/agents/ez-devops-agent.md +331 -0
  10. package/agents/ez-executor.md +487 -487
  11. package/agents/ez-frontend-agent.md +322 -0
  12. package/agents/ez-phase-researcher.md +553 -553
  13. package/agents/ez-planner.md +1307 -1307
  14. package/agents/ez-product-engineer.md +435 -0
  15. package/agents/ez-project-researcher.md +629 -629
  16. package/agents/ez-qa-agent.md +320 -0
  17. package/agents/ez-release-agent.md +333 -333
  18. package/agents/ez-requirements-agent.md +377 -377
  19. package/agents/ez-roadmapper.md +650 -650
  20. package/agents/ez-technical-writer.md +551 -0
  21. package/agents/ez-ux-expert.md +393 -0
  22. package/agents/ez-verifier.md +579 -579
  23. package/bin/guards/autonomy-guard.cjs +346 -0
  24. package/bin/guards/context-budget-guard.cjs +278 -0
  25. package/bin/guards/hallucination-guard.cjs +380 -0
  26. package/bin/guards/hidden-state-guard.cjs +182 -0
  27. package/bin/guards/team-overhead-guard.cjs +266 -0
  28. package/bin/guards/tool-sprawl-guard.cjs +271 -0
  29. package/bin/lib/analytics/analytics-collector.cjs +86 -0
  30. package/bin/lib/analytics/analytics-reporter.cjs +130 -0
  31. package/bin/lib/analytics/cohort-analyzer.cjs +138 -0
  32. package/bin/lib/analytics/funnel-analyzer.cjs +147 -0
  33. package/bin/lib/analytics/nps-tracker.cjs +147 -0
  34. package/bin/lib/archetype-detector.cjs +289 -0
  35. package/bin/lib/assistant-adapter.cjs +361 -0
  36. package/bin/lib/audit-exec.cjs +175 -0
  37. package/bin/lib/auth.cjs +176 -0
  38. package/bin/lib/backup-service.cjs +422 -0
  39. package/bin/lib/bdd-validator.cjs +622 -0
  40. package/bin/lib/business-flow-mapper.cjs +429 -0
  41. package/bin/lib/circuit-breaker.cjs +276 -0
  42. package/bin/lib/code-complexity-analyzer.cjs +360 -0
  43. package/bin/lib/codebase-analyzer.cjs +241 -0
  44. package/bin/lib/commands.cjs +691 -0
  45. package/bin/lib/config.cjs +236 -0
  46. package/bin/lib/constraint-extractor.cjs +526 -0
  47. package/bin/lib/content-scanner.cjs +238 -0
  48. package/bin/lib/context-cache.cjs +154 -0
  49. package/bin/lib/context-compressor.cjs +102 -0
  50. package/bin/lib/context-deduplicator.cjs +105 -0
  51. package/bin/lib/context-errors.cjs +78 -0
  52. package/bin/lib/context-manager.cjs +338 -0
  53. package/bin/lib/context-metadata-tracker.cjs +140 -0
  54. package/bin/lib/context-relevance-scorer.cjs +99 -0
  55. package/bin/lib/core.cjs +507 -0
  56. package/bin/lib/cost-alerts.cjs +174 -0
  57. package/bin/lib/cost-tracker.cjs +275 -0
  58. package/bin/lib/crash-recovery.cjs +220 -0
  59. package/bin/lib/dependency-graph.cjs +319 -0
  60. package/bin/lib/deploy/deploy-audit-log.cjs +76 -0
  61. package/bin/lib/deploy/deploy-detector.cjs +69 -0
  62. package/bin/lib/deploy/deploy-env-manager.cjs +109 -0
  63. package/bin/lib/deploy/deploy-health-check.cjs +88 -0
  64. package/bin/lib/deploy/deploy-pre-flight.cjs +57 -0
  65. package/bin/lib/deploy/deploy-rollback.cjs +72 -0
  66. package/bin/lib/deploy/deploy-runner.cjs +97 -0
  67. package/bin/lib/deploy/deploy-status.cjs +74 -0
  68. package/bin/lib/discussion-synthesizer.cjs +439 -0
  69. package/bin/lib/error-cache.cjs +114 -0
  70. package/bin/lib/error-registry.cjs +177 -0
  71. package/bin/lib/file-access.cjs +207 -0
  72. package/bin/lib/file-lock.cjs +236 -0
  73. package/bin/lib/finops/budget-enforcer.cjs +126 -0
  74. package/bin/lib/finops/cost-reporter.cjs +132 -0
  75. package/bin/lib/finops/finops-analyzer.cjs +112 -0
  76. package/bin/lib/finops/spot-manager.cjs +118 -0
  77. package/bin/lib/framework-detector.cjs +396 -0
  78. package/bin/lib/frontmatter.cjs +313 -0
  79. package/bin/lib/fs-utils.cjs +153 -0
  80. package/bin/lib/gate-executor.cjs +272 -0
  81. package/bin/lib/gates/README.md +374 -0
  82. package/bin/lib/gates/gate-01-requirement.cjs +303 -0
  83. package/bin/lib/gates/gate-02-architecture.cjs +555 -0
  84. package/bin/lib/gates/gate-03-code.cjs +635 -0
  85. package/bin/lib/gates/gate-04-security.cjs +829 -0
  86. package/bin/lib/git-errors.cjs +83 -0
  87. package/bin/lib/git-utils.cjs +321 -0
  88. package/bin/lib/git-workflow-engine.cjs +1157 -0
  89. package/bin/lib/health-check.cjs +227 -0
  90. package/bin/lib/index.cjs +279 -0
  91. package/bin/lib/init.cjs +725 -0
  92. package/bin/lib/lock-logger.cjs +194 -0
  93. package/bin/lib/lock-state.cjs +263 -0
  94. package/bin/lib/lockfile-validator.cjs +227 -0
  95. package/bin/lib/log-rotation.cjs +71 -0
  96. package/bin/lib/logger.cjs +125 -0
  97. package/bin/lib/memory-compression.cjs +256 -0
  98. package/bin/lib/milestone.cjs +247 -0
  99. package/bin/lib/model-provider.cjs +241 -0
  100. package/bin/lib/package-manager-detector.cjs +203 -0
  101. package/bin/lib/package-manager-executor.cjs +385 -0
  102. package/bin/lib/package-manager-service.cjs +216 -0
  103. package/bin/lib/perf/api-monitor.cjs +88 -0
  104. package/bin/lib/perf/db-optimizer.cjs +78 -0
  105. package/bin/lib/perf/frontend-performance.cjs +56 -0
  106. package/bin/lib/perf/perf-analyzer.cjs +77 -0
  107. package/bin/lib/perf/perf-baseline.cjs +102 -0
  108. package/bin/lib/perf/perf-reporter.cjs +117 -0
  109. package/bin/lib/perf/regression-detector.cjs +92 -0
  110. package/bin/lib/phase.cjs +963 -0
  111. package/bin/lib/planning-write.cjs +123 -0
  112. package/bin/lib/project-reporter.cjs +565 -0
  113. package/bin/lib/quality-gate.cjs +332 -0
  114. package/bin/lib/quality-metrics.cjs +324 -0
  115. package/bin/lib/recovery-manager.cjs +98 -0
  116. package/bin/lib/release-validator.cjs +617 -0
  117. package/bin/lib/retry.cjs +119 -0
  118. package/bin/lib/roadmap.cjs +309 -0
  119. package/bin/lib/safe-exec.cjs +173 -0
  120. package/bin/lib/safe-path.cjs +130 -0
  121. package/bin/lib/security-errors.cjs +62 -0
  122. package/bin/lib/session-chain.cjs +304 -0
  123. package/bin/lib/session-errors.cjs +81 -0
  124. package/bin/lib/session-export.cjs +251 -0
  125. package/bin/lib/session-import.cjs +262 -0
  126. package/bin/lib/session-manager.cjs +280 -0
  127. package/bin/lib/skill-context.cjs +148 -0
  128. package/bin/lib/skill-matcher.cjs +236 -0
  129. package/bin/lib/skill-registry.cjs +360 -0
  130. package/bin/lib/skill-resolver.cjs +449 -0
  131. package/bin/lib/skill-triggers.cjs +90 -0
  132. package/bin/lib/skill-validator.cjs +270 -0
  133. package/bin/lib/skill-versioning.cjs +355 -0
  134. package/bin/lib/stack-detector.cjs +399 -0
  135. package/bin/lib/state.cjs +736 -0
  136. package/bin/lib/tech-debt-analyzer.cjs +309 -0
  137. package/bin/lib/temp-file.cjs +239 -0
  138. package/bin/lib/template.cjs +223 -0
  139. package/bin/lib/test-file-lock.cjs +112 -0
  140. package/bin/lib/test-graceful.cjs +93 -0
  141. package/bin/lib/test-logger.cjs +60 -0
  142. package/bin/lib/test-safe-exec.cjs +38 -0
  143. package/bin/lib/test-safe-path.cjs +33 -0
  144. package/bin/lib/test-temp-file.cjs +125 -0
  145. package/bin/lib/tier-manager.cjs +428 -0
  146. package/bin/lib/timeout-exec.cjs +63 -0
  147. package/bin/lib/tradeoff-analyzer.cjs +284 -0
  148. package/bin/lib/url-fetch.cjs +170 -0
  149. package/bin/lib/verify.cjs +863 -0
  150. package/bin/update.js +217 -214
  151. package/commands/deploy.cjs +53 -0
  152. package/commands/ez/add-tests.md +41 -41
  153. package/commands/ez/audit-milestone.md +36 -36
  154. package/commands/ez/complete-milestone.md +136 -136
  155. package/commands/ez/discuss-phase.md +90 -90
  156. package/commands/ez/execute-phase.md +52 -52
  157. package/commands/ez/help.md +22 -22
  158. package/commands/ez/map-codebase.md +71 -71
  159. package/commands/ez/new-milestone.md +44 -44
  160. package/commands/ez/new-project.md +51 -42
  161. package/commands/ez/plan-phase.md +53 -53
  162. package/commands/ez/progress.md +36 -36
  163. package/commands/ez/quick.md +45 -45
  164. package/commands/ez/resume-work.md +40 -40
  165. package/commands/ez/run-phase.md +580 -0
  166. package/commands/ez/settings.md +36 -36
  167. package/commands/ez/update.md +37 -37
  168. package/commands/ez/verify-work.md +402 -38
  169. package/commands/health-check.cjs +44 -0
  170. package/commands/rollback.cjs +47 -0
  171. package/ez-agents/bin/ez-tools.cjs +599 -2
  172. package/ez-agents/bin/guards/autonomy-guard.cjs +346 -0
  173. package/ez-agents/bin/guards/context-budget-guard.cjs +247 -0
  174. package/ez-agents/bin/guards/hallucination-guard.cjs +271 -0
  175. package/ez-agents/bin/guards/hidden-state-guard.cjs +182 -0
  176. package/ez-agents/bin/guards/team-overhead-guard.cjs +266 -0
  177. package/ez-agents/bin/guards/tool-sprawl-guard.cjs +271 -0
  178. package/ez-agents/bin/lib/analytics/analytics-collector.cjs +86 -0
  179. package/ez-agents/bin/lib/analytics/analytics-reporter.cjs +130 -0
  180. package/ez-agents/bin/lib/analytics/cohort-analyzer.cjs +138 -0
  181. package/ez-agents/bin/lib/analytics/funnel-analyzer.cjs +147 -0
  182. package/ez-agents/bin/lib/analytics/nps-tracker.cjs +147 -0
  183. package/ez-agents/bin/lib/archetype-detector.cjs +289 -0
  184. package/ez-agents/bin/lib/audit-exec.cjs +166 -167
  185. package/ez-agents/bin/lib/auth.cjs +176 -176
  186. package/ez-agents/bin/lib/backup-service.cjs +422 -0
  187. package/ez-agents/bin/lib/bdd-validator.cjs +622 -622
  188. package/ez-agents/bin/lib/business-flow-mapper.cjs +429 -0
  189. package/ez-agents/bin/lib/code-complexity-analyzer.cjs +360 -0
  190. package/ez-agents/bin/lib/codebase-analyzer.cjs +241 -0
  191. package/ez-agents/bin/lib/commands.cjs +685 -685
  192. package/ez-agents/bin/lib/config.cjs +41 -1
  193. package/ez-agents/bin/lib/constraint-extractor.cjs +526 -0
  194. package/ez-agents/bin/lib/content-scanner.cjs +238 -238
  195. package/ez-agents/bin/lib/context-cache.cjs +154 -154
  196. package/ez-agents/bin/lib/context-errors.cjs +71 -71
  197. package/ez-agents/bin/lib/context-manager.cjs +220 -220
  198. package/ez-agents/bin/lib/core.cjs +507 -512
  199. package/ez-agents/bin/lib/cost-tracker.cjs +243 -0
  200. package/ez-agents/bin/lib/crash-recovery.cjs +172 -0
  201. package/ez-agents/bin/lib/dependency-graph.cjs +319 -0
  202. package/ez-agents/bin/lib/deploy/deploy-audit-log.cjs +76 -0
  203. package/ez-agents/bin/lib/deploy/deploy-detector.cjs +69 -0
  204. package/ez-agents/bin/lib/deploy/deploy-env-manager.cjs +109 -0
  205. package/ez-agents/bin/lib/deploy/deploy-health-check.cjs +88 -0
  206. package/ez-agents/bin/lib/deploy/deploy-pre-flight.cjs +57 -0
  207. package/ez-agents/bin/lib/deploy/deploy-rollback.cjs +72 -0
  208. package/ez-agents/bin/lib/deploy/deploy-runner.cjs +97 -0
  209. package/ez-agents/bin/lib/deploy/deploy-status.cjs +74 -0
  210. package/ez-agents/bin/lib/file-access.cjs +207 -207
  211. package/ez-agents/bin/lib/finops/budget-enforcer.cjs +126 -0
  212. package/ez-agents/bin/lib/finops/cost-reporter.cjs +132 -0
  213. package/ez-agents/bin/lib/finops/finops-analyzer.cjs +112 -0
  214. package/ez-agents/bin/lib/finops/spot-manager.cjs +118 -0
  215. package/ez-agents/bin/lib/framework-detector.cjs +396 -0
  216. package/ez-agents/bin/lib/frontmatter.cjs +3 -1
  217. package/ez-agents/bin/lib/gates/README.md +374 -0
  218. package/ez-agents/bin/lib/gates/gate-01-requirement.cjs +303 -0
  219. package/ez-agents/bin/lib/gates/gate-02-architecture.cjs +555 -0
  220. package/ez-agents/bin/lib/gates/gate-03-code.cjs +635 -0
  221. package/ez-agents/bin/lib/gates/gate-04-security.cjs +829 -0
  222. package/ez-agents/bin/lib/git-errors.cjs +83 -83
  223. package/ez-agents/bin/lib/git-utils.cjs +321 -321
  224. package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -1157
  225. package/ez-agents/bin/lib/health-check.cjs +162 -162
  226. package/ez-agents/bin/lib/index.cjs +2 -8
  227. package/ez-agents/bin/lib/init.cjs +0 -2
  228. package/ez-agents/bin/lib/lockfile-validator.cjs +227 -227
  229. package/ez-agents/bin/lib/log-rotation.cjs +71 -0
  230. package/ez-agents/bin/lib/logger.cjs +22 -47
  231. package/ez-agents/bin/lib/memory-compression.cjs +256 -256
  232. package/ez-agents/bin/lib/package-manager-detector.cjs +203 -203
  233. package/ez-agents/bin/lib/package-manager-executor.cjs +385 -385
  234. package/ez-agents/bin/lib/package-manager-service.cjs +216 -216
  235. package/ez-agents/bin/lib/perf/api-monitor.cjs +88 -0
  236. package/ez-agents/bin/lib/perf/db-optimizer.cjs +78 -0
  237. package/ez-agents/bin/lib/perf/frontend-performance.cjs +56 -0
  238. package/ez-agents/bin/lib/perf/perf-analyzer.cjs +77 -0
  239. package/ez-agents/bin/lib/perf/perf-baseline.cjs +102 -0
  240. package/ez-agents/bin/lib/perf/perf-reporter.cjs +117 -0
  241. package/ez-agents/bin/lib/perf/regression-detector.cjs +92 -0
  242. package/ez-agents/bin/lib/project-reporter.cjs +502 -0
  243. package/ez-agents/bin/lib/quality-gate.cjs +332 -0
  244. package/ez-agents/bin/lib/recovery-manager.cjs +98 -0
  245. package/ez-agents/bin/lib/release-validator.cjs +617 -614
  246. package/ez-agents/bin/lib/security-errors.cjs +62 -0
  247. package/ez-agents/bin/lib/session-chain.cjs +304 -304
  248. package/ez-agents/bin/lib/session-errors.cjs +81 -81
  249. package/ez-agents/bin/lib/session-export.cjs +251 -251
  250. package/ez-agents/bin/lib/session-import.cjs +262 -262
  251. package/ez-agents/bin/lib/session-manager.cjs +280 -280
  252. package/ez-agents/bin/lib/skill-context.cjs +148 -0
  253. package/ez-agents/bin/lib/skill-matcher.cjs +236 -0
  254. package/ez-agents/bin/lib/skill-registry.cjs +341 -0
  255. package/ez-agents/bin/lib/skill-resolver.cjs +449 -0
  256. package/ez-agents/bin/lib/skill-triggers.cjs +90 -0
  257. package/ez-agents/bin/lib/skill-validator.cjs +270 -0
  258. package/ez-agents/bin/lib/skill-versioning.cjs +355 -0
  259. package/ez-agents/bin/lib/stack-detector.cjs +399 -0
  260. package/ez-agents/bin/lib/tech-debt-analyzer.cjs +309 -0
  261. package/ez-agents/bin/lib/tier-manager.cjs +428 -428
  262. package/ez-agents/bin/lib/tradeoff-analyzer.cjs +284 -0
  263. package/ez-agents/bin/lib/url-fetch.cjs +170 -170
  264. package/ez-agents/bin/lib/verify.cjs +863 -863
  265. package/ez-agents/references/decimal-phase-calculation.md +65 -65
  266. package/ez-agents/references/git-integration.md +248 -248
  267. package/ez-agents/references/git-planning-commit.md +38 -38
  268. package/ez-agents/references/metrics-schema.md +118 -118
  269. package/ez-agents/references/model-profile-resolution.md +34 -34
  270. package/ez-agents/references/model-profiles.md +93 -93
  271. package/ez-agents/references/phase-argument-parsing.md +61 -61
  272. package/ez-agents/references/planning-config.md +340 -340
  273. package/ez-agents/references/tier-strategy.md +103 -103
  274. package/ez-agents/references/ui-brand.md +160 -160
  275. package/ez-agents/references/verification-patterns.md +612 -612
  276. package/ez-agents/templates/DEBUG.md +164 -164
  277. package/ez-agents/templates/UAT.md +247 -247
  278. package/ez-agents/templates/agent-output-format.md +404 -0
  279. package/ez-agents/templates/bdd-feature.md +173 -173
  280. package/ez-agents/templates/codebase/architecture.md +255 -255
  281. package/ez-agents/templates/codebase/structure.md +285 -285
  282. package/ez-agents/templates/copilot-instructions.md +7 -7
  283. package/ez-agents/templates/debug-subagent-prompt.md +91 -91
  284. package/ez-agents/templates/discovery.md +146 -146
  285. package/ez-agents/templates/discussion.md +68 -68
  286. package/ez-agents/templates/handoff-protocol.md +294 -0
  287. package/ez-agents/templates/incident-runbook.md +205 -205
  288. package/ez-agents/templates/mode-workflow-templates.md +301 -0
  289. package/ez-agents/templates/phase-prompt.md +610 -610
  290. package/ez-agents/templates/planner-subagent-prompt.md +117 -117
  291. package/ez-agents/templates/project.md +184 -184
  292. package/ez-agents/templates/release-checklist.md +136 -133
  293. package/ez-agents/templates/research.md +552 -552
  294. package/ez-agents/templates/rollback-plan.md +201 -201
  295. package/ez-agents/templates/security-user-setup.md +244 -0
  296. package/ez-agents/templates/skill-validation-rules.md +476 -0
  297. package/ez-agents/templates/state.md +180 -176
  298. package/ez-agents/templates/summary-complex.md +59 -59
  299. package/ez-agents/tests/gates/gate-01-02.test.cjs +812 -0
  300. package/ez-agents/tests/gates/gate-03-04.test.cjs +762 -0
  301. package/ez-agents/tests/gates/gate-05-validator.test.cjs +145 -0
  302. package/ez-agents/tests/gates/gate-06-docs-validator.test.cjs +244 -0
  303. package/ez-agents/tests/gates/gate-07-release-validator.test.cjs +219 -0
  304. package/ez-agents/tests/guards/context-budget-guard.test.cjs +145 -0
  305. package/ez-agents/tests/guards/edge-case-guards.test.cjs +238 -0
  306. package/ez-agents/tests/guards/hallucination-guard.test.cjs +124 -0
  307. package/ez-agents/workflows/audit-milestone.md +1 -1
  308. package/ez-agents/workflows/autonomous.md +844 -844
  309. package/ez-agents/workflows/complete-milestone.md +1 -1
  310. package/ez-agents/workflows/discuss-phase.md +1 -1
  311. package/ez-agents/workflows/execute-phase.md +124 -3
  312. package/ez-agents/workflows/help.md +42 -181
  313. package/ez-agents/workflows/hotfix.md +291 -291
  314. package/ez-agents/workflows/new-milestone.md +713 -713
  315. package/ez-agents/workflows/new-project.md +1089 -1107
  316. package/ez-agents/workflows/plan-phase.md +0 -40
  317. package/ez-agents/workflows/release.md +253 -253
  318. package/ez-agents/workflows/resume-session.md +215 -215
  319. package/ez-agents/workflows/run-phase.md +531 -0
  320. package/ez-agents/workflows/settings.md +2 -35
  321. package/hooks/dist/ez-check-update.js +81 -81
  322. package/hooks/dist/ez-context-monitor.js +148 -141
  323. package/hooks/dist/ez-statusline.js +115 -115
  324. package/package.json +78 -71
  325. package/scripts/fix-qwen-installation.js +144 -144
  326. package/agents/ez-integration-checker.md +0 -443
  327. package/agents/ez-nyquist-auditor.md +0 -176
  328. package/agents/ez-observer-agent.md +0 -260
  329. package/agents/ez-plan-checker.md +0 -706
  330. package/agents/ez-research-synthesizer.md +0 -247
  331. package/agents/ez-scrum-master-agent.md +0 -242
  332. package/agents/ez-tech-lead-agent.md +0 -267
  333. package/agents/ez-ui-auditor.md +0 -439
  334. package/agents/ez-ui-checker.md +0 -300
  335. package/agents/ez-ui-researcher.md +0 -353
  336. package/commands/ez/add-phase.md +0 -43
  337. package/commands/ez/add-todo.md +0 -47
  338. package/commands/ez/arch-review.md +0 -102
  339. package/commands/ez/auth.md +0 -87
  340. package/commands/ez/autonomous.md +0 -41
  341. package/commands/ez/check-todos.md +0 -45
  342. package/commands/ez/cleanup.md +0 -18
  343. package/commands/ez/debug.md +0 -168
  344. package/commands/ez/export-session.md +0 -79
  345. package/commands/ez/gather-requirements.md +0 -117
  346. package/commands/ez/git-workflow.md +0 -72
  347. package/commands/ez/health.md +0 -22
  348. package/commands/ez/hotfix.md +0 -120
  349. package/commands/ez/import-session.md +0 -82
  350. package/commands/ez/insert-phase.md +0 -32
  351. package/commands/ez/join-discord.md +0 -18
  352. package/commands/ez/list-phase-assumptions.md +0 -46
  353. package/commands/ez/list-sessions.md +0 -96
  354. package/commands/ez/package-manager.md +0 -316
  355. package/commands/ez/pause-work.md +0 -38
  356. package/commands/ez/plan-milestone-gaps.md +0 -34
  357. package/commands/ez/preflight.md +0 -79
  358. package/commands/ez/reapply-patches.md +0 -124
  359. package/commands/ez/release.md +0 -153
  360. package/commands/ez/remove-phase.md +0 -31
  361. package/commands/ez/research-phase.md +0 -190
  362. package/commands/ez/resume.md +0 -107
  363. package/commands/ez/set-profile.md +0 -34
  364. package/commands/ez/standup.md +0 -85
  365. package/commands/ez/stats.md +0 -18
  366. package/commands/ez/ui-phase.md +0 -34
  367. package/commands/ez/ui-review.md +0 -32
  368. package/commands/ez/validate-phase.md +0 -35
  369. package/ez-agents/bin/lib/metrics-tracker.cjs +0 -406
  370. package/ez-agents/templates/UI-SPEC.md +0 -100
  371. package/ez-agents/templates/VALIDATION.md +0 -76
  372. package/ez-agents/templates/context.md +0 -352
  373. package/ez-agents/templates/verification-report.md +0 -322
  374. package/ez-agents/workflows/arch-review.md +0 -54
  375. package/ez-agents/workflows/export-session.md +0 -255
  376. package/ez-agents/workflows/gather-requirements.md +0 -206
  377. package/ez-agents/workflows/import-session.md +0 -303
  378. package/ez-agents/workflows/research-phase.md +0 -74
  379. package/ez-agents/workflows/standup.md +0 -64
  380. package/ez-agents/workflows/ui-phase.md +0 -290
  381. package/ez-agents/workflows/ui-review.md +0 -157
  382. package/ez-agents/workflows/validate-phase.md +0 -167
@@ -1,863 +1,863 @@
1
- /**
2
- * Verify — Verification suite, consistency, and health validation
3
- */
4
-
5
- const fs = require('fs');
6
- const path = require('path');
7
- const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, output, error } = require('./core.cjs');
8
- const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
9
- const { writeStateMd } = require('./state.cjs');
10
- const { defaultLogger: logger } = require('./logger.cjs');
11
-
12
- async function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
13
- if (!summaryPath) {
14
- error('summary-path required');
15
- }
16
-
17
- const fullPath = path.join(cwd, summaryPath);
18
- const checkCount = checkFileCount || 2;
19
-
20
- // Check 1: Summary exists
21
- if (!fs.existsSync(fullPath)) {
22
- const result = {
23
- passed: false,
24
- checks: {
25
- summary_exists: false,
26
- files_created: { checked: 0, found: 0, missing: [] },
27
- commits_exist: false,
28
- self_check: 'not_found',
29
- },
30
- errors: ['SUMMARY.md not found'],
31
- };
32
- output(result, raw, 'failed');
33
- return;
34
- }
35
-
36
- const content = fs.readFileSync(fullPath, 'utf-8');
37
- const errors = [];
38
-
39
- // Check 2: Spot-check files mentioned in summary
40
- const mentionedFiles = new Set();
41
- const patterns = [
42
- /`([^`]+\.[a-zA-Z]+)`/g,
43
- /(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`]+\.[a-zA-Z]+)`?/gi,
44
- ];
45
-
46
- for (const pattern of patterns) {
47
- let m;
48
- while ((m = pattern.exec(content)) !== null) {
49
- const filePath = m[1];
50
- if (filePath && !filePath.startsWith('http') && filePath.includes('/')) {
51
- mentionedFiles.add(filePath);
52
- }
53
- }
54
- }
55
-
56
- const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
57
- const missing = [];
58
- for (const file of filesToCheck) {
59
- if (!fs.existsSync(path.join(cwd, file))) {
60
- missing.push(file);
61
- }
62
- }
63
-
64
- // Check 3: Commits exist
65
- const commitHashPattern = /\b[0-9a-f]{7,40}\b/g;
66
- const hashes = content.match(commitHashPattern) || [];
67
- let commitsExist = false;
68
- if (hashes.length > 0) {
69
- for (const hash of hashes.slice(0, 3)) {
70
- const result = await execGit(cwd, ['cat-file', '-t', hash]);
71
- if (result.exitCode === 0 && result.stdout === 'commit') {
72
- commitsExist = true;
73
- break;
74
- }
75
- }
76
- }
77
-
78
- // Check 4: Self-check section
79
- let selfCheck = 'not_found';
80
- const selfCheckPattern = /##\s*(?:Self[- ]?Check|Verification|Quality Check)/i;
81
- if (selfCheckPattern.test(content)) {
82
- const passPattern = /(?:all\s+)?(?:pass|✓|✅|complete|succeeded)/i;
83
- const failPattern = /(?:fail|✗|❌|incomplete|blocked)/i;
84
- const checkSection = content.slice(content.search(selfCheckPattern));
85
- if (failPattern.test(checkSection)) {
86
- selfCheck = 'failed';
87
- } else if (passPattern.test(checkSection)) {
88
- selfCheck = 'passed';
89
- }
90
- }
91
-
92
- if (missing.length > 0) errors.push('Missing files: ' + missing.join(', '));
93
- if (!commitsExist && hashes.length > 0) errors.push('Referenced commit hashes not found in git history');
94
- if (selfCheck === 'failed') errors.push('Self-check section indicates failure');
95
-
96
- const checks = {
97
- summary_exists: true,
98
- files_created: { checked: filesToCheck.length, found: filesToCheck.length - missing.length, missing },
99
- commits_exist: commitsExist,
100
- self_check: selfCheck,
101
- };
102
-
103
- const passed = missing.length === 0 && selfCheck !== 'failed';
104
- const result = { passed, checks, errors };
105
- output(result, raw, passed ? 'passed' : 'failed');
106
- }
107
-
108
- function cmdVerifyPlanStructure(cwd, filePath, raw) {
109
- if (!filePath) { error('file path required'); }
110
- const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
111
- const content = safeReadFile(fullPath);
112
- if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
113
-
114
- const fm = extractFrontmatter(content);
115
- const errors = [];
116
- const warnings = [];
117
-
118
- // Check required frontmatter fields
119
- const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
120
- for (const field of required) {
121
- if (fm[field] === undefined) errors.push(`Missing required frontmatter field: ${field}`);
122
- }
123
-
124
- // Parse and check task elements
125
- const taskPattern = /<task[^>]*>([\s\S]*?)<\/task>/g;
126
- const tasks = [];
127
- let taskMatch;
128
- while ((taskMatch = taskPattern.exec(content)) !== null) {
129
- const taskContent = taskMatch[1];
130
- const nameMatch = taskContent.match(/<name>([\s\S]*?)<\/name>/);
131
- const taskName = nameMatch ? nameMatch[1].trim() : 'unnamed';
132
- const hasFiles = /<files>/.test(taskContent);
133
- const hasAction = /<action>/.test(taskContent);
134
- const hasVerify = /<verify>/.test(taskContent);
135
- const hasDone = /<done>/.test(taskContent);
136
-
137
- if (!nameMatch) errors.push('Task missing <name> element');
138
- if (!hasAction) errors.push(`Task '${taskName}' missing <action>`);
139
- if (!hasVerify) warnings.push(`Task '${taskName}' missing <verify>`);
140
- if (!hasDone) warnings.push(`Task '${taskName}' missing <done>`);
141
- if (!hasFiles) warnings.push(`Task '${taskName}' missing <files>`);
142
-
143
- tasks.push({ name: taskName, hasFiles, hasAction, hasVerify, hasDone });
144
- }
145
-
146
- if (tasks.length === 0) warnings.push('No <task> elements found');
147
-
148
- // Wave/depends_on consistency
149
- if (fm.wave && parseInt(fm.wave) > 1 && (!fm.depends_on || (Array.isArray(fm.depends_on) && fm.depends_on.length === 0))) {
150
- warnings.push('Wave > 1 but depends_on is empty');
151
- }
152
-
153
- // Autonomous/checkpoint consistency
154
- const hasCheckpoints = /<task\s+type=["']?checkpoint/.test(content);
155
- if (hasCheckpoints && fm.autonomous !== 'false' && fm.autonomous !== false) {
156
- errors.push('Has checkpoint tasks but autonomous is not false');
157
- }
158
-
159
- output({
160
- valid: errors.length === 0,
161
- errors,
162
- warnings,
163
- task_count: tasks.length,
164
- tasks,
165
- frontmatter_fields: Object.keys(fm),
166
- }, raw, errors.length === 0 ? 'valid' : 'invalid');
167
- }
168
-
169
- function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
170
- if (!phase) { error('phase required'); }
171
- const phaseInfo = findPhaseInternal(cwd, phase);
172
- if (!phaseInfo || !phaseInfo.found) {
173
- output({ error: 'Phase not found', phase }, raw);
174
- return;
175
- }
176
-
177
- const errors = [];
178
- const warnings = [];
179
- const phaseDir = path.join(cwd, phaseInfo.directory);
180
-
181
- // List plans and summaries
182
- let files;
183
- try {
184
- files = fs.readdirSync(phaseDir);
185
- } catch (err) {
186
- logger.warn('Failed to read phase directory in cmdVerifyPhaseCompleteness', { phaseDir, error: err.message });
187
- output({ error: 'Cannot read phase directory' }, raw);
188
- return;
189
- }
190
-
191
- const plans = files.filter(f => f.match(/-PLAN\.md$/i));
192
- const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i));
193
-
194
- // Extract plan IDs (everything before -PLAN.md)
195
- const planIds = new Set(plans.map(p => p.replace(/-PLAN\.md$/i, '')));
196
- const summaryIds = new Set(summaries.map(s => s.replace(/-SUMMARY\.md$/i, '')));
197
-
198
- // Plans without summaries
199
- const incompletePlans = [...planIds].filter(id => !summaryIds.has(id));
200
- if (incompletePlans.length > 0) {
201
- errors.push(`Plans without summaries: ${incompletePlans.join(', ')}`);
202
- }
203
-
204
- // Summaries without plans (orphans)
205
- const orphanSummaries = [...summaryIds].filter(id => !planIds.has(id));
206
- if (orphanSummaries.length > 0) {
207
- warnings.push(`Summaries without plans: ${orphanSummaries.join(', ')}`);
208
- }
209
-
210
- output({
211
- complete: errors.length === 0,
212
- phase: phaseInfo.phase_number,
213
- plan_count: plans.length,
214
- summary_count: summaries.length,
215
- incomplete_plans: incompletePlans,
216
- orphan_summaries: orphanSummaries,
217
- errors,
218
- warnings,
219
- }, raw, errors.length === 0 ? 'complete' : 'incomplete');
220
- }
221
-
222
- function cmdVerifyReferences(cwd, filePath, raw) {
223
- if (!filePath) { error('file path required'); }
224
- const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
225
- const content = safeReadFile(fullPath);
226
- if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
227
-
228
- const found = [];
229
- const missing = [];
230
-
231
- // Find @-references: @path/to/file (must contain / to be a file path)
232
- const atRefs = content.match(/@([^\s\n,)]+\/[^\s\n,)]+)/g) || [];
233
- for (const ref of atRefs) {
234
- const cleanRef = ref.slice(1); // remove @
235
- const resolved = cleanRef.startsWith('~/')
236
- ? path.join(process.env.HOME || '', cleanRef.slice(2))
237
- : path.join(cwd, cleanRef);
238
- if (fs.existsSync(resolved)) {
239
- found.push(cleanRef);
240
- } else {
241
- missing.push(cleanRef);
242
- }
243
- }
244
-
245
- // Find backtick file paths that look like real paths (contain / and have extension)
246
- const backtickRefs = content.match(/`([^`]+\/[^`]+\.[a-zA-Z]{1,10})`/g) || [];
247
- for (const ref of backtickRefs) {
248
- const cleanRef = ref.slice(1, -1); // remove backticks
249
- if (cleanRef.startsWith('http') || cleanRef.includes('${') || cleanRef.includes('{{')) continue;
250
- if (found.includes(cleanRef) || missing.includes(cleanRef)) continue; // dedup
251
- const resolved = path.join(cwd, cleanRef);
252
- if (fs.existsSync(resolved)) {
253
- found.push(cleanRef);
254
- } else {
255
- missing.push(cleanRef);
256
- }
257
- }
258
-
259
- output({
260
- valid: missing.length === 0,
261
- found: found.length,
262
- missing,
263
- total: found.length + missing.length,
264
- }, raw, missing.length === 0 ? 'valid' : 'invalid');
265
- }
266
-
267
- async function cmdVerifyCommits(cwd, hashes, raw) {
268
- if (!hashes || hashes.length === 0) { error('At least one commit hash required'); }
269
-
270
- const valid = [];
271
- const invalid = [];
272
- for (const hash of hashes) {
273
- // Use git cat-file -t which supports both short and full hashes
274
- // First try with the hash as-is (works for both short and full)
275
- const result = await execGit(cwd, ['cat-file', '-t', hash]);
276
- if (result.exitCode === 0 && result.stdout.trim() === 'commit') {
277
- valid.push(hash);
278
- } else {
279
- // If that fails, try to resolve to full hash first
280
- const resolveResult = await execGit(cwd, ['rev-parse', hash]);
281
- if (resolveResult.exitCode === 0) {
282
- const fullHash = resolveResult.stdout.trim();
283
- const result2 = await execGit(cwd, ['cat-file', '-t', fullHash]);
284
- if (result2.exitCode === 0 && result2.stdout.trim() === 'commit') {
285
- valid.push(hash);
286
- } else {
287
- invalid.push(hash);
288
- }
289
- } else {
290
- invalid.push(hash);
291
- }
292
- }
293
- }
294
-
295
- output({
296
- all_valid: invalid.length === 0,
297
- valid,
298
- invalid,
299
- total: hashes.length,
300
- }, raw, invalid.length === 0 ? 'valid' : 'invalid');
301
- }
302
-
303
- function cmdVerifyArtifacts(cwd, planFilePath, raw) {
304
- if (!planFilePath) { error('plan file path required'); }
305
- const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
306
- const content = safeReadFile(fullPath);
307
- if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
308
-
309
- const artifacts = parseMustHavesBlock(content, 'artifacts');
310
- if (artifacts.length === 0) {
311
- output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
312
- return;
313
- }
314
-
315
- const results = [];
316
- for (const artifact of artifacts) {
317
- if (typeof artifact === 'string') continue; // skip simple string items
318
- const artPath = artifact.path;
319
- if (!artPath) continue;
320
-
321
- const artFullPath = path.join(cwd, artPath);
322
- const exists = fs.existsSync(artFullPath);
323
- const check = { path: artPath, exists, issues: [], passed: false };
324
-
325
- if (exists) {
326
- const fileContent = safeReadFile(artFullPath) || '';
327
- const lineCount = fileContent.split('\n').length;
328
-
329
- if (artifact.min_lines && lineCount < artifact.min_lines) {
330
- check.issues.push(`Only ${lineCount} lines, need ${artifact.min_lines}`);
331
- }
332
- if (artifact.contains && !fileContent.includes(artifact.contains)) {
333
- check.issues.push(`Missing pattern: ${artifact.contains}`);
334
- }
335
- if (artifact.exports) {
336
- const exports = Array.isArray(artifact.exports) ? artifact.exports : [artifact.exports];
337
- for (const exp of exports) {
338
- if (!fileContent.includes(exp)) check.issues.push(`Missing export: ${exp}`);
339
- }
340
- }
341
- check.passed = check.issues.length === 0;
342
- } else {
343
- check.issues.push('File not found');
344
- }
345
-
346
- results.push(check);
347
- }
348
-
349
- const passed = results.filter(r => r.passed).length;
350
- output({
351
- all_passed: passed === results.length,
352
- passed,
353
- total: results.length,
354
- artifacts: results,
355
- }, raw, passed === results.length ? 'valid' : 'invalid');
356
- }
357
-
358
- function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
359
- if (!planFilePath) { error('plan file path required'); }
360
- const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
361
- const content = safeReadFile(fullPath);
362
- if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
363
-
364
- const keyLinks = parseMustHavesBlock(content, 'key_links');
365
- if (keyLinks.length === 0) {
366
- output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
367
- return;
368
- }
369
-
370
- const results = [];
371
- for (const link of keyLinks) {
372
- if (typeof link === 'string') continue;
373
- const check = { from: link.from, to: link.to, via: link.via || '', verified: false, detail: '' };
374
-
375
- const sourceContent = safeReadFile(path.join(cwd, link.from || ''));
376
- if (!sourceContent) {
377
- check.detail = 'Source file not found';
378
- } else if (link.pattern) {
379
- try {
380
- const regex = new RegExp(link.pattern);
381
- if (regex.test(sourceContent)) {
382
- check.verified = true;
383
- check.detail = 'Pattern found in source';
384
- } else {
385
- const targetContent = safeReadFile(path.join(cwd, link.to || ''));
386
- if (targetContent && regex.test(targetContent)) {
387
- check.verified = true;
388
- check.detail = 'Pattern found in target';
389
- } else {
390
- check.detail = `Pattern "${link.pattern}" not found in source or target`;
391
- }
392
- }
393
- } catch (err) {
394
- logger.warn('Invalid regex while verifying key links', { pattern: link.pattern, error: err.message });
395
- check.detail = `Invalid regex pattern: ${link.pattern}`;
396
- }
397
- } else {
398
- // No pattern: just check source references target
399
- if (sourceContent.includes(link.to || '')) {
400
- check.verified = true;
401
- check.detail = 'Target referenced in source';
402
- } else {
403
- check.detail = 'Target not referenced in source';
404
- }
405
- }
406
-
407
- results.push(check);
408
- }
409
-
410
- const verified = results.filter(r => r.verified).length;
411
- output({
412
- all_verified: verified === results.length,
413
- verified,
414
- total: results.length,
415
- links: results,
416
- }, raw, verified === results.length ? 'valid' : 'invalid');
417
- }
418
-
419
- function cmdValidateConsistency(cwd, raw) {
420
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
421
- const phasesDir = path.join(cwd, '.planning', 'phases');
422
- const errors = [];
423
- const warnings = [];
424
-
425
- // Check for ROADMAP
426
- if (!fs.existsSync(roadmapPath)) {
427
- errors.push('ROADMAP.md not found');
428
- output({ passed: false, errors, warnings }, raw, 'failed');
429
- return;
430
- }
431
-
432
- const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
433
-
434
- // Extract phases from ROADMAP
435
- const roadmapPhases = new Set();
436
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
437
- let m;
438
- while ((m = phasePattern.exec(roadmapContent)) !== null) {
439
- roadmapPhases.add(m[1]);
440
- }
441
-
442
- // Get phases on disk
443
- const diskPhases = new Set();
444
- try {
445
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
446
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
447
- for (const dir of dirs) {
448
- const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
449
- if (dm) diskPhases.add(dm[1]);
450
- }
451
- } catch (err) {
452
- logger.warn('Failed to enumerate phase directories while validating consistency', { phasesDir, error: err.message });
453
- }
454
-
455
- // Check: phases in ROADMAP but not on disk
456
- for (const p of roadmapPhases) {
457
- if (!diskPhases.has(p) && !diskPhases.has(normalizePhaseName(p))) {
458
- warnings.push(`Phase ${p} in ROADMAP.md but no directory on disk`);
459
- }
460
- }
461
-
462
- // Check: phases on disk but not in ROADMAP
463
- for (const p of diskPhases) {
464
- const unpadded = String(parseInt(p, 10));
465
- if (!roadmapPhases.has(p) && !roadmapPhases.has(unpadded)) {
466
- warnings.push(`Phase ${p} exists on disk but not in ROADMAP.md`);
467
- }
468
- }
469
-
470
- // Check: sequential phase numbers (integers only)
471
- const integerPhases = [...diskPhases]
472
- .filter(p => !p.includes('.'))
473
- .map(p => parseInt(p, 10))
474
- .sort((a, b) => a - b);
475
-
476
- for (let i = 1; i < integerPhases.length; i++) {
477
- if (integerPhases[i] !== integerPhases[i - 1] + 1) {
478
- warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
479
- }
480
- }
481
-
482
- // Check: plan numbering within phases
483
- try {
484
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
485
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
486
-
487
- for (const dir of dirs) {
488
- const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
489
- const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md')).sort();
490
-
491
- // Extract plan numbers
492
- const planNums = plans.map(p => {
493
- const pm = p.match(/-(\d{2})-PLAN\.md$/);
494
- return pm ? parseInt(pm[1], 10) : null;
495
- }).filter(n => n !== null);
496
-
497
- for (let i = 1; i < planNums.length; i++) {
498
- if (planNums[i] !== planNums[i - 1] + 1) {
499
- warnings.push(`Gap in plan numbering in ${dir}: plan ${planNums[i - 1]} → ${planNums[i]}`);
500
- }
501
- }
502
-
503
- // Check: plans without summaries (completed plans)
504
- const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md'));
505
- const planIds = new Set(plans.map(p => p.replace('-PLAN.md', '')));
506
- const summaryIds = new Set(summaries.map(s => s.replace('-SUMMARY.md', '')));
507
-
508
- // Summary without matching plan is suspicious
509
- for (const sid of summaryIds) {
510
- if (!planIds.has(sid)) {
511
- warnings.push(`Summary ${sid}-SUMMARY.md in ${dir} has no matching PLAN.md`);
512
- }
513
- }
514
- }
515
- } catch (err) {
516
- logger.warn('Failed to validate plan numbering while validating consistency', { phasesDir, error: err.message });
517
- }
518
-
519
- // Check: frontmatter in plans has required fields
520
- try {
521
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
522
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
523
-
524
- for (const dir of dirs) {
525
- const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
526
- const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md'));
527
-
528
- for (const plan of plans) {
529
- const content = fs.readFileSync(path.join(phasesDir, dir, plan), 'utf-8');
530
- const fm = extractFrontmatter(content);
531
-
532
- if (!fm.wave) {
533
- warnings.push(`${dir}/${plan}: missing 'wave' in frontmatter`);
534
- }
535
- }
536
- }
537
- } catch (err) {
538
- logger.warn('Failed to validate plan frontmatter while validating consistency', { phasesDir, error: err.message });
539
- }
540
-
541
- const passed = errors.length === 0;
542
- output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
543
- }
544
-
545
- function cmdValidateHealth(cwd, options, raw) {
546
- const planningDir = path.join(cwd, '.planning');
547
- const projectPath = path.join(planningDir, 'PROJECT.md');
548
- const roadmapPath = path.join(planningDir, 'ROADMAP.md');
549
- const statePath = path.join(planningDir, 'STATE.md');
550
- const configPath = path.join(planningDir, 'config.json');
551
- const phasesDir = path.join(planningDir, 'phases');
552
-
553
- const errors = [];
554
- const warnings = [];
555
- const info = [];
556
- const repairs = [];
557
-
558
- // Helper to add issue
559
- const addIssue = (severity, code, message, fix, repairable = false) => {
560
- const issue = { code, message, fix, repairable };
561
- if (severity === 'error') errors.push(issue);
562
- else if (severity === 'warning') warnings.push(issue);
563
- else info.push(issue);
564
- };
565
-
566
- // ─── Check 1: .planning/ exists ───────────────────────────────────────────
567
- if (!fs.existsSync(planningDir)) {
568
- addIssue('error', 'E001', '.planning/ directory not found', 'Run /ez:new-project to initialize');
569
- output({
570
- status: 'broken',
571
- errors,
572
- warnings,
573
- info,
574
- repairable_count: 0,
575
- }, raw);
576
- return;
577
- }
578
-
579
- // ─── Check 2: PROJECT.md exists and has required sections ─────────────────
580
- if (!fs.existsSync(projectPath)) {
581
- addIssue('error', 'E002', 'PROJECT.md not found', 'Run /ez:new-project to create');
582
- } else {
583
- const content = fs.readFileSync(projectPath, 'utf-8');
584
- const requiredSections = ['## What This Is', '## Core Value', '## Requirements'];
585
- for (const section of requiredSections) {
586
- if (!content.includes(section)) {
587
- addIssue('warning', 'W001', `PROJECT.md missing section: ${section}`, 'Add section manually');
588
- }
589
- }
590
- }
591
-
592
- // ─── Check 3: ROADMAP.md exists ───────────────────────────────────────────
593
- if (!fs.existsSync(roadmapPath)) {
594
- addIssue('error', 'E003', 'ROADMAP.md not found', 'Run /ez:new-milestone to create roadmap');
595
- }
596
-
597
- // ─── Check 4: STATE.md exists and references valid phases ─────────────────
598
- if (!fs.existsSync(statePath)) {
599
- addIssue('error', 'E004', 'STATE.md not found', 'Run /ez:health --repair to regenerate', true);
600
- repairs.push('regenerateState');
601
- } else {
602
- const stateContent = fs.readFileSync(statePath, 'utf-8');
603
- // Extract phase references from STATE.md
604
- const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+(?:\.\d+)*)/g)].map(m => m[1]);
605
- // Get disk phases
606
- const diskPhases = new Set();
607
- try {
608
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
609
- for (const e of entries) {
610
- if (e.isDirectory()) {
611
- const m = e.name.match(/^(\d+(?:\.\d+)*)/);
612
- if (m) diskPhases.add(m[1]);
613
- }
614
- }
615
- } catch (err) {
616
- logger.warn('Failed to read phase directories while validating STATE references', { phasesDir, error: err.message });
617
- }
618
- // Check for invalid references
619
- for (const ref of phaseRefs) {
620
- const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
621
- if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) {
622
- // Only warn if phases dir has any content (not just an empty project)
623
- if (diskPhases.size > 0) {
624
- addIssue('warning', 'W002', `STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`, 'Run /ez:health --repair to regenerate STATE.md', true);
625
- if (!repairs.includes('regenerateState')) repairs.push('regenerateState');
626
- }
627
- }
628
- }
629
- }
630
-
631
- // ─── Check 5: config.json valid JSON + valid schema ───────────────────────
632
- if (!fs.existsSync(configPath)) {
633
- addIssue('warning', 'W003', 'config.json not found', 'Run /ez:health --repair to create with defaults', true);
634
- repairs.push('createConfig');
635
- } else {
636
- try {
637
- const raw = fs.readFileSync(configPath, 'utf-8');
638
- const parsed = JSON.parse(raw);
639
- // Validate known fields
640
- const validProfiles = ['quality', 'balanced', 'budget'];
641
- if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) {
642
- addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
643
- }
644
- } catch (err) {
645
- logger.warn('Failed to parse config.json in cmdValidateHealth', { configPath, error: err.message });
646
- addIssue('error', 'E005', `config.json: JSON parse error - ${err.message}`, 'Run /ez:health --repair to reset to defaults', true);
647
- repairs.push('resetConfig');
648
- }
649
- }
650
-
651
- // ─── Check 5b: Nyquist validation key presence ──────────────────────────
652
- if (fs.existsSync(configPath)) {
653
- try {
654
- const configRaw = fs.readFileSync(configPath, 'utf-8');
655
- const configParsed = JSON.parse(configRaw);
656
- if (configParsed.workflow && configParsed.workflow.nyquist_validation === undefined) {
657
- addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /ez:health --repair to add key', true);
658
- if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey');
659
- }
660
- } catch (err) {
661
- logger.warn('Failed to parse config for nyquist key check', { configPath, error: err.message });
662
- }
663
- }
664
-
665
- // ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
666
- try {
667
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
668
- for (const e of entries) {
669
- if (e.isDirectory() && !e.name.match(/^\d{2}(?:\.\d+)*-[\w-]+$/)) {
670
- addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
671
- }
672
- }
673
- } catch (err) {
674
- logger.warn('Failed to inspect phase directory naming in health validation', { phasesDir, error: err.message });
675
- }
676
-
677
- // ─── Check 7: Orphaned plans (PLAN without SUMMARY) ───────────────────────
678
- try {
679
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
680
- for (const e of entries) {
681
- if (!e.isDirectory()) continue;
682
- const phaseFiles = fs.readdirSync(path.join(phasesDir, e.name));
683
- const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
684
- const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
685
- const summaryBases = new Set(summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '')));
686
-
687
- for (const plan of plans) {
688
- const planBase = plan.replace('-PLAN.md', '').replace('PLAN.md', '');
689
- if (!summaryBases.has(planBase)) {
690
- addIssue('info', 'I001', `${e.name}/${plan} has no SUMMARY.md`, 'May be in progress');
691
- }
692
- }
693
- }
694
- } catch (err) {
695
- logger.warn('Failed to inspect orphaned plans in health validation', { phasesDir, error: err.message });
696
- }
697
-
698
- // ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
699
- try {
700
- const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
701
- for (const e of phaseEntries) {
702
- if (!e.isDirectory()) continue;
703
- const phaseFiles = fs.readdirSync(path.join(phasesDir, e.name));
704
- const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md'));
705
- const hasValidation = phaseFiles.some(f => f.endsWith('-VALIDATION.md'));
706
- if (hasResearch && !hasValidation) {
707
- const researchFile = phaseFiles.find(f => f.endsWith('-RESEARCH.md'));
708
- const researchContent = fs.readFileSync(path.join(phasesDir, e.name, researchFile), 'utf-8');
709
- if (researchContent.includes('## Validation Architecture')) {
710
- addIssue('warning', 'W009', `Phase ${e.name}: has Validation Architecture in RESEARCH.md but no VALIDATION.md`, 'Re-run /ez-plan-phase with --research to regenerate');
711
- }
712
- }
713
- }
714
- } catch (err) {
715
- logger.warn('Failed to inspect validation architecture consistency in health validation', { phasesDir, error: err.message });
716
- }
717
-
718
- // ─── Check 8: Run existing consistency checks ─────────────────────────────
719
- // Inline subset of cmdValidateConsistency
720
- if (fs.existsSync(roadmapPath)) {
721
- const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
722
- const roadmapPhases = new Set();
723
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
724
- let m;
725
- while ((m = phasePattern.exec(roadmapContent)) !== null) {
726
- roadmapPhases.add(m[1]);
727
- }
728
-
729
- const diskPhases = new Set();
730
- try {
731
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
732
- for (const e of entries) {
733
- if (e.isDirectory()) {
734
- const dm = e.name.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
735
- if (dm) diskPhases.add(dm[1]);
736
- }
737
- }
738
- } catch (err) {
739
- logger.warn('Failed to run roadmap/disk consistency checks in health validation', { roadmapPath, phasesDir, error: err.message });
740
- }
741
-
742
- // Phases in ROADMAP but not on disk
743
- for (const p of roadmapPhases) {
744
- const padded = String(parseInt(p, 10)).padStart(2, '0');
745
- if (!diskPhases.has(p) && !diskPhases.has(padded)) {
746
- addIssue('warning', 'W006', `Phase ${p} in ROADMAP.md but no directory on disk`, 'Create phase directory or remove from roadmap');
747
- }
748
- }
749
-
750
- // Phases on disk but not in ROADMAP
751
- for (const p of diskPhases) {
752
- const unpadded = String(parseInt(p, 10));
753
- if (!roadmapPhases.has(p) && !roadmapPhases.has(unpadded)) {
754
- addIssue('warning', 'W007', `Phase ${p} exists on disk but not in ROADMAP.md`, 'Add to roadmap or remove directory');
755
- }
756
- }
757
- }
758
-
759
- // ─── Perform repairs if requested ─────────────────────────────────────────
760
- const repairActions = [];
761
- if (options.repair && repairs.length > 0) {
762
- for (const repair of repairs) {
763
- try {
764
- switch (repair) {
765
- case 'createConfig':
766
- case 'resetConfig': {
767
- const defaults = {
768
- model_profile: 'balanced',
769
- commit_docs: true,
770
- search_gitignored: false,
771
- branching_strategy: 'none',
772
- research: true,
773
- plan_checker: true,
774
- verifier: true,
775
- parallelization: true,
776
- };
777
- fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
778
- repairActions.push({ action: repair, success: true, path: 'config.json' });
779
- break;
780
- }
781
- case 'regenerateState': {
782
- // Create timestamped backup before overwriting
783
- if (fs.existsSync(statePath)) {
784
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
785
- const backupPath = `${statePath}.bak-${timestamp}`;
786
- fs.copyFileSync(statePath, backupPath);
787
- repairActions.push({ action: 'backupState', success: true, path: backupPath });
788
- }
789
- // Generate minimal STATE.md from ROADMAP.md structure
790
- const milestone = getMilestoneInfo(cwd);
791
- let stateContent = `# Session State\n\n`;
792
- stateContent += `## Project Reference\n\n`;
793
- stateContent += `See: .planning/PROJECT.md\n\n`;
794
- stateContent += `## Position\n\n`;
795
- stateContent += `**Milestone:** ${milestone.version} ${milestone.name}\n`;
796
- stateContent += `**Current phase:** (determining...)\n`;
797
- stateContent += `**Status:** Resuming\n\n`;
798
- stateContent += `## Session Log\n\n`;
799
- stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by /ez:health --repair\n`;
800
- writeStateMd(statePath, stateContent, cwd);
801
- repairActions.push({ action: repair, success: true, path: 'STATE.md' });
802
- break;
803
- }
804
- case 'addNyquistKey': {
805
- if (fs.existsSync(configPath)) {
806
- try {
807
- const configRaw = fs.readFileSync(configPath, 'utf-8');
808
- const configParsed = JSON.parse(configRaw);
809
- if (!configParsed.workflow) configParsed.workflow = {};
810
- if (configParsed.workflow.nyquist_validation === undefined) {
811
- configParsed.workflow.nyquist_validation = true;
812
- fs.writeFileSync(configPath, JSON.stringify(configParsed, null, 2), 'utf-8');
813
- }
814
- repairActions.push({ action: repair, success: true, path: 'config.json' });
815
- } catch (err) {
816
- logger.error('Failed to repair nyquist key', { error: err.message });
817
- repairActions.push({ action: repair, success: false, error: err.message });
818
- }
819
- }
820
- break;
821
- }
822
- }
823
- } catch (err) {
824
- logger.error('Failed to perform repair action', { action: repair, error: err.message });
825
- repairActions.push({ action: repair, success: false, error: err.message });
826
- }
827
- }
828
- }
829
-
830
- // ─── Determine overall status ─────────────────────────────────────────────
831
- let status;
832
- if (errors.length > 0) {
833
- status = 'broken';
834
- } else if (warnings.length > 0) {
835
- status = 'degraded';
836
- } else {
837
- status = 'healthy';
838
- }
839
-
840
- const repairableCount = errors.filter(e => e.repairable).length +
841
- warnings.filter(w => w.repairable).length;
842
-
843
- output({
844
- status,
845
- errors,
846
- warnings,
847
- info,
848
- repairable_count: repairableCount,
849
- repairs_performed: repairActions.length > 0 ? repairActions : undefined,
850
- }, raw);
851
- }
852
-
853
- module.exports = {
854
- cmdVerifySummary,
855
- cmdVerifyPlanStructure,
856
- cmdVerifyPhaseCompleteness,
857
- cmdVerifyReferences,
858
- cmdVerifyCommits,
859
- cmdVerifyArtifacts,
860
- cmdVerifyKeyLinks,
861
- cmdValidateConsistency,
862
- cmdValidateHealth,
863
- };
1
+ /**
2
+ * Verify — Verification suite, consistency, and health validation
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, output, error } = require('./core.cjs');
8
+ const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
9
+ const { writeStateMd } = require('./state.cjs');
10
+ const { defaultLogger: logger } = require('./logger.cjs');
11
+
12
+ async function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
13
+ if (!summaryPath) {
14
+ error('summary-path required');
15
+ }
16
+
17
+ const fullPath = path.join(cwd, summaryPath);
18
+ const checkCount = checkFileCount || 2;
19
+
20
+ // Check 1: Summary exists
21
+ if (!fs.existsSync(fullPath)) {
22
+ const result = {
23
+ passed: false,
24
+ checks: {
25
+ summary_exists: false,
26
+ files_created: { checked: 0, found: 0, missing: [] },
27
+ commits_exist: false,
28
+ self_check: 'not_found',
29
+ },
30
+ errors: ['SUMMARY.md not found'],
31
+ };
32
+ output(result, raw, 'failed');
33
+ return;
34
+ }
35
+
36
+ const content = fs.readFileSync(fullPath, 'utf-8');
37
+ const errors = [];
38
+
39
+ // Check 2: Spot-check files mentioned in summary
40
+ const mentionedFiles = new Set();
41
+ const patterns = [
42
+ /`([^`]+\.[a-zA-Z]+)`/g,
43
+ /(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`]+\.[a-zA-Z]+)`?/gi,
44
+ ];
45
+
46
+ for (const pattern of patterns) {
47
+ let m;
48
+ while ((m = pattern.exec(content)) !== null) {
49
+ const filePath = m[1];
50
+ if (filePath && !filePath.startsWith('http') && filePath.includes('/')) {
51
+ mentionedFiles.add(filePath);
52
+ }
53
+ }
54
+ }
55
+
56
+ const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
57
+ const missing = [];
58
+ for (const file of filesToCheck) {
59
+ if (!fs.existsSync(path.join(cwd, file))) {
60
+ missing.push(file);
61
+ }
62
+ }
63
+
64
+ // Check 3: Commits exist
65
+ const commitHashPattern = /\b[0-9a-f]{7,40}\b/g;
66
+ const hashes = content.match(commitHashPattern) || [];
67
+ let commitsExist = false;
68
+ if (hashes.length > 0) {
69
+ for (const hash of hashes.slice(0, 3)) {
70
+ const result = await execGit(cwd, ['cat-file', '-t', hash]);
71
+ if (result.exitCode === 0 && result.stdout === 'commit') {
72
+ commitsExist = true;
73
+ break;
74
+ }
75
+ }
76
+ }
77
+
78
+ // Check 4: Self-check section
79
+ let selfCheck = 'not_found';
80
+ const selfCheckPattern = /##\s*(?:Self[- ]?Check|Verification|Quality Check)/i;
81
+ if (selfCheckPattern.test(content)) {
82
+ const passPattern = /(?:all\s+)?(?:pass|✓|✅|complete|succeeded)/i;
83
+ const failPattern = /(?:fail|✗|❌|incomplete|blocked)/i;
84
+ const checkSection = content.slice(content.search(selfCheckPattern));
85
+ if (failPattern.test(checkSection)) {
86
+ selfCheck = 'failed';
87
+ } else if (passPattern.test(checkSection)) {
88
+ selfCheck = 'passed';
89
+ }
90
+ }
91
+
92
+ if (missing.length > 0) errors.push('Missing files: ' + missing.join(', '));
93
+ if (!commitsExist && hashes.length > 0) errors.push('Referenced commit hashes not found in git history');
94
+ if (selfCheck === 'failed') errors.push('Self-check section indicates failure');
95
+
96
+ const checks = {
97
+ summary_exists: true,
98
+ files_created: { checked: filesToCheck.length, found: filesToCheck.length - missing.length, missing },
99
+ commits_exist: commitsExist,
100
+ self_check: selfCheck,
101
+ };
102
+
103
+ const passed = missing.length === 0 && selfCheck !== 'failed';
104
+ const result = { passed, checks, errors };
105
+ output(result, raw, passed ? 'passed' : 'failed');
106
+ }
107
+
108
+ function cmdVerifyPlanStructure(cwd, filePath, raw) {
109
+ if (!filePath) { error('file path required'); }
110
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
111
+ const content = safeReadFile(fullPath);
112
+ if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
113
+
114
+ const fm = extractFrontmatter(content);
115
+ const errors = [];
116
+ const warnings = [];
117
+
118
+ // Check required frontmatter fields
119
+ const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
120
+ for (const field of required) {
121
+ if (fm[field] === undefined) errors.push(`Missing required frontmatter field: ${field}`);
122
+ }
123
+
124
+ // Parse and check task elements
125
+ const taskPattern = /<task[^>]*>([\s\S]*?)<\/task>/g;
126
+ const tasks = [];
127
+ let taskMatch;
128
+ while ((taskMatch = taskPattern.exec(content)) !== null) {
129
+ const taskContent = taskMatch[1];
130
+ const nameMatch = taskContent.match(/<name>([\s\S]*?)<\/name>/);
131
+ const taskName = nameMatch ? nameMatch[1].trim() : 'unnamed';
132
+ const hasFiles = /<files>/.test(taskContent);
133
+ const hasAction = /<action>/.test(taskContent);
134
+ const hasVerify = /<verify>/.test(taskContent);
135
+ const hasDone = /<done>/.test(taskContent);
136
+
137
+ if (!nameMatch) errors.push('Task missing <name> element');
138
+ if (!hasAction) errors.push(`Task '${taskName}' missing <action>`);
139
+ if (!hasVerify) warnings.push(`Task '${taskName}' missing <verify>`);
140
+ if (!hasDone) warnings.push(`Task '${taskName}' missing <done>`);
141
+ if (!hasFiles) warnings.push(`Task '${taskName}' missing <files>`);
142
+
143
+ tasks.push({ name: taskName, hasFiles, hasAction, hasVerify, hasDone });
144
+ }
145
+
146
+ if (tasks.length === 0) warnings.push('No <task> elements found');
147
+
148
+ // Wave/depends_on consistency
149
+ if (fm.wave && parseInt(fm.wave) > 1 && (!fm.depends_on || (Array.isArray(fm.depends_on) && fm.depends_on.length === 0))) {
150
+ warnings.push('Wave > 1 but depends_on is empty');
151
+ }
152
+
153
+ // Autonomous/checkpoint consistency
154
+ const hasCheckpoints = /<task\s+type=["']?checkpoint/.test(content);
155
+ if (hasCheckpoints && fm.autonomous !== 'false' && fm.autonomous !== false) {
156
+ errors.push('Has checkpoint tasks but autonomous is not false');
157
+ }
158
+
159
+ output({
160
+ valid: errors.length === 0,
161
+ errors,
162
+ warnings,
163
+ task_count: tasks.length,
164
+ tasks,
165
+ frontmatter_fields: Object.keys(fm),
166
+ }, raw, errors.length === 0 ? 'valid' : 'invalid');
167
+ }
168
+
169
+ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
170
+ if (!phase) { error('phase required'); }
171
+ const phaseInfo = findPhaseInternal(cwd, phase);
172
+ if (!phaseInfo || !phaseInfo.found) {
173
+ output({ error: 'Phase not found', phase }, raw);
174
+ return;
175
+ }
176
+
177
+ const errors = [];
178
+ const warnings = [];
179
+ const phaseDir = path.join(cwd, phaseInfo.directory);
180
+
181
+ // List plans and summaries
182
+ let files;
183
+ try {
184
+ files = fs.readdirSync(phaseDir);
185
+ } catch (err) {
186
+ logger.warn('Failed to read phase directory in cmdVerifyPhaseCompleteness', { phaseDir, error: err.message });
187
+ output({ error: 'Cannot read phase directory' }, raw);
188
+ return;
189
+ }
190
+
191
+ const plans = files.filter(f => f.match(/-PLAN\.md$/i));
192
+ const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i));
193
+
194
+ // Extract plan IDs (everything before -PLAN.md)
195
+ const planIds = new Set(plans.map(p => p.replace(/-PLAN\.md$/i, '')));
196
+ const summaryIds = new Set(summaries.map(s => s.replace(/-SUMMARY\.md$/i, '')));
197
+
198
+ // Plans without summaries
199
+ const incompletePlans = [...planIds].filter(id => !summaryIds.has(id));
200
+ if (incompletePlans.length > 0) {
201
+ errors.push(`Plans without summaries: ${incompletePlans.join(', ')}`);
202
+ }
203
+
204
+ // Summaries without plans (orphans)
205
+ const orphanSummaries = [...summaryIds].filter(id => !planIds.has(id));
206
+ if (orphanSummaries.length > 0) {
207
+ warnings.push(`Summaries without plans: ${orphanSummaries.join(', ')}`);
208
+ }
209
+
210
+ output({
211
+ complete: errors.length === 0,
212
+ phase: phaseInfo.phase_number,
213
+ plan_count: plans.length,
214
+ summary_count: summaries.length,
215
+ incomplete_plans: incompletePlans,
216
+ orphan_summaries: orphanSummaries,
217
+ errors,
218
+ warnings,
219
+ }, raw, errors.length === 0 ? 'complete' : 'incomplete');
220
+ }
221
+
222
+ function cmdVerifyReferences(cwd, filePath, raw) {
223
+ if (!filePath) { error('file path required'); }
224
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
225
+ const content = safeReadFile(fullPath);
226
+ if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
227
+
228
+ const found = [];
229
+ const missing = [];
230
+
231
+ // Find @-references: @path/to/file (must contain / to be a file path)
232
+ const atRefs = content.match(/@([^\s\n,)]+\/[^\s\n,)]+)/g) || [];
233
+ for (const ref of atRefs) {
234
+ const cleanRef = ref.slice(1); // remove @
235
+ const resolved = cleanRef.startsWith('~/')
236
+ ? path.join(process.env.HOME || '', cleanRef.slice(2))
237
+ : path.join(cwd, cleanRef);
238
+ if (fs.existsSync(resolved)) {
239
+ found.push(cleanRef);
240
+ } else {
241
+ missing.push(cleanRef);
242
+ }
243
+ }
244
+
245
+ // Find backtick file paths that look like real paths (contain / and have extension)
246
+ const backtickRefs = content.match(/`([^`]+\/[^`]+\.[a-zA-Z]{1,10})`/g) || [];
247
+ for (const ref of backtickRefs) {
248
+ const cleanRef = ref.slice(1, -1); // remove backticks
249
+ if (cleanRef.startsWith('http') || cleanRef.includes('${') || cleanRef.includes('{{')) continue;
250
+ if (found.includes(cleanRef) || missing.includes(cleanRef)) continue; // dedup
251
+ const resolved = path.join(cwd, cleanRef);
252
+ if (fs.existsSync(resolved)) {
253
+ found.push(cleanRef);
254
+ } else {
255
+ missing.push(cleanRef);
256
+ }
257
+ }
258
+
259
+ output({
260
+ valid: missing.length === 0,
261
+ found: found.length,
262
+ missing,
263
+ total: found.length + missing.length,
264
+ }, raw, missing.length === 0 ? 'valid' : 'invalid');
265
+ }
266
+
267
+ async function cmdVerifyCommits(cwd, hashes, raw) {
268
+ if (!hashes || hashes.length === 0) { error('At least one commit hash required'); }
269
+
270
+ const valid = [];
271
+ const invalid = [];
272
+ for (const hash of hashes) {
273
+ // Use git cat-file -t which supports both short and full hashes
274
+ // First try with the hash as-is (works for both short and full)
275
+ const result = await execGit(cwd, ['cat-file', '-t', hash]);
276
+ if (result.exitCode === 0 && result.stdout.trim() === 'commit') {
277
+ valid.push(hash);
278
+ } else {
279
+ // If that fails, try to resolve to full hash first
280
+ const resolveResult = await execGit(cwd, ['rev-parse', hash]);
281
+ if (resolveResult.exitCode === 0) {
282
+ const fullHash = resolveResult.stdout.trim();
283
+ const result2 = await execGit(cwd, ['cat-file', '-t', fullHash]);
284
+ if (result2.exitCode === 0 && result2.stdout.trim() === 'commit') {
285
+ valid.push(hash);
286
+ } else {
287
+ invalid.push(hash);
288
+ }
289
+ } else {
290
+ invalid.push(hash);
291
+ }
292
+ }
293
+ }
294
+
295
+ output({
296
+ all_valid: invalid.length === 0,
297
+ valid,
298
+ invalid,
299
+ total: hashes.length,
300
+ }, raw, invalid.length === 0 ? 'valid' : 'invalid');
301
+ }
302
+
303
+ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
304
+ if (!planFilePath) { error('plan file path required'); }
305
+ const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
306
+ const content = safeReadFile(fullPath);
307
+ if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
308
+
309
+ const artifacts = parseMustHavesBlock(content, 'artifacts');
310
+ if (artifacts.length === 0) {
311
+ output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
312
+ return;
313
+ }
314
+
315
+ const results = [];
316
+ for (const artifact of artifacts) {
317
+ if (typeof artifact === 'string') continue; // skip simple string items
318
+ const artPath = artifact.path;
319
+ if (!artPath) continue;
320
+
321
+ const artFullPath = path.join(cwd, artPath);
322
+ const exists = fs.existsSync(artFullPath);
323
+ const check = { path: artPath, exists, issues: [], passed: false };
324
+
325
+ if (exists) {
326
+ const fileContent = safeReadFile(artFullPath) || '';
327
+ const lineCount = fileContent.split('\n').length;
328
+
329
+ if (artifact.min_lines && lineCount < artifact.min_lines) {
330
+ check.issues.push(`Only ${lineCount} lines, need ${artifact.min_lines}`);
331
+ }
332
+ if (artifact.contains && !fileContent.includes(artifact.contains)) {
333
+ check.issues.push(`Missing pattern: ${artifact.contains}`);
334
+ }
335
+ if (artifact.exports) {
336
+ const exports = Array.isArray(artifact.exports) ? artifact.exports : [artifact.exports];
337
+ for (const exp of exports) {
338
+ if (!fileContent.includes(exp)) check.issues.push(`Missing export: ${exp}`);
339
+ }
340
+ }
341
+ check.passed = check.issues.length === 0;
342
+ } else {
343
+ check.issues.push('File not found');
344
+ }
345
+
346
+ results.push(check);
347
+ }
348
+
349
+ const passed = results.filter(r => r.passed).length;
350
+ output({
351
+ all_passed: passed === results.length,
352
+ passed,
353
+ total: results.length,
354
+ artifacts: results,
355
+ }, raw, passed === results.length ? 'valid' : 'invalid');
356
+ }
357
+
358
+ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
359
+ if (!planFilePath) { error('plan file path required'); }
360
+ const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
361
+ const content = safeReadFile(fullPath);
362
+ if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
363
+
364
+ const keyLinks = parseMustHavesBlock(content, 'key_links');
365
+ if (keyLinks.length === 0) {
366
+ output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
367
+ return;
368
+ }
369
+
370
+ const results = [];
371
+ for (const link of keyLinks) {
372
+ if (typeof link === 'string') continue;
373
+ const check = { from: link.from, to: link.to, via: link.via || '', verified: false, detail: '' };
374
+
375
+ const sourceContent = safeReadFile(path.join(cwd, link.from || ''));
376
+ if (!sourceContent) {
377
+ check.detail = 'Source file not found';
378
+ } else if (link.pattern) {
379
+ try {
380
+ const regex = new RegExp(link.pattern);
381
+ if (regex.test(sourceContent)) {
382
+ check.verified = true;
383
+ check.detail = 'Pattern found in source';
384
+ } else {
385
+ const targetContent = safeReadFile(path.join(cwd, link.to || ''));
386
+ if (targetContent && regex.test(targetContent)) {
387
+ check.verified = true;
388
+ check.detail = 'Pattern found in target';
389
+ } else {
390
+ check.detail = `Pattern "${link.pattern}" not found in source or target`;
391
+ }
392
+ }
393
+ } catch (err) {
394
+ logger.warn('Invalid regex while verifying key links', { pattern: link.pattern, error: err.message });
395
+ check.detail = `Invalid regex pattern: ${link.pattern}`;
396
+ }
397
+ } else {
398
+ // No pattern: just check source references target
399
+ if (sourceContent.includes(link.to || '')) {
400
+ check.verified = true;
401
+ check.detail = 'Target referenced in source';
402
+ } else {
403
+ check.detail = 'Target not referenced in source';
404
+ }
405
+ }
406
+
407
+ results.push(check);
408
+ }
409
+
410
+ const verified = results.filter(r => r.verified).length;
411
+ output({
412
+ all_verified: verified === results.length,
413
+ verified,
414
+ total: results.length,
415
+ links: results,
416
+ }, raw, verified === results.length ? 'valid' : 'invalid');
417
+ }
418
+
419
+ function cmdValidateConsistency(cwd, raw) {
420
+ const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
421
+ const phasesDir = path.join(cwd, '.planning', 'phases');
422
+ const errors = [];
423
+ const warnings = [];
424
+
425
+ // Check for ROADMAP
426
+ if (!fs.existsSync(roadmapPath)) {
427
+ errors.push('ROADMAP.md not found');
428
+ output({ passed: false, errors, warnings }, raw, 'failed');
429
+ return;
430
+ }
431
+
432
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
433
+
434
+ // Extract phases from ROADMAP
435
+ const roadmapPhases = new Set();
436
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
437
+ let m;
438
+ while ((m = phasePattern.exec(roadmapContent)) !== null) {
439
+ roadmapPhases.add(m[1]);
440
+ }
441
+
442
+ // Get phases on disk
443
+ const diskPhases = new Set();
444
+ try {
445
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
446
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
447
+ for (const dir of dirs) {
448
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
449
+ if (dm) diskPhases.add(dm[1]);
450
+ }
451
+ } catch (err) {
452
+ logger.warn('Failed to enumerate phase directories while validating consistency', { phasesDir, error: err.message });
453
+ }
454
+
455
+ // Check: phases in ROADMAP but not on disk
456
+ for (const p of roadmapPhases) {
457
+ if (!diskPhases.has(p) && !diskPhases.has(normalizePhaseName(p))) {
458
+ warnings.push(`Phase ${p} in ROADMAP.md but no directory on disk`);
459
+ }
460
+ }
461
+
462
+ // Check: phases on disk but not in ROADMAP
463
+ for (const p of diskPhases) {
464
+ const unpadded = String(parseInt(p, 10));
465
+ if (!roadmapPhases.has(p) && !roadmapPhases.has(unpadded)) {
466
+ warnings.push(`Phase ${p} exists on disk but not in ROADMAP.md`);
467
+ }
468
+ }
469
+
470
+ // Check: sequential phase numbers (integers only)
471
+ const integerPhases = [...diskPhases]
472
+ .filter(p => !p.includes('.'))
473
+ .map(p => parseInt(p, 10))
474
+ .sort((a, b) => a - b);
475
+
476
+ for (let i = 1; i < integerPhases.length; i++) {
477
+ if (integerPhases[i] !== integerPhases[i - 1] + 1) {
478
+ warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
479
+ }
480
+ }
481
+
482
+ // Check: plan numbering within phases
483
+ try {
484
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
485
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
486
+
487
+ for (const dir of dirs) {
488
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
489
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md')).sort();
490
+
491
+ // Extract plan numbers
492
+ const planNums = plans.map(p => {
493
+ const pm = p.match(/-(\d{2})-PLAN\.md$/);
494
+ return pm ? parseInt(pm[1], 10) : null;
495
+ }).filter(n => n !== null);
496
+
497
+ for (let i = 1; i < planNums.length; i++) {
498
+ if (planNums[i] !== planNums[i - 1] + 1) {
499
+ warnings.push(`Gap in plan numbering in ${dir}: plan ${planNums[i - 1]} → ${planNums[i]}`);
500
+ }
501
+ }
502
+
503
+ // Check: plans without summaries (completed plans)
504
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md'));
505
+ const planIds = new Set(plans.map(p => p.replace('-PLAN.md', '')));
506
+ const summaryIds = new Set(summaries.map(s => s.replace('-SUMMARY.md', '')));
507
+
508
+ // Summary without matching plan is suspicious
509
+ for (const sid of summaryIds) {
510
+ if (!planIds.has(sid)) {
511
+ warnings.push(`Summary ${sid}-SUMMARY.md in ${dir} has no matching PLAN.md`);
512
+ }
513
+ }
514
+ }
515
+ } catch (err) {
516
+ logger.warn('Failed to validate plan numbering while validating consistency', { phasesDir, error: err.message });
517
+ }
518
+
519
+ // Check: frontmatter in plans has required fields
520
+ try {
521
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
522
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
523
+
524
+ for (const dir of dirs) {
525
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
526
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md'));
527
+
528
+ for (const plan of plans) {
529
+ const content = fs.readFileSync(path.join(phasesDir, dir, plan), 'utf-8');
530
+ const fm = extractFrontmatter(content);
531
+
532
+ if (!fm.wave) {
533
+ warnings.push(`${dir}/${plan}: missing 'wave' in frontmatter`);
534
+ }
535
+ }
536
+ }
537
+ } catch (err) {
538
+ logger.warn('Failed to validate plan frontmatter while validating consistency', { phasesDir, error: err.message });
539
+ }
540
+
541
+ const passed = errors.length === 0;
542
+ output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
543
+ }
544
+
545
+ function cmdValidateHealth(cwd, options, raw) {
546
+ const planningDir = path.join(cwd, '.planning');
547
+ const projectPath = path.join(planningDir, 'PROJECT.md');
548
+ const roadmapPath = path.join(planningDir, 'ROADMAP.md');
549
+ const statePath = path.join(planningDir, 'STATE.md');
550
+ const configPath = path.join(planningDir, 'config.json');
551
+ const phasesDir = path.join(planningDir, 'phases');
552
+
553
+ const errors = [];
554
+ const warnings = [];
555
+ const info = [];
556
+ const repairs = [];
557
+
558
+ // Helper to add issue
559
+ const addIssue = (severity, code, message, fix, repairable = false) => {
560
+ const issue = { code, message, fix, repairable };
561
+ if (severity === 'error') errors.push(issue);
562
+ else if (severity === 'warning') warnings.push(issue);
563
+ else info.push(issue);
564
+ };
565
+
566
+ // ─── Check 1: .planning/ exists ───────────────────────────────────────────
567
+ if (!fs.existsSync(planningDir)) {
568
+ addIssue('error', 'E001', '.planning/ directory not found', 'Run /ez:new-project to initialize');
569
+ output({
570
+ status: 'broken',
571
+ errors,
572
+ warnings,
573
+ info,
574
+ repairable_count: 0,
575
+ }, raw);
576
+ return;
577
+ }
578
+
579
+ // ─── Check 2: PROJECT.md exists and has required sections ─────────────────
580
+ if (!fs.existsSync(projectPath)) {
581
+ addIssue('error', 'E002', 'PROJECT.md not found', 'Run /ez:new-project to create');
582
+ } else {
583
+ const content = fs.readFileSync(projectPath, 'utf-8');
584
+ const requiredSections = ['## What This Is', '## Core Value', '## Requirements'];
585
+ for (const section of requiredSections) {
586
+ if (!content.includes(section)) {
587
+ addIssue('warning', 'W001', `PROJECT.md missing section: ${section}`, 'Add section manually');
588
+ }
589
+ }
590
+ }
591
+
592
+ // ─── Check 3: ROADMAP.md exists ───────────────────────────────────────────
593
+ if (!fs.existsSync(roadmapPath)) {
594
+ addIssue('error', 'E003', 'ROADMAP.md not found', 'Run /ez:new-milestone to create roadmap');
595
+ }
596
+
597
+ // ─── Check 4: STATE.md exists and references valid phases ─────────────────
598
+ if (!fs.existsSync(statePath)) {
599
+ addIssue('error', 'E004', 'STATE.md not found', 'Run /ez:health --repair to regenerate', true);
600
+ repairs.push('regenerateState');
601
+ } else {
602
+ const stateContent = fs.readFileSync(statePath, 'utf-8');
603
+ // Extract phase references from STATE.md
604
+ const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+(?:\.\d+)*)/g)].map(m => m[1]);
605
+ // Get disk phases
606
+ const diskPhases = new Set();
607
+ try {
608
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
609
+ for (const e of entries) {
610
+ if (e.isDirectory()) {
611
+ const m = e.name.match(/^(\d+(?:\.\d+)*)/);
612
+ if (m) diskPhases.add(m[1]);
613
+ }
614
+ }
615
+ } catch (err) {
616
+ logger.warn('Failed to read phase directories while validating STATE references', { phasesDir, error: err.message });
617
+ }
618
+ // Check for invalid references
619
+ for (const ref of phaseRefs) {
620
+ const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
621
+ if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) {
622
+ // Only warn if phases dir has any content (not just an empty project)
623
+ if (diskPhases.size > 0) {
624
+ addIssue('warning', 'W002', `STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`, 'Run /ez:health --repair to regenerate STATE.md', true);
625
+ if (!repairs.includes('regenerateState')) repairs.push('regenerateState');
626
+ }
627
+ }
628
+ }
629
+ }
630
+
631
+ // ─── Check 5: config.json valid JSON + valid schema ───────────────────────
632
+ if (!fs.existsSync(configPath)) {
633
+ addIssue('warning', 'W003', 'config.json not found', 'Run /ez:health --repair to create with defaults', true);
634
+ repairs.push('createConfig');
635
+ } else {
636
+ try {
637
+ const raw = fs.readFileSync(configPath, 'utf-8');
638
+ const parsed = JSON.parse(raw);
639
+ // Validate known fields
640
+ const validProfiles = ['quality', 'balanced', 'budget'];
641
+ if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) {
642
+ addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
643
+ }
644
+ } catch (err) {
645
+ logger.warn('Failed to parse config.json in cmdValidateHealth', { configPath, error: err.message });
646
+ addIssue('error', 'E005', `config.json: JSON parse error - ${err.message}`, 'Run /ez:health --repair to reset to defaults', true);
647
+ repairs.push('resetConfig');
648
+ }
649
+ }
650
+
651
+ // ─── Check 5b: Nyquist validation key presence ──────────────────────────
652
+ if (fs.existsSync(configPath)) {
653
+ try {
654
+ const configRaw = fs.readFileSync(configPath, 'utf-8');
655
+ const configParsed = JSON.parse(configRaw);
656
+ if (configParsed.workflow && configParsed.workflow.nyquist_validation === undefined) {
657
+ addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /ez:health --repair to add key', true);
658
+ if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey');
659
+ }
660
+ } catch (err) {
661
+ logger.warn('Failed to parse config for nyquist key check', { configPath, error: err.message });
662
+ }
663
+ }
664
+
665
+ // ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
666
+ try {
667
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
668
+ for (const e of entries) {
669
+ if (e.isDirectory() && !e.name.match(/^\d{2}(?:\.\d+)*-[\w-]+$/)) {
670
+ addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
671
+ }
672
+ }
673
+ } catch (err) {
674
+ logger.warn('Failed to inspect phase directory naming in health validation', { phasesDir, error: err.message });
675
+ }
676
+
677
+ // ─── Check 7: Orphaned plans (PLAN without SUMMARY) ───────────────────────
678
+ try {
679
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
680
+ for (const e of entries) {
681
+ if (!e.isDirectory()) continue;
682
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, e.name));
683
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
684
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
685
+ const summaryBases = new Set(summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '')));
686
+
687
+ for (const plan of plans) {
688
+ const planBase = plan.replace('-PLAN.md', '').replace('PLAN.md', '');
689
+ if (!summaryBases.has(planBase)) {
690
+ addIssue('info', 'I001', `${e.name}/${plan} has no SUMMARY.md`, 'May be in progress');
691
+ }
692
+ }
693
+ }
694
+ } catch (err) {
695
+ logger.warn('Failed to inspect orphaned plans in health validation', { phasesDir, error: err.message });
696
+ }
697
+
698
+ // ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
699
+ try {
700
+ const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
701
+ for (const e of phaseEntries) {
702
+ if (!e.isDirectory()) continue;
703
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, e.name));
704
+ const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md'));
705
+ const hasValidation = phaseFiles.some(f => f.endsWith('-VALIDATION.md'));
706
+ if (hasResearch && !hasValidation) {
707
+ const researchFile = phaseFiles.find(f => f.endsWith('-RESEARCH.md'));
708
+ const researchContent = fs.readFileSync(path.join(phasesDir, e.name, researchFile), 'utf-8');
709
+ if (researchContent.includes('## Validation Architecture')) {
710
+ addIssue('warning', 'W009', `Phase ${e.name}: has Validation Architecture in RESEARCH.md but no VALIDATION.md`, 'Re-run /ez-plan-phase with --research to regenerate');
711
+ }
712
+ }
713
+ }
714
+ } catch (err) {
715
+ logger.warn('Failed to inspect validation architecture consistency in health validation', { phasesDir, error: err.message });
716
+ }
717
+
718
+ // ─── Check 8: Run existing consistency checks ─────────────────────────────
719
+ // Inline subset of cmdValidateConsistency
720
+ if (fs.existsSync(roadmapPath)) {
721
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
722
+ const roadmapPhases = new Set();
723
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
724
+ let m;
725
+ while ((m = phasePattern.exec(roadmapContent)) !== null) {
726
+ roadmapPhases.add(m[1]);
727
+ }
728
+
729
+ const diskPhases = new Set();
730
+ try {
731
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
732
+ for (const e of entries) {
733
+ if (e.isDirectory()) {
734
+ const dm = e.name.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
735
+ if (dm) diskPhases.add(dm[1]);
736
+ }
737
+ }
738
+ } catch (err) {
739
+ logger.warn('Failed to run roadmap/disk consistency checks in health validation', { roadmapPath, phasesDir, error: err.message });
740
+ }
741
+
742
+ // Phases in ROADMAP but not on disk
743
+ for (const p of roadmapPhases) {
744
+ const padded = String(parseInt(p, 10)).padStart(2, '0');
745
+ if (!diskPhases.has(p) && !diskPhases.has(padded)) {
746
+ addIssue('warning', 'W006', `Phase ${p} in ROADMAP.md but no directory on disk`, 'Create phase directory or remove from roadmap');
747
+ }
748
+ }
749
+
750
+ // Phases on disk but not in ROADMAP
751
+ for (const p of diskPhases) {
752
+ const unpadded = String(parseInt(p, 10));
753
+ if (!roadmapPhases.has(p) && !roadmapPhases.has(unpadded)) {
754
+ addIssue('warning', 'W007', `Phase ${p} exists on disk but not in ROADMAP.md`, 'Add to roadmap or remove directory');
755
+ }
756
+ }
757
+ }
758
+
759
+ // ─── Perform repairs if requested ─────────────────────────────────────────
760
+ const repairActions = [];
761
+ if (options.repair && repairs.length > 0) {
762
+ for (const repair of repairs) {
763
+ try {
764
+ switch (repair) {
765
+ case 'createConfig':
766
+ case 'resetConfig': {
767
+ const defaults = {
768
+ model_profile: 'balanced',
769
+ commit_docs: true,
770
+ search_gitignored: false,
771
+ branching_strategy: 'none',
772
+ research: true,
773
+ plan_checker: true,
774
+ verifier: true,
775
+ parallelization: true,
776
+ };
777
+ fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
778
+ repairActions.push({ action: repair, success: true, path: 'config.json' });
779
+ break;
780
+ }
781
+ case 'regenerateState': {
782
+ // Create timestamped backup before overwriting
783
+ if (fs.existsSync(statePath)) {
784
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
785
+ const backupPath = `${statePath}.bak-${timestamp}`;
786
+ fs.copyFileSync(statePath, backupPath);
787
+ repairActions.push({ action: 'backupState', success: true, path: backupPath });
788
+ }
789
+ // Generate minimal STATE.md from ROADMAP.md structure
790
+ const milestone = getMilestoneInfo(cwd);
791
+ let stateContent = `# Session State\n\n`;
792
+ stateContent += `## Project Reference\n\n`;
793
+ stateContent += `See: .planning/PROJECT.md\n\n`;
794
+ stateContent += `## Position\n\n`;
795
+ stateContent += `**Milestone:** ${milestone.version} ${milestone.name}\n`;
796
+ stateContent += `**Current phase:** (determining...)\n`;
797
+ stateContent += `**Status:** Resuming\n\n`;
798
+ stateContent += `## Session Log\n\n`;
799
+ stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by /ez:health --repair\n`;
800
+ writeStateMd(statePath, stateContent, cwd);
801
+ repairActions.push({ action: repair, success: true, path: 'STATE.md' });
802
+ break;
803
+ }
804
+ case 'addNyquistKey': {
805
+ if (fs.existsSync(configPath)) {
806
+ try {
807
+ const configRaw = fs.readFileSync(configPath, 'utf-8');
808
+ const configParsed = JSON.parse(configRaw);
809
+ if (!configParsed.workflow) configParsed.workflow = {};
810
+ if (configParsed.workflow.nyquist_validation === undefined) {
811
+ configParsed.workflow.nyquist_validation = true;
812
+ fs.writeFileSync(configPath, JSON.stringify(configParsed, null, 2), 'utf-8');
813
+ }
814
+ repairActions.push({ action: repair, success: true, path: 'config.json' });
815
+ } catch (err) {
816
+ logger.error('Failed to repair nyquist key', { error: err.message });
817
+ repairActions.push({ action: repair, success: false, error: err.message });
818
+ }
819
+ }
820
+ break;
821
+ }
822
+ }
823
+ } catch (err) {
824
+ logger.error('Failed to perform repair action', { action: repair, error: err.message });
825
+ repairActions.push({ action: repair, success: false, error: err.message });
826
+ }
827
+ }
828
+ }
829
+
830
+ // ─── Determine overall status ─────────────────────────────────────────────
831
+ let status;
832
+ if (errors.length > 0) {
833
+ status = 'broken';
834
+ } else if (warnings.length > 0) {
835
+ status = 'degraded';
836
+ } else {
837
+ status = 'healthy';
838
+ }
839
+
840
+ const repairableCount = errors.filter(e => e.repairable).length +
841
+ warnings.filter(w => w.repairable).length;
842
+
843
+ output({
844
+ status,
845
+ errors,
846
+ warnings,
847
+ info,
848
+ repairable_count: repairableCount,
849
+ repairs_performed: repairActions.length > 0 ? repairActions : undefined,
850
+ }, raw);
851
+ }
852
+
853
+ module.exports = {
854
+ cmdVerifySummary,
855
+ cmdVerifyPlanStructure,
856
+ cmdVerifyPhaseCompleteness,
857
+ cmdVerifyReferences,
858
+ cmdVerifyCommits,
859
+ cmdVerifyArtifacts,
860
+ cmdVerifyKeyLinks,
861
+ cmdValidateConsistency,
862
+ cmdValidateHealth,
863
+ };