@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,685 +1,685 @@
1
- /**
2
- * Commands — Standalone utility commands
3
- */
4
- const fs = require('fs');
5
- const path = require('path');
6
- const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
7
- const { extractFrontmatter } = require('./frontmatter.cjs');
8
- const { defaultLogger: logger } = require('./logger.cjs');
9
-
10
- function cmdGenerateSlug(text, raw) {
11
- if (!text) {
12
- error('text required for slug generation');
13
- }
14
-
15
- const slug = text
16
- .toLowerCase()
17
- .replace(/[^a-z0-9]+/g, '-')
18
- .replace(/^-+|-+$/g, '');
19
-
20
- const result = { slug };
21
- output(result, raw, slug);
22
- }
23
-
24
- function cmdCurrentTimestamp(format, raw) {
25
- const now = new Date();
26
- let result;
27
-
28
- switch (format) {
29
- case 'date':
30
- result = now.toISOString().split('T')[0];
31
- break;
32
- case 'filename':
33
- result = now.toISOString().replace(/:/g, '-').replace(/\..+/, '');
34
- break;
35
- case 'full':
36
- default:
37
- result = now.toISOString();
38
- break;
39
- }
40
-
41
- output({ timestamp: result }, raw, result);
42
- }
43
-
44
- function cmdListTodos(cwd, area, raw) {
45
- const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
46
-
47
- let count = 0;
48
- const todos = [];
49
-
50
- try {
51
- const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
52
-
53
- for (const file of files) {
54
- try {
55
- const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
56
- const createdMatch = content.match(/^created:\s*(.+)$/m);
57
- const titleMatch = content.match(/^title:\s*(.+)$/m);
58
- const areaMatch = content.match(/^area:\s*(.+)$/m);
59
-
60
- const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
61
-
62
- // Apply area filter if specified
63
- if (area && todoArea !== area) continue;
64
-
65
- count++;
66
- todos.push({
67
- file,
68
- created: createdMatch ? createdMatch[1].trim() : 'unknown',
69
- title: titleMatch ? titleMatch[1].trim() : 'Untitled',
70
- area: todoArea,
71
- path: toPosixPath(path.join('.planning', 'todos', 'pending', file)),
72
- });
73
- } catch (err) {
74
- logger.warn('Failed to parse todo file in cmdListTodos', { file, error: err.message });
75
- }
76
- }
77
- } catch (err) {
78
- logger.warn('Failed to list pending todos in cmdListTodos', { pendingDir, error: err.message });
79
- }
80
-
81
- const result = { count, todos };
82
- output(result, raw, count.toString());
83
- }
84
-
85
- function cmdVerifyPathExists(cwd, targetPath, raw) {
86
- if (!targetPath) {
87
- error('path required for verification');
88
- }
89
-
90
- const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
91
-
92
- try {
93
- const stats = fs.statSync(fullPath);
94
- const type = stats.isDirectory() ? 'directory' : stats.isFile() ? 'file' : 'other';
95
- const result = { exists: true, type };
96
- output(result, raw, 'true');
97
- } catch (err) {
98
- logger.warn('Path verification failed in cmdVerifyPathExists', { fullPath, error: err.message });
99
- const result = { exists: false, type: null };
100
- output(result, raw, 'false');
101
- }
102
- }
103
-
104
- function cmdHistoryDigest(cwd, raw) {
105
- const phasesDir = path.join(cwd, '.planning', 'phases');
106
- const digest = { phases: {}, decisions: [], tech_stack: new Set() };
107
-
108
- // Collect all phase directories: archived + current
109
- const allPhaseDirs = [];
110
-
111
- // Add archived phases first (oldest milestones first)
112
- const archived = getArchivedPhaseDirs(cwd);
113
- for (const a of archived) {
114
- allPhaseDirs.push({ name: a.name, fullPath: a.fullPath, milestone: a.milestone });
115
- }
116
-
117
- // Add current phases
118
- if (fs.existsSync(phasesDir)) {
119
- try {
120
- const currentDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
121
- .filter(e => e.isDirectory())
122
- .map(e => e.name)
123
- .sort();
124
- for (const dir of currentDirs) {
125
- allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null });
126
- }
127
- } catch (err) {
128
- logger.warn('Failed to enumerate current phase directories in cmdHistoryDigest', { phasesDir, error: err.message });
129
- }
130
- }
131
-
132
- if (allPhaseDirs.length === 0) {
133
- digest.tech_stack = [];
134
- output(digest, raw);
135
- return;
136
- }
137
-
138
- try {
139
- for (const { name: dir, fullPath: dirPath } of allPhaseDirs) {
140
- const summaries = fs.readdirSync(dirPath).filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
141
-
142
- for (const summary of summaries) {
143
- try {
144
- const content = fs.readFileSync(path.join(dirPath, summary), 'utf-8');
145
- const fm = extractFrontmatter(content);
146
-
147
- const phaseNum = fm.phase || dir.split('-')[0];
148
-
149
- if (!digest.phases[phaseNum]) {
150
- digest.phases[phaseNum] = {
151
- name: fm.name || dir.split('-').slice(1).join(' ') || 'Unknown',
152
- provides: new Set(),
153
- affects: new Set(),
154
- patterns: new Set(),
155
- };
156
- }
157
-
158
- // Merge provides
159
- if (fm['dependency-graph'] && fm['dependency-graph'].provides) {
160
- fm['dependency-graph'].provides.forEach(p => digest.phases[phaseNum].provides.add(p));
161
- } else if (fm.provides) {
162
- fm.provides.forEach(p => digest.phases[phaseNum].provides.add(p));
163
- }
164
-
165
- // Merge affects
166
- if (fm['dependency-graph'] && fm['dependency-graph'].affects) {
167
- fm['dependency-graph'].affects.forEach(a => digest.phases[phaseNum].affects.add(a));
168
- }
169
-
170
- // Merge patterns
171
- if (fm['patterns-established']) {
172
- fm['patterns-established'].forEach(p => digest.phases[phaseNum].patterns.add(p));
173
- }
174
-
175
- // Merge decisions
176
- if (fm['key-decisions']) {
177
- fm['key-decisions'].forEach(d => {
178
- digest.decisions.push({ phase: phaseNum, decision: d });
179
- });
180
- }
181
-
182
- // Merge tech stack
183
- if (fm['tech-stack'] && fm['tech-stack'].added) {
184
- fm['tech-stack'].added.forEach(t => digest.tech_stack.add(typeof t === 'string' ? t : t.name));
185
- }
186
-
187
- } catch (err) {
188
- logger.warn('Skipping malformed summary in cmdHistoryDigest', { summary, dirPath, error: err.message });
189
- }
190
- }
191
- }
192
-
193
- // Convert Sets to Arrays for JSON output
194
- Object.keys(digest.phases).forEach(p => {
195
- digest.phases[p].provides = [...digest.phases[p].provides];
196
- digest.phases[p].affects = [...digest.phases[p].affects];
197
- digest.phases[p].patterns = [...digest.phases[p].patterns];
198
- });
199
- digest.tech_stack = [...digest.tech_stack];
200
-
201
- output(digest, raw);
202
- } catch (err) {
203
- logger.error('Failed to generate history digest', { error: err.message });
204
- error('Failed to generate history digest: ' + err.message);
205
- }
206
- }
207
-
208
- function cmdResolveModel(cwd, agentType, raw) {
209
- if (!agentType) {
210
- error('agent-type required');
211
- }
212
-
213
- const config = loadConfig(cwd);
214
- const profile = config.model_profile || 'balanced';
215
- const model = resolveModelInternal(cwd, agentType);
216
-
217
- const agentModels = MODEL_PROFILES[agentType];
218
- const result = agentModels
219
- ? { model, profile }
220
- : { model, profile, unknown_agent: true };
221
- output(result, raw, model);
222
- }
223
-
224
- async function cmdCommit(cwd, message, files, raw, amend) {
225
- if (!message && !amend) {
226
- error('commit message required');
227
- }
228
-
229
- const config = loadConfig(cwd);
230
-
231
- // Check commit_docs config
232
- if (!config.commit_docs) {
233
- const result = { committed: false, hash: null, reason: 'skipped_commit_docs_false' };
234
- output(result, raw, 'skipped');
235
- return;
236
- }
237
-
238
- // Check if .planning is gitignored
239
- if (await isGitIgnored(cwd, '.planning')) {
240
- const result = { committed: false, hash: null, reason: 'skipped_gitignored' };
241
- output(result, raw, 'skipped');
242
- return;
243
- }
244
-
245
- // Stage files
246
- const filesToStage = files && files.length > 0 ? files : ['.planning/'];
247
- for (const file of filesToStage) {
248
- await execGit(cwd, ['add', file]);
249
- }
250
-
251
- // Commit
252
- const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
253
- const commitResult = await execGit(cwd, commitArgs);
254
- if (commitResult.exitCode !== 0) {
255
- if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
256
- const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
257
- output(result, raw, 'nothing');
258
- return;
259
- }
260
- const result = { committed: false, hash: null, reason: 'nothing_to_commit', error: commitResult.stderr };
261
- output(result, raw, 'nothing');
262
- return;
263
- }
264
-
265
- // Get short hash
266
- const hashResult = await execGit(cwd, ['rev-parse', '--short', 'HEAD']);
267
- const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
268
- const result = { committed: true, hash, reason: 'committed' };
269
- output(result, raw, hash || 'committed');
270
- }
271
-
272
- function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
273
- if (!summaryPath) {
274
- error('summary-path required for summary-extract');
275
- }
276
-
277
- const fullPath = path.join(cwd, summaryPath);
278
-
279
- if (!fs.existsSync(fullPath)) {
280
- output({ error: 'File not found', path: summaryPath }, raw);
281
- return;
282
- }
283
-
284
- const content = fs.readFileSync(fullPath, 'utf-8');
285
- const fm = extractFrontmatter(content);
286
-
287
- // Parse key-decisions into structured format
288
- const parseDecisions = (decisionsList) => {
289
- if (!decisionsList || !Array.isArray(decisionsList)) return [];
290
- return decisionsList.map(d => {
291
- const colonIdx = d.indexOf(':');
292
- if (colonIdx > 0) {
293
- return {
294
- summary: d.substring(0, colonIdx).trim(),
295
- rationale: d.substring(colonIdx + 1).trim(),
296
- };
297
- }
298
- return { summary: d, rationale: null };
299
- });
300
- };
301
-
302
- // Build full result
303
- const fullResult = {
304
- path: summaryPath,
305
- one_liner: fm['one-liner'] || null,
306
- key_files: fm['key-files'] || [],
307
- tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
308
- patterns: fm['patterns-established'] || [],
309
- decisions: parseDecisions(fm['key-decisions']),
310
- requirements_completed: fm['requirements-completed'] || [],
311
- };
312
-
313
- // If fields specified, filter to only those fields
314
- if (fields && fields.length > 0) {
315
- const filtered = { path: summaryPath };
316
- for (const field of fields) {
317
- if (fullResult[field] !== undefined) {
318
- filtered[field] = fullResult[field];
319
- }
320
- }
321
- output(filtered, raw);
322
- return;
323
- }
324
-
325
- output(fullResult, raw);
326
- }
327
-
328
- async function cmdWebsearch(query, options, raw) {
329
- const apiKey = process.env.BRAVE_API_KEY;
330
-
331
- if (!apiKey) {
332
- // No key = silent skip, agent falls back to built-in WebSearch
333
- output({ available: false, reason: 'BRAVE_API_KEY not set' }, raw, '');
334
- return;
335
- }
336
-
337
- if (!query) {
338
- output({ available: false, error: 'Query required' }, raw, '');
339
- return;
340
- }
341
-
342
- const params = new URLSearchParams({
343
- q: query,
344
- count: String(options.limit || 10),
345
- country: 'us',
346
- search_lang: 'en',
347
- text_decorations: 'false'
348
- });
349
-
350
- if (options.freshness) {
351
- params.set('freshness', options.freshness);
352
- }
353
-
354
- try {
355
- const response = await fetch(
356
- `https://api.search.brave.com/res/v1/web/search?${params}`,
357
- {
358
- headers: {
359
- 'Accept': 'application/json',
360
- 'X-Subscription-Token': apiKey
361
- }
362
- }
363
- );
364
-
365
- if (!response.ok) {
366
- output({ available: false, error: `API error: ${response.status}` }, raw, '');
367
- return;
368
- }
369
-
370
- const data = await response.json();
371
-
372
- const results = (data.web?.results || []).map(r => ({
373
- title: r.title,
374
- url: r.url,
375
- description: r.description,
376
- age: r.age || null
377
- }));
378
-
379
- output({
380
- available: true,
381
- query,
382
- count: results.length,
383
- results
384
- }, raw, results.map(r => `${r.title}\n${r.url}\n${r.description}`).join('\n\n'));
385
- } catch (err) {
386
- logger.warn('Websearch request failed in cmdWebsearch', { query, error: err.message });
387
- output({ available: false, error: err.message }, raw, '');
388
- }
389
- }
390
-
391
- function cmdProgressRender(cwd, format, raw) {
392
- const phasesDir = path.join(cwd, '.planning', 'phases');
393
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
394
- const milestone = getMilestoneInfo(cwd);
395
-
396
- const phases = [];
397
- let totalPlans = 0;
398
- let totalSummaries = 0;
399
-
400
- try {
401
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
402
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
403
-
404
- for (const dir of dirs) {
405
- const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
406
- const phaseNum = dm ? dm[1] : dir;
407
- const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
408
- const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
409
- const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
410
- const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
411
-
412
- totalPlans += plans;
413
- totalSummaries += summaries;
414
-
415
- let status;
416
- if (plans === 0) status = 'Pending';
417
- else if (summaries >= plans) status = 'Complete';
418
- else if (summaries > 0) status = 'In Progress';
419
- else status = 'Planned';
420
-
421
- phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
422
- }
423
- } catch (err) {
424
- logger.warn('Failed to enumerate phase directories in cmdProgressRender', { phasesDir, error: err.message });
425
- }
426
-
427
- const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
428
-
429
- if (format === 'table') {
430
- // Render markdown table
431
- const barWidth = 10;
432
- const filled = Math.round((percent / 100) * barWidth);
433
- const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
434
- let out = `# ${milestone.version} ${milestone.name}\n\n`;
435
- out += `**Progress:** [${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)\n\n`;
436
- out += `| Phase | Name | Plans | Status |\n`;
437
- out += `|-------|------|-------|--------|\n`;
438
- for (const p of phases) {
439
- out += `| ${p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`;
440
- }
441
- output({ rendered: out }, raw, out);
442
- } else if (format === 'bar') {
443
- const barWidth = 20;
444
- const filled = Math.round((percent / 100) * barWidth);
445
- const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
446
- const text = `[${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)`;
447
- output({ bar: text, percent, completed: totalSummaries, total: totalPlans }, raw, text);
448
- } else {
449
- // JSON format
450
- output({
451
- milestone_version: milestone.version,
452
- milestone_name: milestone.name,
453
- phases,
454
- total_plans: totalPlans,
455
- total_summaries: totalSummaries,
456
- percent,
457
- }, raw);
458
- }
459
- }
460
-
461
- function cmdTodoComplete(cwd, filename, raw) {
462
- if (!filename) {
463
- error('filename required for todo complete');
464
- }
465
-
466
- const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
467
- const completedDir = path.join(cwd, '.planning', 'todos', 'completed');
468
- const sourcePath = path.join(pendingDir, filename);
469
-
470
- if (!fs.existsSync(sourcePath)) {
471
- error(`Todo not found: ${filename}`);
472
- }
473
-
474
- // Ensure completed directory exists
475
- fs.mkdirSync(completedDir, { recursive: true });
476
-
477
- // Read, add completion timestamp, move
478
- let content = fs.readFileSync(sourcePath, 'utf-8');
479
- const today = new Date().toISOString().split('T')[0];
480
- content = `completed: ${today}\n` + content;
481
-
482
- fs.writeFileSync(path.join(completedDir, filename), content, 'utf-8');
483
- fs.unlinkSync(sourcePath);
484
-
485
- output({ completed: true, file: filename, date: today }, raw, 'completed');
486
- }
487
-
488
- function cmdScaffold(cwd, type, options, raw) {
489
- const { phase, name } = options;
490
- const padded = phase ? normalizePhaseName(phase) : '00';
491
- const today = new Date().toISOString().split('T')[0];
492
-
493
- // Find phase directory
494
- const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
495
- const phaseDir = phaseInfo ? path.join(cwd, phaseInfo.directory) : null;
496
-
497
- if (phase && !phaseDir && type !== 'phase-dir') {
498
- error(`Phase ${phase} directory not found`);
499
- }
500
-
501
- let filePath, content;
502
-
503
- switch (type) {
504
- case 'context': {
505
- filePath = path.join(phaseDir, `${padded}-CONTEXT.md`);
506
- content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during /ez-discuss-phase ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
507
- break;
508
- }
509
- case 'uat': {
510
- filePath = path.join(phaseDir, `${padded}-UAT.md`);
511
- content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — User Acceptance Testing\n\n## Test Results\n\n| # | Test | Status | Notes |\n|---|------|--------|-------|\n\n## Summary\n\n_Pending UAT_\n`;
512
- break;
513
- }
514
- case 'verification': {
515
- filePath = path.join(phaseDir, `${padded}-VERIFICATION.md`);
516
- content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Verification\n\n## Goal-Backward Verification\n\n**Phase Goal:** [From ROADMAP.md]\n\n## Checks\n\n| # | Requirement | Status | Evidence |\n|---|------------|--------|----------|\n\n## Result\n\n_Pending verification_\n`;
517
- break;
518
- }
519
- case 'phase-dir': {
520
- if (!phase || !name) {
521
- error('phase and name required for phase-dir scaffold');
522
- }
523
- const slug = generateSlugInternal(name);
524
- const dirName = `${padded}-${slug}`;
525
- const phasesParent = path.join(cwd, '.planning', 'phases');
526
- fs.mkdirSync(phasesParent, { recursive: true });
527
- const dirPath = path.join(phasesParent, dirName);
528
- fs.mkdirSync(dirPath, { recursive: true });
529
- output({ created: true, directory: `.planning/phases/${dirName}`, path: dirPath }, raw, dirPath);
530
- return;
531
- }
532
- default:
533
- error(`Unknown scaffold type: ${type}. Available: context, uat, verification, phase-dir`);
534
- }
535
-
536
- if (fs.existsSync(filePath)) {
537
- output({ created: false, reason: 'already_exists', path: filePath }, raw, 'exists');
538
- return;
539
- }
540
-
541
- fs.writeFileSync(filePath, content, 'utf-8');
542
- const relPath = toPosixPath(path.relative(cwd, filePath));
543
- output({ created: true, path: relPath }, raw, relPath);
544
- }
545
-
546
- async function cmdStats(cwd, format, raw) {
547
- const phasesDir = path.join(cwd, '.planning', 'phases');
548
- const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
549
- const statePath = path.join(cwd, '.planning', 'STATE.md');
550
- const milestone = getMilestoneInfo(cwd);
551
-
552
- // Phase & plan stats (reuse progress pattern)
553
- const phases = [];
554
- let totalPlans = 0;
555
- let totalSummaries = 0;
556
-
557
- try {
558
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
559
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
560
-
561
- for (const dir of dirs) {
562
- const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
563
- const phaseNum = dm ? dm[1] : dir;
564
- const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
565
- const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
566
- const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
567
- const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
568
-
569
- totalPlans += plans;
570
- totalSummaries += summaries;
571
-
572
- let status;
573
- if (plans === 0) status = 'Pending';
574
- else if (summaries >= plans) status = 'Complete';
575
- else if (summaries > 0) status = 'In Progress';
576
- else status = 'Planned';
577
-
578
- phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
579
- }
580
- } catch (err) {
581
- logger.warn('Failed to enumerate phase directories in cmdStats', { phasesDir, error: err.message });
582
- }
583
-
584
- const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
585
-
586
- // Requirements stats
587
- let requirementsTotal = 0;
588
- let requirementsComplete = 0;
589
- try {
590
- if (fs.existsSync(reqPath)) {
591
- const reqContent = fs.readFileSync(reqPath, 'utf-8');
592
- const checked = reqContent.match(/^- \[x\] \*\*/gm);
593
- const unchecked = reqContent.match(/^- \[ \] \*\*/gm);
594
- requirementsComplete = checked ? checked.length : 0;
595
- requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
596
- }
597
- } catch (err) {
598
- logger.warn('Failed to parse REQUIREMENTS.md in cmdStats', { reqPath, error: err.message });
599
- }
600
-
601
- // Last activity from STATE.md
602
- let lastActivity = null;
603
- try {
604
- if (fs.existsSync(statePath)) {
605
- const stateContent = fs.readFileSync(statePath, 'utf-8');
606
- const activityMatch = stateContent.match(/\*\*Last Activity:\*\*\s*(.+)/);
607
- if (activityMatch) lastActivity = activityMatch[1].trim();
608
- }
609
- } catch (err) {
610
- logger.warn('Failed to read STATE.md in cmdStats', { statePath, error: err.message });
611
- }
612
-
613
- // Git stats
614
- let gitCommits = 0;
615
- let gitFirstCommitDate = null;
616
- try {
617
- const commitCount = await execGit(cwd, ['rev-list', '--count', 'HEAD']);
618
- gitCommits = parseInt(commitCount.trim(), 10) || 0;
619
- const firstDate = await execGit(cwd, ['log', '--reverse', '--format=%as', '--max-count=1']);
620
- gitFirstCommitDate = firstDate.trim() || null;
621
- } catch (err) {
622
- logger.warn('Failed to compute git stats in cmdStats', { cwd, error: err.message });
623
- }
624
-
625
- const completedPhases = phases.filter(p => p.status === 'Complete').length;
626
-
627
- const result = {
628
- milestone_version: milestone.version,
629
- milestone_name: milestone.name,
630
- phases,
631
- phases_completed: completedPhases,
632
- phases_total: phases.length,
633
- total_plans: totalPlans,
634
- total_summaries: totalSummaries,
635
- percent,
636
- requirements_total: requirementsTotal,
637
- requirements_complete: requirementsComplete,
638
- git_commits: gitCommits,
639
- git_first_commit_date: gitFirstCommitDate,
640
- last_activity: lastActivity,
641
- };
642
-
643
- if (format === 'table') {
644
- const barWidth = 10;
645
- const filled = Math.round((percent / 100) * barWidth);
646
- const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
647
- let out = `# ${milestone.version} ${milestone.name} \u2014 Statistics\n\n`;
648
- out += `**Progress:** [${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)\n`;
649
- out += `**Phases:** ${completedPhases}/${phases.length} complete\n`;
650
- if (requirementsTotal > 0) {
651
- out += `**Requirements:** ${requirementsComplete}/${requirementsTotal} complete\n`;
652
- }
653
- out += '\n';
654
- out += `| Phase | Name | Plans | Completed | Status |\n`;
655
- out += `|-------|------|-------|-----------|--------|\n`;
656
- for (const p of phases) {
657
- out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
658
- }
659
- if (gitCommits > 0) {
660
- out += `\n**Git:** ${gitCommits} commits`;
661
- if (gitFirstCommitDate) out += ` (since ${gitFirstCommitDate})`;
662
- out += '\n';
663
- }
664
- if (lastActivity) out += `**Last activity:** ${lastActivity}\n`;
665
- output({ rendered: out }, raw, out);
666
- } else {
667
- output(result, raw);
668
- }
669
- }
670
-
671
- module.exports = {
672
- cmdGenerateSlug,
673
- cmdCurrentTimestamp,
674
- cmdListTodos,
675
- cmdVerifyPathExists,
676
- cmdHistoryDigest,
677
- cmdResolveModel,
678
- cmdCommit,
679
- cmdSummaryExtract,
680
- cmdWebsearch,
681
- cmdProgressRender,
682
- cmdTodoComplete,
683
- cmdScaffold,
684
- cmdStats,
685
- };
1
+ /**
2
+ * Commands — Standalone utility commands
3
+ */
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
7
+ const { extractFrontmatter } = require('./frontmatter.cjs');
8
+ const { defaultLogger: logger } = require('./logger.cjs');
9
+
10
+ function cmdGenerateSlug(text, raw) {
11
+ if (!text) {
12
+ error('text required for slug generation');
13
+ }
14
+
15
+ const slug = text
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, '-')
18
+ .replace(/^-+|-+$/g, '');
19
+
20
+ const result = { slug };
21
+ output(result, raw, slug);
22
+ }
23
+
24
+ function cmdCurrentTimestamp(format, raw) {
25
+ const now = new Date();
26
+ let result;
27
+
28
+ switch (format) {
29
+ case 'date':
30
+ result = now.toISOString().split('T')[0];
31
+ break;
32
+ case 'filename':
33
+ result = now.toISOString().replace(/:/g, '-').replace(/\..+/, '');
34
+ break;
35
+ case 'full':
36
+ default:
37
+ result = now.toISOString();
38
+ break;
39
+ }
40
+
41
+ output({ timestamp: result }, raw, result);
42
+ }
43
+
44
+ function cmdListTodos(cwd, area, raw) {
45
+ const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
46
+
47
+ let count = 0;
48
+ const todos = [];
49
+
50
+ try {
51
+ const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
52
+
53
+ for (const file of files) {
54
+ try {
55
+ const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
56
+ const createdMatch = content.match(/^created:\s*(.+)$/m);
57
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
58
+ const areaMatch = content.match(/^area:\s*(.+)$/m);
59
+
60
+ const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
61
+
62
+ // Apply area filter if specified
63
+ if (area && todoArea !== area) continue;
64
+
65
+ count++;
66
+ todos.push({
67
+ file,
68
+ created: createdMatch ? createdMatch[1].trim() : 'unknown',
69
+ title: titleMatch ? titleMatch[1].trim() : 'Untitled',
70
+ area: todoArea,
71
+ path: toPosixPath(path.join('.planning', 'todos', 'pending', file)),
72
+ });
73
+ } catch (err) {
74
+ logger.warn('Failed to parse todo file in cmdListTodos', { file, error: err.message });
75
+ }
76
+ }
77
+ } catch (err) {
78
+ logger.warn('Failed to list pending todos in cmdListTodos', { pendingDir, error: err.message });
79
+ }
80
+
81
+ const result = { count, todos };
82
+ output(result, raw, count.toString());
83
+ }
84
+
85
+ function cmdVerifyPathExists(cwd, targetPath, raw) {
86
+ if (!targetPath) {
87
+ error('path required for verification');
88
+ }
89
+
90
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
91
+
92
+ try {
93
+ const stats = fs.statSync(fullPath);
94
+ const type = stats.isDirectory() ? 'directory' : stats.isFile() ? 'file' : 'other';
95
+ const result = { exists: true, type };
96
+ output(result, raw, 'true');
97
+ } catch (err) {
98
+ logger.warn('Path verification failed in cmdVerifyPathExists', { fullPath, error: err.message });
99
+ const result = { exists: false, type: null };
100
+ output(result, raw, 'false');
101
+ }
102
+ }
103
+
104
+ function cmdHistoryDigest(cwd, raw) {
105
+ const phasesDir = path.join(cwd, '.planning', 'phases');
106
+ const digest = { phases: {}, decisions: [], tech_stack: new Set() };
107
+
108
+ // Collect all phase directories: archived + current
109
+ const allPhaseDirs = [];
110
+
111
+ // Add archived phases first (oldest milestones first)
112
+ const archived = getArchivedPhaseDirs(cwd);
113
+ for (const a of archived) {
114
+ allPhaseDirs.push({ name: a.name, fullPath: a.fullPath, milestone: a.milestone });
115
+ }
116
+
117
+ // Add current phases
118
+ if (fs.existsSync(phasesDir)) {
119
+ try {
120
+ const currentDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
121
+ .filter(e => e.isDirectory())
122
+ .map(e => e.name)
123
+ .sort();
124
+ for (const dir of currentDirs) {
125
+ allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null });
126
+ }
127
+ } catch (err) {
128
+ logger.warn('Failed to enumerate current phase directories in cmdHistoryDigest', { phasesDir, error: err.message });
129
+ }
130
+ }
131
+
132
+ if (allPhaseDirs.length === 0) {
133
+ digest.tech_stack = [];
134
+ output(digest, raw);
135
+ return;
136
+ }
137
+
138
+ try {
139
+ for (const { name: dir, fullPath: dirPath } of allPhaseDirs) {
140
+ const summaries = fs.readdirSync(dirPath).filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
141
+
142
+ for (const summary of summaries) {
143
+ try {
144
+ const content = fs.readFileSync(path.join(dirPath, summary), 'utf-8');
145
+ const fm = extractFrontmatter(content);
146
+
147
+ const phaseNum = fm.phase || dir.split('-')[0];
148
+
149
+ if (!digest.phases[phaseNum]) {
150
+ digest.phases[phaseNum] = {
151
+ name: fm.name || dir.split('-').slice(1).join(' ') || 'Unknown',
152
+ provides: new Set(),
153
+ affects: new Set(),
154
+ patterns: new Set(),
155
+ };
156
+ }
157
+
158
+ // Merge provides
159
+ if (fm['dependency-graph'] && fm['dependency-graph'].provides) {
160
+ fm['dependency-graph'].provides.forEach(p => digest.phases[phaseNum].provides.add(p));
161
+ } else if (fm.provides) {
162
+ fm.provides.forEach(p => digest.phases[phaseNum].provides.add(p));
163
+ }
164
+
165
+ // Merge affects
166
+ if (fm['dependency-graph'] && fm['dependency-graph'].affects) {
167
+ fm['dependency-graph'].affects.forEach(a => digest.phases[phaseNum].affects.add(a));
168
+ }
169
+
170
+ // Merge patterns
171
+ if (fm['patterns-established']) {
172
+ fm['patterns-established'].forEach(p => digest.phases[phaseNum].patterns.add(p));
173
+ }
174
+
175
+ // Merge decisions
176
+ if (fm['key-decisions']) {
177
+ fm['key-decisions'].forEach(d => {
178
+ digest.decisions.push({ phase: phaseNum, decision: d });
179
+ });
180
+ }
181
+
182
+ // Merge tech stack
183
+ if (fm['tech-stack'] && fm['tech-stack'].added) {
184
+ fm['tech-stack'].added.forEach(t => digest.tech_stack.add(typeof t === 'string' ? t : t.name));
185
+ }
186
+
187
+ } catch (err) {
188
+ logger.warn('Skipping malformed summary in cmdHistoryDigest', { summary, dirPath, error: err.message });
189
+ }
190
+ }
191
+ }
192
+
193
+ // Convert Sets to Arrays for JSON output
194
+ Object.keys(digest.phases).forEach(p => {
195
+ digest.phases[p].provides = [...digest.phases[p].provides];
196
+ digest.phases[p].affects = [...digest.phases[p].affects];
197
+ digest.phases[p].patterns = [...digest.phases[p].patterns];
198
+ });
199
+ digest.tech_stack = [...digest.tech_stack];
200
+
201
+ output(digest, raw);
202
+ } catch (err) {
203
+ logger.error('Failed to generate history digest', { error: err.message });
204
+ error('Failed to generate history digest: ' + err.message);
205
+ }
206
+ }
207
+
208
+ function cmdResolveModel(cwd, agentType, raw) {
209
+ if (!agentType) {
210
+ error('agent-type required');
211
+ }
212
+
213
+ const config = loadConfig(cwd);
214
+ const profile = config.model_profile || 'balanced';
215
+ const model = resolveModelInternal(cwd, agentType);
216
+
217
+ const agentModels = MODEL_PROFILES[agentType];
218
+ const result = agentModels
219
+ ? { model, profile }
220
+ : { model, profile, unknown_agent: true };
221
+ output(result, raw, model);
222
+ }
223
+
224
+ async function cmdCommit(cwd, message, files, raw, amend) {
225
+ if (!message && !amend) {
226
+ error('commit message required');
227
+ }
228
+
229
+ const config = loadConfig(cwd);
230
+
231
+ // Check commit_docs config
232
+ if (!config.commit_docs) {
233
+ const result = { committed: false, hash: null, reason: 'skipped_commit_docs_false' };
234
+ output(result, raw, 'skipped');
235
+ return;
236
+ }
237
+
238
+ // Check if .planning is gitignored
239
+ if (await isGitIgnored(cwd, '.planning')) {
240
+ const result = { committed: false, hash: null, reason: 'skipped_gitignored' };
241
+ output(result, raw, 'skipped');
242
+ return;
243
+ }
244
+
245
+ // Stage files
246
+ const filesToStage = files && files.length > 0 ? files : ['.planning/'];
247
+ for (const file of filesToStage) {
248
+ await execGit(cwd, ['add', file]);
249
+ }
250
+
251
+ // Commit
252
+ const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
253
+ const commitResult = await execGit(cwd, commitArgs);
254
+ if (commitResult.exitCode !== 0) {
255
+ if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
256
+ const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
257
+ output(result, raw, 'nothing');
258
+ return;
259
+ }
260
+ const result = { committed: false, hash: null, reason: 'nothing_to_commit', error: commitResult.stderr };
261
+ output(result, raw, 'nothing');
262
+ return;
263
+ }
264
+
265
+ // Get short hash
266
+ const hashResult = await execGit(cwd, ['rev-parse', '--short', 'HEAD']);
267
+ const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
268
+ const result = { committed: true, hash, reason: 'committed' };
269
+ output(result, raw, hash || 'committed');
270
+ }
271
+
272
+ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
273
+ if (!summaryPath) {
274
+ error('summary-path required for summary-extract');
275
+ }
276
+
277
+ const fullPath = path.join(cwd, summaryPath);
278
+
279
+ if (!fs.existsSync(fullPath)) {
280
+ output({ error: 'File not found', path: summaryPath }, raw);
281
+ return;
282
+ }
283
+
284
+ const content = fs.readFileSync(fullPath, 'utf-8');
285
+ const fm = extractFrontmatter(content);
286
+
287
+ // Parse key-decisions into structured format
288
+ const parseDecisions = (decisionsList) => {
289
+ if (!decisionsList || !Array.isArray(decisionsList)) return [];
290
+ return decisionsList.map(d => {
291
+ const colonIdx = d.indexOf(':');
292
+ if (colonIdx > 0) {
293
+ return {
294
+ summary: d.substring(0, colonIdx).trim(),
295
+ rationale: d.substring(colonIdx + 1).trim(),
296
+ };
297
+ }
298
+ return { summary: d, rationale: null };
299
+ });
300
+ };
301
+
302
+ // Build full result
303
+ const fullResult = {
304
+ path: summaryPath,
305
+ one_liner: fm['one-liner'] || null,
306
+ key_files: fm['key-files'] || [],
307
+ tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
308
+ patterns: fm['patterns-established'] || [],
309
+ decisions: parseDecisions(fm['key-decisions']),
310
+ requirements_completed: fm['requirements-completed'] || [],
311
+ };
312
+
313
+ // If fields specified, filter to only those fields
314
+ if (fields && fields.length > 0) {
315
+ const filtered = { path: summaryPath };
316
+ for (const field of fields) {
317
+ if (fullResult[field] !== undefined) {
318
+ filtered[field] = fullResult[field];
319
+ }
320
+ }
321
+ output(filtered, raw);
322
+ return;
323
+ }
324
+
325
+ output(fullResult, raw);
326
+ }
327
+
328
+ async function cmdWebsearch(query, options, raw) {
329
+ const apiKey = process.env.BRAVE_API_KEY;
330
+
331
+ if (!apiKey) {
332
+ // No key = silent skip, agent falls back to built-in WebSearch
333
+ output({ available: false, reason: 'BRAVE_API_KEY not set' }, raw, '');
334
+ return;
335
+ }
336
+
337
+ if (!query) {
338
+ output({ available: false, error: 'Query required' }, raw, '');
339
+ return;
340
+ }
341
+
342
+ const params = new URLSearchParams({
343
+ q: query,
344
+ count: String(options.limit || 10),
345
+ country: 'us',
346
+ search_lang: 'en',
347
+ text_decorations: 'false'
348
+ });
349
+
350
+ if (options.freshness) {
351
+ params.set('freshness', options.freshness);
352
+ }
353
+
354
+ try {
355
+ const response = await fetch(
356
+ `https://api.search.brave.com/res/v1/web/search?${params}`,
357
+ {
358
+ headers: {
359
+ 'Accept': 'application/json',
360
+ 'X-Subscription-Token': apiKey
361
+ }
362
+ }
363
+ );
364
+
365
+ if (!response.ok) {
366
+ output({ available: false, error: `API error: ${response.status}` }, raw, '');
367
+ return;
368
+ }
369
+
370
+ const data = await response.json();
371
+
372
+ const results = (data.web?.results || []).map(r => ({
373
+ title: r.title,
374
+ url: r.url,
375
+ description: r.description,
376
+ age: r.age || null
377
+ }));
378
+
379
+ output({
380
+ available: true,
381
+ query,
382
+ count: results.length,
383
+ results
384
+ }, raw, results.map(r => `${r.title}\n${r.url}\n${r.description}`).join('\n\n'));
385
+ } catch (err) {
386
+ logger.warn('Websearch request failed in cmdWebsearch', { query, error: err.message });
387
+ output({ available: false, error: err.message }, raw, '');
388
+ }
389
+ }
390
+
391
+ function cmdProgressRender(cwd, format, raw) {
392
+ const phasesDir = path.join(cwd, '.planning', 'phases');
393
+ const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
394
+ const milestone = getMilestoneInfo(cwd);
395
+
396
+ const phases = [];
397
+ let totalPlans = 0;
398
+ let totalSummaries = 0;
399
+
400
+ try {
401
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
402
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
403
+
404
+ for (const dir of dirs) {
405
+ const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
406
+ const phaseNum = dm ? dm[1] : dir;
407
+ const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
408
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
409
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
410
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
411
+
412
+ totalPlans += plans;
413
+ totalSummaries += summaries;
414
+
415
+ let status;
416
+ if (plans === 0) status = 'Pending';
417
+ else if (summaries >= plans) status = 'Complete';
418
+ else if (summaries > 0) status = 'In Progress';
419
+ else status = 'Planned';
420
+
421
+ phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
422
+ }
423
+ } catch (err) {
424
+ logger.warn('Failed to enumerate phase directories in cmdProgressRender', { phasesDir, error: err.message });
425
+ }
426
+
427
+ const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
428
+
429
+ if (format === 'table') {
430
+ // Render markdown table
431
+ const barWidth = 10;
432
+ const filled = Math.round((percent / 100) * barWidth);
433
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
434
+ let out = `# ${milestone.version} ${milestone.name}\n\n`;
435
+ out += `**Progress:** [${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)\n\n`;
436
+ out += `| Phase | Name | Plans | Status |\n`;
437
+ out += `|-------|------|-------|--------|\n`;
438
+ for (const p of phases) {
439
+ out += `| ${p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`;
440
+ }
441
+ output({ rendered: out }, raw, out);
442
+ } else if (format === 'bar') {
443
+ const barWidth = 20;
444
+ const filled = Math.round((percent / 100) * barWidth);
445
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
446
+ const text = `[${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)`;
447
+ output({ bar: text, percent, completed: totalSummaries, total: totalPlans }, raw, text);
448
+ } else {
449
+ // JSON format
450
+ output({
451
+ milestone_version: milestone.version,
452
+ milestone_name: milestone.name,
453
+ phases,
454
+ total_plans: totalPlans,
455
+ total_summaries: totalSummaries,
456
+ percent,
457
+ }, raw);
458
+ }
459
+ }
460
+
461
+ function cmdTodoComplete(cwd, filename, raw) {
462
+ if (!filename) {
463
+ error('filename required for todo complete');
464
+ }
465
+
466
+ const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
467
+ const completedDir = path.join(cwd, '.planning', 'todos', 'completed');
468
+ const sourcePath = path.join(pendingDir, filename);
469
+
470
+ if (!fs.existsSync(sourcePath)) {
471
+ error(`Todo not found: ${filename}`);
472
+ }
473
+
474
+ // Ensure completed directory exists
475
+ fs.mkdirSync(completedDir, { recursive: true });
476
+
477
+ // Read, add completion timestamp, move
478
+ let content = fs.readFileSync(sourcePath, 'utf-8');
479
+ const today = new Date().toISOString().split('T')[0];
480
+ content = `completed: ${today}\n` + content;
481
+
482
+ fs.writeFileSync(path.join(completedDir, filename), content, 'utf-8');
483
+ fs.unlinkSync(sourcePath);
484
+
485
+ output({ completed: true, file: filename, date: today }, raw, 'completed');
486
+ }
487
+
488
+ function cmdScaffold(cwd, type, options, raw) {
489
+ const { phase, name } = options;
490
+ const padded = phase ? normalizePhaseName(phase) : '00';
491
+ const today = new Date().toISOString().split('T')[0];
492
+
493
+ // Find phase directory
494
+ const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
495
+ const phaseDir = phaseInfo ? path.join(cwd, phaseInfo.directory) : null;
496
+
497
+ if (phase && !phaseDir && type !== 'phase-dir') {
498
+ error(`Phase ${phase} directory not found`);
499
+ }
500
+
501
+ let filePath, content;
502
+
503
+ switch (type) {
504
+ case 'context': {
505
+ filePath = path.join(phaseDir, `${padded}-CONTEXT.md`);
506
+ content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during /ez-discuss-phase ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
507
+ break;
508
+ }
509
+ case 'uat': {
510
+ filePath = path.join(phaseDir, `${padded}-UAT.md`);
511
+ content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — User Acceptance Testing\n\n## Test Results\n\n| # | Test | Status | Notes |\n|---|------|--------|-------|\n\n## Summary\n\n_Pending UAT_\n`;
512
+ break;
513
+ }
514
+ case 'verification': {
515
+ filePath = path.join(phaseDir, `${padded}-VERIFICATION.md`);
516
+ content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Verification\n\n## Goal-Backward Verification\n\n**Phase Goal:** [From ROADMAP.md]\n\n## Checks\n\n| # | Requirement | Status | Evidence |\n|---|------------|--------|----------|\n\n## Result\n\n_Pending verification_\n`;
517
+ break;
518
+ }
519
+ case 'phase-dir': {
520
+ if (!phase || !name) {
521
+ error('phase and name required for phase-dir scaffold');
522
+ }
523
+ const slug = generateSlugInternal(name);
524
+ const dirName = `${padded}-${slug}`;
525
+ const phasesParent = path.join(cwd, '.planning', 'phases');
526
+ fs.mkdirSync(phasesParent, { recursive: true });
527
+ const dirPath = path.join(phasesParent, dirName);
528
+ fs.mkdirSync(dirPath, { recursive: true });
529
+ output({ created: true, directory: `.planning/phases/${dirName}`, path: dirPath }, raw, dirPath);
530
+ return;
531
+ }
532
+ default:
533
+ error(`Unknown scaffold type: ${type}. Available: context, uat, verification, phase-dir`);
534
+ }
535
+
536
+ if (fs.existsSync(filePath)) {
537
+ output({ created: false, reason: 'already_exists', path: filePath }, raw, 'exists');
538
+ return;
539
+ }
540
+
541
+ fs.writeFileSync(filePath, content, 'utf-8');
542
+ const relPath = toPosixPath(path.relative(cwd, filePath));
543
+ output({ created: true, path: relPath }, raw, relPath);
544
+ }
545
+
546
+ async function cmdStats(cwd, format, raw) {
547
+ const phasesDir = path.join(cwd, '.planning', 'phases');
548
+ const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
549
+ const statePath = path.join(cwd, '.planning', 'STATE.md');
550
+ const milestone = getMilestoneInfo(cwd);
551
+
552
+ // Phase & plan stats (reuse progress pattern)
553
+ const phases = [];
554
+ let totalPlans = 0;
555
+ let totalSummaries = 0;
556
+
557
+ try {
558
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
559
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
560
+
561
+ for (const dir of dirs) {
562
+ const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
563
+ const phaseNum = dm ? dm[1] : dir;
564
+ const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
565
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
566
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
567
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
568
+
569
+ totalPlans += plans;
570
+ totalSummaries += summaries;
571
+
572
+ let status;
573
+ if (plans === 0) status = 'Pending';
574
+ else if (summaries >= plans) status = 'Complete';
575
+ else if (summaries > 0) status = 'In Progress';
576
+ else status = 'Planned';
577
+
578
+ phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
579
+ }
580
+ } catch (err) {
581
+ logger.warn('Failed to enumerate phase directories in cmdStats', { phasesDir, error: err.message });
582
+ }
583
+
584
+ const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
585
+
586
+ // Requirements stats
587
+ let requirementsTotal = 0;
588
+ let requirementsComplete = 0;
589
+ try {
590
+ if (fs.existsSync(reqPath)) {
591
+ const reqContent = fs.readFileSync(reqPath, 'utf-8');
592
+ const checked = reqContent.match(/^- \[x\] \*\*/gm);
593
+ const unchecked = reqContent.match(/^- \[ \] \*\*/gm);
594
+ requirementsComplete = checked ? checked.length : 0;
595
+ requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
596
+ }
597
+ } catch (err) {
598
+ logger.warn('Failed to parse REQUIREMENTS.md in cmdStats', { reqPath, error: err.message });
599
+ }
600
+
601
+ // Last activity from STATE.md
602
+ let lastActivity = null;
603
+ try {
604
+ if (fs.existsSync(statePath)) {
605
+ const stateContent = fs.readFileSync(statePath, 'utf-8');
606
+ const activityMatch = stateContent.match(/\*\*Last Activity:\*\*\s*(.+)/);
607
+ if (activityMatch) lastActivity = activityMatch[1].trim();
608
+ }
609
+ } catch (err) {
610
+ logger.warn('Failed to read STATE.md in cmdStats', { statePath, error: err.message });
611
+ }
612
+
613
+ // Git stats
614
+ let gitCommits = 0;
615
+ let gitFirstCommitDate = null;
616
+ try {
617
+ const commitCount = await execGit(cwd, ['rev-list', '--count', 'HEAD']);
618
+ gitCommits = parseInt(commitCount.trim(), 10) || 0;
619
+ const firstDate = await execGit(cwd, ['log', '--reverse', '--format=%as', '--max-count=1']);
620
+ gitFirstCommitDate = firstDate.trim() || null;
621
+ } catch (err) {
622
+ logger.warn('Failed to compute git stats in cmdStats', { cwd, error: err.message });
623
+ }
624
+
625
+ const completedPhases = phases.filter(p => p.status === 'Complete').length;
626
+
627
+ const result = {
628
+ milestone_version: milestone.version,
629
+ milestone_name: milestone.name,
630
+ phases,
631
+ phases_completed: completedPhases,
632
+ phases_total: phases.length,
633
+ total_plans: totalPlans,
634
+ total_summaries: totalSummaries,
635
+ percent,
636
+ requirements_total: requirementsTotal,
637
+ requirements_complete: requirementsComplete,
638
+ git_commits: gitCommits,
639
+ git_first_commit_date: gitFirstCommitDate,
640
+ last_activity: lastActivity,
641
+ };
642
+
643
+ if (format === 'table') {
644
+ const barWidth = 10;
645
+ const filled = Math.round((percent / 100) * barWidth);
646
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
647
+ let out = `# ${milestone.version} ${milestone.name} \u2014 Statistics\n\n`;
648
+ out += `**Progress:** [${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)\n`;
649
+ out += `**Phases:** ${completedPhases}/${phases.length} complete\n`;
650
+ if (requirementsTotal > 0) {
651
+ out += `**Requirements:** ${requirementsComplete}/${requirementsTotal} complete\n`;
652
+ }
653
+ out += '\n';
654
+ out += `| Phase | Name | Plans | Completed | Status |\n`;
655
+ out += `|-------|------|-------|-----------|--------|\n`;
656
+ for (const p of phases) {
657
+ out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
658
+ }
659
+ if (gitCommits > 0) {
660
+ out += `\n**Git:** ${gitCommits} commits`;
661
+ if (gitFirstCommitDate) out += ` (since ${gitFirstCommitDate})`;
662
+ out += '\n';
663
+ }
664
+ if (lastActivity) out += `**Last activity:** ${lastActivity}\n`;
665
+ output({ rendered: out }, raw, out);
666
+ } else {
667
+ output(result, raw);
668
+ }
669
+ }
670
+
671
+ module.exports = {
672
+ cmdGenerateSlug,
673
+ cmdCurrentTimestamp,
674
+ cmdListTodos,
675
+ cmdVerifyPathExists,
676
+ cmdHistoryDigest,
677
+ cmdResolveModel,
678
+ cmdCommit,
679
+ cmdSummaryExtract,
680
+ cmdWebsearch,
681
+ cmdProgressRender,
682
+ cmdTodoComplete,
683
+ cmdScaffold,
684
+ cmdStats,
685
+ };