qualia-framework 2.5.1 → 3.1.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 (327) hide show
  1. package/CLAUDE.md +63 -0
  2. package/README.md +108 -30
  3. package/agents/builder.md +110 -0
  4. package/agents/planner.md +186 -0
  5. package/agents/qa-browser.md +186 -0
  6. package/agents/verifier.md +369 -0
  7. package/bin/cli.js +706 -417
  8. package/bin/install.js +622 -0
  9. package/bin/qualia-ui.js +284 -0
  10. package/bin/state.js +824 -0
  11. package/bin/statusline.js +252 -0
  12. package/docs/erp-contract.md +161 -0
  13. package/guide.md +63 -0
  14. package/hooks/auto-update.js +117 -0
  15. package/hooks/block-env-edit.js +52 -0
  16. package/hooks/branch-guard.js +68 -0
  17. package/hooks/migration-guard.js +83 -0
  18. package/hooks/pre-compact.js +52 -0
  19. package/hooks/pre-deploy-gate.js +149 -0
  20. package/hooks/pre-push.js +53 -0
  21. package/hooks/session-start.js +126 -0
  22. package/package.json +31 -17
  23. package/rules/design-reference.md +179 -0
  24. package/rules/frontend.md +126 -0
  25. package/rules/infrastructure.md +87 -0
  26. package/skills/qualia/SKILL.md +88 -0
  27. package/skills/qualia-build/SKILL.md +115 -0
  28. package/skills/qualia-debug/SKILL.md +87 -0
  29. package/skills/qualia-design/SKILL.md +99 -0
  30. package/skills/qualia-handoff/SKILL.md +66 -0
  31. package/skills/qualia-help/SKILL.md +60 -0
  32. package/skills/qualia-idk/SKILL.md +8 -0
  33. package/skills/qualia-learn/SKILL.md +111 -0
  34. package/skills/qualia-new/SKILL.md +323 -0
  35. package/skills/qualia-pause/SKILL.md +63 -0
  36. package/skills/qualia-plan/SKILL.md +101 -0
  37. package/skills/qualia-polish/SKILL.md +207 -0
  38. package/skills/qualia-quick/SKILL.md +37 -0
  39. package/skills/qualia-report/SKILL.md +114 -0
  40. package/skills/qualia-resume/SKILL.md +49 -0
  41. package/skills/qualia-review/SKILL.md +161 -0
  42. package/skills/qualia-ship/SKILL.md +90 -0
  43. package/skills/qualia-skill-new/SKILL.md +167 -0
  44. package/skills/qualia-task/SKILL.md +91 -0
  45. package/skills/qualia-test/SKILL.md +134 -0
  46. package/skills/qualia-verify/SKILL.md +113 -0
  47. package/templates/DESIGN.md +475 -0
  48. package/templates/help.html +476 -0
  49. package/templates/plan.md +42 -0
  50. package/templates/project.md +22 -0
  51. package/templates/state.md +27 -0
  52. package/templates/tracking.json +20 -0
  53. package/tests/bin.test.sh +687 -0
  54. package/tests/hooks.test.sh +384 -0
  55. package/tests/runner.js +1956 -0
  56. package/tests/state.test.sh +713 -0
  57. package/tests/statusline.test.sh +243 -0
  58. package/bin/collect-metrics.sh +0 -62
  59. package/framework/.claudeignore +0 -51
  60. package/framework/CLAUDE.md +0 -51
  61. package/framework/MCP_SETUP.md +0 -229
  62. package/framework/agents/architecture-strategist.md +0 -53
  63. package/framework/agents/backend-agent.md +0 -150
  64. package/framework/agents/code-simplicity-reviewer.md +0 -86
  65. package/framework/agents/frontend-agent.md +0 -111
  66. package/framework/agents/kieran-typescript-reviewer.md +0 -96
  67. package/framework/agents/performance-oracle.md +0 -111
  68. package/framework/agents/qualia-codebase-mapper.md +0 -761
  69. package/framework/agents/qualia-debugger.md +0 -1204
  70. package/framework/agents/qualia-executor.md +0 -882
  71. package/framework/agents/qualia-integration-checker.md +0 -424
  72. package/framework/agents/qualia-phase-researcher.md +0 -457
  73. package/framework/agents/qualia-plan-checker.md +0 -700
  74. package/framework/agents/qualia-planner.md +0 -1245
  75. package/framework/agents/qualia-project-researcher.md +0 -603
  76. package/framework/agents/qualia-research-synthesizer.md +0 -200
  77. package/framework/agents/qualia-roadmapper.md +0 -606
  78. package/framework/agents/qualia-verifier.md +0 -686
  79. package/framework/agents/red-team-qa.md +0 -130
  80. package/framework/agents/security-auditor.md +0 -72
  81. package/framework/agents/team-orchestrator.md +0 -229
  82. package/framework/agents/teams/framework-audit-team.md +0 -66
  83. package/framework/agents/teams/full-stack-team.md +0 -48
  84. package/framework/agents/teams/optimize-team.md +0 -53
  85. package/framework/agents/teams/review-team.md +0 -70
  86. package/framework/agents/teams/ship-team.md +0 -86
  87. package/framework/agents/test-agent.md +0 -182
  88. package/framework/hooks/auto-format.sh +0 -54
  89. package/framework/hooks/block-env-edit.sh +0 -42
  90. package/framework/hooks/branch-guard.sh +0 -43
  91. package/framework/hooks/confirm-delete.sh +0 -59
  92. package/framework/hooks/migration-validate.sh +0 -77
  93. package/framework/hooks/notification-speak.sh +0 -16
  94. package/framework/hooks/pre-commit.sh +0 -100
  95. package/framework/hooks/pre-compact.sh +0 -56
  96. package/framework/hooks/pre-deploy-gate.sh +0 -160
  97. package/framework/hooks/qualia-colors.sh +0 -32
  98. package/framework/hooks/retention-cleanup.sh +0 -62
  99. package/framework/hooks/save-session-state.sh +0 -185
  100. package/framework/hooks/session-context-loader.sh +0 -96
  101. package/framework/hooks/session-learn.sh +0 -32
  102. package/framework/hooks/skill-announce.sh +0 -123
  103. package/framework/hooks/tool-error-announce.sh +0 -27
  104. package/framework/install.ps1 +0 -323
  105. package/framework/install.sh +0 -313
  106. package/framework/qualia-framework/VERSION +0 -1
  107. package/framework/qualia-framework/assets/qualia-logo.png +0 -0
  108. package/framework/qualia-framework/bin/collect-metrics.sh +0 -67
  109. package/framework/qualia-framework/bin/generate-report-docx.py +0 -429
  110. package/framework/qualia-framework/bin/qualia-tools.js +0 -2201
  111. package/framework/qualia-framework/bin/qualia-tools.test.js +0 -1054
  112. package/framework/qualia-framework/references/checkpoints.md +0 -775
  113. package/framework/qualia-framework/references/completion-checklists.md +0 -359
  114. package/framework/qualia-framework/references/continuation-format.md +0 -249
  115. package/framework/qualia-framework/references/continuation-prompt.md +0 -97
  116. package/framework/qualia-framework/references/decimal-phase-calculation.md +0 -65
  117. package/framework/qualia-framework/references/design-quality.md +0 -56
  118. package/framework/qualia-framework/references/employee-guide.md +0 -167
  119. package/framework/qualia-framework/references/git-integration.md +0 -254
  120. package/framework/qualia-framework/references/git-planning-commit.md +0 -50
  121. package/framework/qualia-framework/references/model-profile-resolution.md +0 -32
  122. package/framework/qualia-framework/references/model-profiles.md +0 -73
  123. package/framework/qualia-framework/references/phase-argument-parsing.md +0 -61
  124. package/framework/qualia-framework/references/planning-config.md +0 -195
  125. package/framework/qualia-framework/references/questioning.md +0 -141
  126. package/framework/qualia-framework/references/tdd.md +0 -263
  127. package/framework/qualia-framework/references/ui-brand.md +0 -160
  128. package/framework/qualia-framework/references/verification-patterns.md +0 -612
  129. package/framework/qualia-framework/templates/DEBUG.md +0 -159
  130. package/framework/qualia-framework/templates/DESIGN.md +0 -81
  131. package/framework/qualia-framework/templates/UAT.md +0 -247
  132. package/framework/qualia-framework/templates/codebase/architecture.md +0 -255
  133. package/framework/qualia-framework/templates/codebase/concerns.md +0 -310
  134. package/framework/qualia-framework/templates/codebase/conventions.md +0 -307
  135. package/framework/qualia-framework/templates/codebase/integrations.md +0 -280
  136. package/framework/qualia-framework/templates/codebase/stack.md +0 -186
  137. package/framework/qualia-framework/templates/codebase/structure.md +0 -285
  138. package/framework/qualia-framework/templates/codebase/testing.md +0 -480
  139. package/framework/qualia-framework/templates/config.json +0 -35
  140. package/framework/qualia-framework/templates/context.md +0 -283
  141. package/framework/qualia-framework/templates/continue-here.md +0 -78
  142. package/framework/qualia-framework/templates/debug-subagent-prompt.md +0 -91
  143. package/framework/qualia-framework/templates/discovery.md +0 -146
  144. package/framework/qualia-framework/templates/lab-notes.md +0 -16
  145. package/framework/qualia-framework/templates/milestone-archive.md +0 -123
  146. package/framework/qualia-framework/templates/milestone.md +0 -115
  147. package/framework/qualia-framework/templates/phase-prompt.md +0 -567
  148. package/framework/qualia-framework/templates/planner-subagent-prompt.md +0 -117
  149. package/framework/qualia-framework/templates/project.md +0 -184
  150. package/framework/qualia-framework/templates/projects/ai-agent.md +0 -156
  151. package/framework/qualia-framework/templates/projects/mobile-app.md +0 -181
  152. package/framework/qualia-framework/templates/projects/voice-agent.md +0 -134
  153. package/framework/qualia-framework/templates/projects/website.md +0 -137
  154. package/framework/qualia-framework/templates/requirements.md +0 -231
  155. package/framework/qualia-framework/templates/research-project/ARCHITECTURE.md +0 -204
  156. package/framework/qualia-framework/templates/research-project/FEATURES.md +0 -147
  157. package/framework/qualia-framework/templates/research-project/PITFALLS.md +0 -200
  158. package/framework/qualia-framework/templates/research-project/STACK.md +0 -120
  159. package/framework/qualia-framework/templates/research-project/SUMMARY.md +0 -170
  160. package/framework/qualia-framework/templates/research.md +0 -552
  161. package/framework/qualia-framework/templates/roadmap.md +0 -206
  162. package/framework/qualia-framework/templates/state.md +0 -179
  163. package/framework/qualia-framework/templates/summary-complex.md +0 -59
  164. package/framework/qualia-framework/templates/summary-minimal.md +0 -41
  165. package/framework/qualia-framework/templates/summary-standard.md +0 -48
  166. package/framework/qualia-framework/templates/summary.md +0 -246
  167. package/framework/qualia-framework/templates/user-setup.md +0 -311
  168. package/framework/qualia-framework/templates/verification-report.md +0 -322
  169. package/framework/qualia-framework/workflows/add-phase.md +0 -179
  170. package/framework/qualia-framework/workflows/add-todo.md +0 -157
  171. package/framework/qualia-framework/workflows/audit-milestone.md +0 -241
  172. package/framework/qualia-framework/workflows/check-todos.md +0 -176
  173. package/framework/qualia-framework/workflows/complete-milestone.md +0 -858
  174. package/framework/qualia-framework/workflows/diagnose-issues.md +0 -219
  175. package/framework/qualia-framework/workflows/discovery-phase.md +0 -289
  176. package/framework/qualia-framework/workflows/discuss-phase.md +0 -534
  177. package/framework/qualia-framework/workflows/execute-phase.md +0 -559
  178. package/framework/qualia-framework/workflows/execute-plan.md +0 -438
  179. package/framework/qualia-framework/workflows/help.md +0 -470
  180. package/framework/qualia-framework/workflows/insert-phase.md +0 -220
  181. package/framework/qualia-framework/workflows/list-phase-assumptions.md +0 -178
  182. package/framework/qualia-framework/workflows/map-codebase.md +0 -327
  183. package/framework/qualia-framework/workflows/new-milestone.md +0 -363
  184. package/framework/qualia-framework/workflows/new-project.md +0 -982
  185. package/framework/qualia-framework/workflows/pause-work.md +0 -122
  186. package/framework/qualia-framework/workflows/plan-milestone-gaps.md +0 -256
  187. package/framework/qualia-framework/workflows/plan-phase.md +0 -422
  188. package/framework/qualia-framework/workflows/progress.md +0 -389
  189. package/framework/qualia-framework/workflows/quick.md +0 -252
  190. package/framework/qualia-framework/workflows/remove-phase.md +0 -326
  191. package/framework/qualia-framework/workflows/research-phase.md +0 -74
  192. package/framework/qualia-framework/workflows/resume-project.md +0 -306
  193. package/framework/qualia-framework/workflows/set-profile.md +0 -80
  194. package/framework/qualia-framework/workflows/settings.md +0 -145
  195. package/framework/qualia-framework/workflows/transition.md +0 -556
  196. package/framework/qualia-framework/workflows/update.md +0 -197
  197. package/framework/qualia-framework/workflows/verify-phase.md +0 -195
  198. package/framework/qualia-framework/workflows/verify-work.md +0 -625
  199. package/framework/rules/context7.md +0 -14
  200. package/framework/rules/frontend.md +0 -33
  201. package/framework/rules/speed.md +0 -23
  202. package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
  203. package/framework/scripts/apply-retention.sh +0 -120
  204. package/framework/scripts/bootstrap-pop-os.sh +0 -354
  205. package/framework/scripts/claude-voice +0 -13
  206. package/framework/scripts/cleanup.sh +0 -131
  207. package/framework/scripts/cowork-mode.sh +0 -141
  208. package/framework/scripts/generate-project-claude-md.sh +0 -153
  209. package/framework/scripts/load-test-webhook.js +0 -172
  210. package/framework/scripts/say.py +0 -236
  211. package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +0 -167
  212. package/framework/scripts/showcase-video-recorder/playwright-helpers.js +0 -216
  213. package/framework/scripts/speak.py +0 -55
  214. package/framework/scripts/speak.sh +0 -18
  215. package/framework/scripts/status.sh +0 -138
  216. package/framework/scripts/sync-to-framework.sh +0 -65
  217. package/framework/scripts/voice-hotkey.py +0 -227
  218. package/framework/scripts/voice-input.sh +0 -51
  219. package/framework/skills/animate/SKILL.md +0 -202
  220. package/framework/skills/bolder/SKILL.md +0 -144
  221. package/framework/skills/browser-qa/SKILL.md +0 -536
  222. package/framework/skills/clarify/SKILL.md +0 -179
  223. package/framework/skills/client-handoff/SKILL.md +0 -135
  224. package/framework/skills/collab-onboard/SKILL.md +0 -111
  225. package/framework/skills/colorize/SKILL.md +0 -170
  226. package/framework/skills/critique/SKILL.md +0 -126
  227. package/framework/skills/deep-research/SKILL.md +0 -240
  228. package/framework/skills/delight/SKILL.md +0 -329
  229. package/framework/skills/deploy/SKILL.md +0 -261
  230. package/framework/skills/deploy-verify/SKILL.md +0 -377
  231. package/framework/skills/deploy-verify/scripts/canary-check.sh +0 -206
  232. package/framework/skills/deploy-verify/scripts/check-console-errors.js +0 -147
  233. package/framework/skills/deploy-verify/scripts/check-cwv.js +0 -139
  234. package/framework/skills/deploy-verify/scripts/project-detect.sh +0 -84
  235. package/framework/skills/deploy-verify/scripts/verify.sh +0 -548
  236. package/framework/skills/design-quieter/SKILL.md +0 -130
  237. package/framework/skills/distill/SKILL.md +0 -149
  238. package/framework/skills/docs-lookup/SKILL.md +0 -79
  239. package/framework/skills/fcm-notifications/SKILL.md +0 -125
  240. package/framework/skills/financial-ledger/SKILL.md +0 -1039
  241. package/framework/skills/frontend-master/NOTICE.md +0 -4
  242. package/framework/skills/frontend-master/SKILL.md +0 -127
  243. package/framework/skills/frontend-master/reference/color-and-contrast.md +0 -132
  244. package/framework/skills/frontend-master/reference/interaction-design.md +0 -123
  245. package/framework/skills/frontend-master/reference/motion-design.md +0 -99
  246. package/framework/skills/frontend-master/reference/responsive-design.md +0 -114
  247. package/framework/skills/frontend-master/reference/spatial-design.md +0 -100
  248. package/framework/skills/frontend-master/reference/typography.md +0 -131
  249. package/framework/skills/frontend-master/reference/ux-writing.md +0 -107
  250. package/framework/skills/harden/SKILL.md +0 -357
  251. package/framework/skills/i18n-rtl/SKILL.md +0 -752
  252. package/framework/skills/learn/SKILL.md +0 -95
  253. package/framework/skills/memory/SKILL.md +0 -50
  254. package/framework/skills/mobile-expo/SKILL.md +0 -977
  255. package/framework/skills/mobile-expo/references/store-checklist.md +0 -550
  256. package/framework/skills/nestjs-backend/README.md +0 -73
  257. package/framework/skills/nestjs-backend/SKILL.md +0 -446
  258. package/framework/skills/nestjs-backend/references/templates.md +0 -1173
  259. package/framework/skills/normalize/SKILL.md +0 -79
  260. package/framework/skills/onboard/SKILL.md +0 -242
  261. package/framework/skills/openrouter-agent/SKILL.md +0 -922
  262. package/framework/skills/polish/SKILL.md +0 -209
  263. package/framework/skills/pr/SKILL.md +0 -66
  264. package/framework/skills/qualia/SKILL.md +0 -199
  265. package/framework/skills/qualia-add-todo/SKILL.md +0 -68
  266. package/framework/skills/qualia-audit-milestone/SKILL.md +0 -95
  267. package/framework/skills/qualia-check-todos/SKILL.md +0 -55
  268. package/framework/skills/qualia-complete-milestone/SKILL.md +0 -134
  269. package/framework/skills/qualia-debug/SKILL.md +0 -149
  270. package/framework/skills/qualia-design/SKILL.md +0 -203
  271. package/framework/skills/qualia-discuss-phase/SKILL.md +0 -72
  272. package/framework/skills/qualia-evolve/SKILL.md +0 -200
  273. package/framework/skills/qualia-execute-phase/SKILL.md +0 -89
  274. package/framework/skills/qualia-framework-audit/SKILL.md +0 -604
  275. package/framework/skills/qualia-guide/SKILL.md +0 -32
  276. package/framework/skills/qualia-help/SKILL.md +0 -114
  277. package/framework/skills/qualia-idk/SKILL.md +0 -352
  278. package/framework/skills/qualia-list-phase-assumptions/SKILL.md +0 -67
  279. package/framework/skills/qualia-new-milestone/SKILL.md +0 -72
  280. package/framework/skills/qualia-new-project/SKILL.md +0 -232
  281. package/framework/skills/qualia-optimize/SKILL.md +0 -417
  282. package/framework/skills/qualia-pause-work/SKILL.md +0 -96
  283. package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +0 -57
  284. package/framework/skills/qualia-plan-phase/SKILL.md +0 -104
  285. package/framework/skills/qualia-production-check/SKILL.md +0 -0
  286. package/framework/skills/qualia-progress/SKILL.md +0 -53
  287. package/framework/skills/qualia-quick/SKILL.md +0 -89
  288. package/framework/skills/qualia-report/SKILL.md +0 -166
  289. package/framework/skills/qualia-research-phase/SKILL.md +0 -88
  290. package/framework/skills/qualia-resume-work/SKILL.md +0 -62
  291. package/framework/skills/qualia-review/SKILL.md +0 -263
  292. package/framework/skills/qualia-start/SKILL.md +0 -161
  293. package/framework/skills/qualia-verify-work/SKILL.md +0 -132
  294. package/framework/skills/rag/SKILL.md +0 -750
  295. package/framework/skills/responsive/SKILL.md +0 -231
  296. package/framework/skills/retro/SKILL.md +0 -284
  297. package/framework/skills/sakani-conventions/SKILL.md +0 -136
  298. package/framework/skills/sakani-conventions/evals/evals.json +0 -23
  299. package/framework/skills/sakani-conventions/references/entities.md +0 -365
  300. package/framework/skills/sakani-conventions/references/error-codes.md +0 -95
  301. package/framework/skills/seo-master/SKILL.md +0 -490
  302. package/framework/skills/seo-master/references/checklist.md +0 -199
  303. package/framework/skills/seo-master/references/structured-data.md +0 -609
  304. package/framework/skills/ship/SKILL.md +0 -239
  305. package/framework/skills/stack-researcher/SKILL.md +0 -215
  306. package/framework/skills/status/SKILL.md +0 -154
  307. package/framework/skills/status/scripts/health-check.sh +0 -562
  308. package/framework/skills/subscription-payments/SKILL.md +0 -250
  309. package/framework/skills/supabase/SKILL.md +0 -973
  310. package/framework/skills/supabase/references/templates.md +0 -159
  311. package/framework/skills/team/SKILL.md +0 -67
  312. package/framework/skills/test-runner/SKILL.md +0 -202
  313. package/framework/skills/voice-agent/SKILL.md +0 -1312
  314. package/framework/skills/zoho-workflow/SKILL.md +0 -51
  315. package/framework/statusline-command.sh +0 -117
  316. package/framework/teams/default/inboxes/plan-04.json +0 -9
  317. package/framework/teams/review-team.md +0 -75
  318. package/framework/teams/ship-team.md +0 -86
  319. package/profiles/fawzi.json +0 -16
  320. package/profiles/hasan.json +0 -16
  321. package/profiles/moayad.json +0 -16
  322. package/templates/CLAUDE-owner.md +0 -52
  323. package/templates/CLAUDE.md.hbs +0 -58
  324. package/templates/env.claude.template +0 -12
  325. package/templates/settings.json +0 -172
  326. /package/{framework/rules → rules}/deployment.md +0 -0
  327. /package/{framework/rules → rules}/security.md +0 -0
@@ -1,2201 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Qualia Tools — CLI utility for Qualia workflow operations
5
- *
6
- * Replaces repetitive inline bash patterns across ~50 workflow/workflow/agent files.
7
- * Centralizes: config parsing, model resolution, phase lookup, git commits, summary verification.
8
- *
9
- * Usage: node qualia-tools.js <command> [args] [--raw]
10
- *
11
- * Atomic Commands:
12
- * state load Load project config + state
13
- * state update <field> <value> Update a STATE.md field
14
- * resolve-model <agent-type> Get model for agent based on profile
15
- * find-phase <phase> Find phase directory by number
16
- * commit <message> [--files f1 f2] Commit planning docs
17
- * verify-summary <path> Verify a SUMMARY.md file
18
- *
19
- * Compound Commands (workflow-specific initialization):
20
- * init execute-phase <phase> All context for execute-phase workflow
21
- * init plan-phase <phase> All context for plan-phase workflow
22
- * init new-project All context for new-project workflow
23
- * init new-milestone All context for new-milestone workflow
24
- * init quick <description> All context for quick workflow
25
- * init resume All context for resume-project workflow
26
- * init verify-work <phase> All context for verify-work workflow
27
- * init phase-op <phase> Generic phase operation context
28
- * init todos [area] All context for todo workflows
29
- * init milestone-op All context for milestone operations
30
- * init map-codebase All context for map-codebase workflow
31
- * init progress All context for progress workflow
32
- */
33
-
34
- const fs = require('fs');
35
- const path = require('path');
36
- const { execSync } = require('child_process');
37
-
38
- // ─── Model Profile Table ─────────────────────────────────────────────────────
39
-
40
- const MODEL_PROFILES = {
41
- 'qualia-planner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
42
- 'qualia-roadmapper': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
43
- 'qualia-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
44
- 'qualia-phase-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
45
- 'qualia-project-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
46
- 'qualia-research-synthesizer': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
47
- 'qualia-debugger': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
48
- 'qualia-codebase-mapper': { quality: 'sonnet', balanced: 'haiku', budget: 'haiku' },
49
- 'qualia-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'sonnet' },
50
- 'qualia-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'sonnet' },
51
- 'qualia-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
52
- };
53
-
54
- // ─── Helpers ──────────────────────────────────────────────────────────────────
55
-
56
- function loadConfig(cwd) {
57
- const configPath = path.join(cwd, '.planning', 'config.json');
58
- const defaults = {
59
- model_profile: 'balanced',
60
- commit_docs: true,
61
- search_gitignored: false,
62
- branching_strategy: 'none',
63
- phase_branch_template: 'qualia/phase-{phase}-{slug}',
64
- milestone_branch_template: 'qualia/{milestone}-{slug}',
65
- research: true,
66
- plan_checker: true,
67
- verifier: true,
68
- parallelization: true,
69
- };
70
-
71
- try {
72
- const raw = fs.readFileSync(configPath, 'utf-8');
73
- const parsed = JSON.parse(raw);
74
-
75
- const get = (key, nested) => {
76
- if (parsed[key] !== undefined) return parsed[key];
77
- if (nested && parsed[nested.section] && parsed[nested.section][nested.field] !== undefined) {
78
- return parsed[nested.section][nested.field];
79
- }
80
- return undefined;
81
- };
82
-
83
- const parallelization = (() => {
84
- const val = get('parallelization');
85
- if (typeof val === 'boolean') return val;
86
- if (typeof val === 'object' && val !== null && 'enabled' in val) return val.enabled;
87
- return defaults.parallelization;
88
- })();
89
-
90
- return {
91
- model_profile: get('model_profile') ?? defaults.model_profile,
92
- commit_docs: get('commit_docs', { section: 'planning', field: 'commit_docs' }) ?? defaults.commit_docs,
93
- search_gitignored: get('search_gitignored', { section: 'planning', field: 'search_gitignored' }) ?? defaults.search_gitignored,
94
- branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy,
95
- phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
96
- milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
97
- research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
98
- plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
99
- verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
100
- parallelization,
101
- };
102
- } catch (e) {
103
- process.stderr.write(`WARNING: .planning/config.json is malformed or unreadable, using defaults: ${e.message}\n`);
104
- return defaults;
105
- }
106
- }
107
-
108
- function isGitIgnored(cwd, targetPath) {
109
- try {
110
- execSync('git check-ignore -q -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
111
- cwd,
112
- stdio: 'pipe',
113
- });
114
- return true;
115
- } catch {
116
- return false;
117
- }
118
- }
119
-
120
- function execGit(cwd, args) {
121
- try {
122
- const escaped = args.map(a => {
123
- if (/^[a-zA-Z0-9._\-/=:@]+$/.test(a)) return a;
124
- return "'" + a.replace(/'/g, "'\\''") + "'";
125
- });
126
- const stdout = execSync('git ' + escaped.join(' '), {
127
- cwd,
128
- stdio: 'pipe',
129
- encoding: 'utf-8',
130
- });
131
- return { exitCode: 0, stdout: stdout.trim(), stderr: '' };
132
- } catch (err) {
133
- return {
134
- exitCode: err.status ?? 1,
135
- stdout: (err.stdout ?? '').toString().trim(),
136
- stderr: (err.stderr ?? '').toString().trim(),
137
- };
138
- }
139
- }
140
-
141
- function normalizePhaseName(phase) {
142
- const match = phase.match(/^(\d+(?:\.\d+)?)/);
143
- if (!match) return phase;
144
- const num = match[1];
145
- const parts = num.split('.');
146
- const padded = parts[0].padStart(2, '0');
147
- return parts.length > 1 ? `${padded}.${parts[1]}` : padded;
148
- }
149
-
150
- function extractFrontmatter(content) {
151
- const frontmatter = {};
152
- const match = content.match(/^---\n([\s\S]+?)\n---/);
153
- if (!match) return frontmatter;
154
-
155
- const yaml = match[1];
156
- const lines = yaml.split('\n');
157
-
158
- // Stack to track nested objects: [{obj, key, indent}]
159
- // obj = object to write to, key = current key collecting array items, indent = indentation level
160
- let stack = [{ obj: frontmatter, key: null, indent: -1 }];
161
-
162
- for (const line of lines) {
163
- // Skip empty lines
164
- if (line.trim() === '') continue;
165
-
166
- // Calculate indentation (number of leading spaces)
167
- const indentMatch = line.match(/^(\s*)/);
168
- const indent = indentMatch ? indentMatch[1].length : 0;
169
-
170
- // Pop stack back to appropriate level
171
- while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
172
- stack.pop();
173
- }
174
-
175
- const current = stack[stack.length - 1];
176
-
177
- // Check for key: value pattern
178
- const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)/);
179
- if (keyMatch) {
180
- const key = keyMatch[2];
181
- const value = keyMatch[3].trim();
182
-
183
- if (value === '' || value === '[') {
184
- // Key with no value or opening bracket — could be nested object or array
185
- // We'll determine based on next lines, for now create placeholder
186
- current.obj[key] = value === '[' ? [] : {};
187
- current.key = null;
188
- // Push new context for potential nested content
189
- stack.push({ obj: current.obj[key], key: null, indent });
190
- } else if (value.startsWith('[') && value.endsWith(']')) {
191
- // Inline array: key: [a, b, c]
192
- current.obj[key] = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
193
- current.key = null;
194
- } else {
195
- // Simple key: value
196
- current.obj[key] = value.replace(/^["']|["']$/g, '');
197
- current.key = null;
198
- }
199
- } else if (line.trim().startsWith('- ')) {
200
- // Array item
201
- const itemValue = line.trim().slice(2).replace(/^["']|["']$/g, '');
202
-
203
- // If current context is an empty object, convert to array
204
- if (typeof current.obj === 'object' && !Array.isArray(current.obj) && Object.keys(current.obj).length === 0) {
205
- // Find the key in parent that points to this object and convert it
206
- const parent = stack.length > 1 ? stack[stack.length - 2] : null;
207
- if (parent) {
208
- for (const k of Object.keys(parent.obj)) {
209
- if (parent.obj[k] === current.obj) {
210
- parent.obj[k] = [itemValue];
211
- current.obj = parent.obj[k];
212
- break;
213
- }
214
- }
215
- }
216
- } else if (Array.isArray(current.obj)) {
217
- current.obj.push(itemValue);
218
- }
219
- }
220
- }
221
-
222
- return frontmatter;
223
- }
224
-
225
- function output(result, raw, rawValue) {
226
- if (raw && rawValue !== undefined) {
227
- process.stdout.write(String(rawValue));
228
- } else {
229
- process.stdout.write(JSON.stringify(result, null, 2));
230
- }
231
- process.exit(0);
232
- }
233
-
234
- function error(message) {
235
- process.stderr.write('Error: ' + message + '\n');
236
- process.exit(1);
237
- }
238
-
239
- // ─── Commands ─────────────────────────────────────────────────────────────────
240
-
241
- function cmdGenerateSlug(text, raw) {
242
- if (!text) {
243
- error('text required for slug generation');
244
- }
245
-
246
- const slug = text
247
- .toLowerCase()
248
- .replace(/[^a-z0-9]+/g, '-')
249
- .replace(/^-+|-+$/g, '');
250
-
251
- const result = { slug };
252
- output(result, raw, slug);
253
- }
254
-
255
- function cmdCurrentTimestamp(format, raw) {
256
- const now = new Date();
257
- let result;
258
-
259
- switch (format) {
260
- case 'date':
261
- result = now.toISOString().split('T')[0];
262
- break;
263
- case 'filename':
264
- result = now.toISOString().replace(/:/g, '-').replace(/\..+/, '');
265
- break;
266
- case 'full':
267
- default:
268
- result = now.toISOString();
269
- break;
270
- }
271
-
272
- output({ timestamp: result }, raw, result);
273
- }
274
-
275
- function cmdListTodos(cwd, area, raw) {
276
- const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
277
-
278
- let count = 0;
279
- const todos = [];
280
-
281
- try {
282
- const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
283
-
284
- for (const file of files) {
285
- try {
286
- const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
287
- const createdMatch = content.match(/^created:\s*(.+)$/m);
288
- const titleMatch = content.match(/^title:\s*(.+)$/m);
289
- const areaMatch = content.match(/^area:\s*(.+)$/m);
290
-
291
- const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
292
-
293
- // Apply area filter if specified
294
- if (area && todoArea !== area) continue;
295
-
296
- count++;
297
- todos.push({
298
- file,
299
- created: createdMatch ? createdMatch[1].trim() : 'unknown',
300
- title: titleMatch ? titleMatch[1].trim() : 'Untitled',
301
- area: todoArea,
302
- path: path.join('.planning', 'todos', 'pending', file),
303
- });
304
- } catch {}
305
- }
306
- } catch {}
307
-
308
- const result = { count, todos };
309
- output(result, raw, count.toString());
310
- }
311
-
312
- function cmdVerifyPathExists(cwd, targetPath, raw) {
313
- if (!targetPath) {
314
- error('path required for verification');
315
- }
316
-
317
- const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
318
-
319
- try {
320
- const stats = fs.statSync(fullPath);
321
- const type = stats.isDirectory() ? 'directory' : stats.isFile() ? 'file' : 'other';
322
- const result = { exists: true, type };
323
- output(result, raw, 'true');
324
- } catch {
325
- const result = { exists: false, type: null };
326
- output(result, raw, 'false');
327
- }
328
- }
329
-
330
- function cmdConfigEnsureSection(cwd, raw) {
331
- const configPath = path.join(cwd, '.planning', 'config.json');
332
- const planningDir = path.join(cwd, '.planning');
333
-
334
- // Ensure .planning directory exists
335
- try {
336
- if (!fs.existsSync(planningDir)) {
337
- fs.mkdirSync(planningDir, { recursive: true });
338
- }
339
- } catch (err) {
340
- error('Failed to create .planning directory: ' + err.message);
341
- }
342
-
343
- // Check if config already exists
344
- if (fs.existsSync(configPath)) {
345
- const result = { created: false, reason: 'already_exists' };
346
- output(result, raw, 'exists');
347
- return;
348
- }
349
-
350
- // Create default config
351
- const defaults = {
352
- model_profile: 'balanced',
353
- commit_docs: true,
354
- search_gitignored: false,
355
- branching_strategy: 'none',
356
- phase_branch_template: 'qualia/phase-{phase}-{slug}',
357
- milestone_branch_template: 'qualia/{milestone}-{slug}',
358
- workflow: {
359
- research: true,
360
- plan_check: true,
361
- verifier: true,
362
- },
363
- parallelization: true,
364
- };
365
-
366
- try {
367
- fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
368
- const result = { created: true, path: '.planning/config.json' };
369
- output(result, raw, 'created');
370
- } catch (err) {
371
- error('Failed to create config.json: ' + err.message);
372
- }
373
- }
374
-
375
- function cmdHistoryDigest(cwd, raw) {
376
- const phasesDir = path.join(cwd, '.planning', 'phases');
377
- const digest = { phases: {}, decisions: [], tech_stack: new Set() };
378
-
379
- if (!fs.existsSync(phasesDir)) {
380
- digest.tech_stack = [];
381
- output(digest, raw);
382
- return;
383
- }
384
-
385
- try {
386
- const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
387
- .filter(e => e.isDirectory())
388
- .map(e => e.name)
389
- .sort();
390
-
391
- for (const dir of phaseDirs) {
392
- const dirPath = path.join(phasesDir, dir);
393
- const summaries = fs.readdirSync(dirPath).filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
394
-
395
- for (const summary of summaries) {
396
- try {
397
- const content = fs.readFileSync(path.join(dirPath, summary), 'utf-8');
398
- const fm = extractFrontmatter(content);
399
-
400
- const phaseNum = fm.phase || dir.split('-')[0];
401
-
402
- if (!digest.phases[phaseNum]) {
403
- digest.phases[phaseNum] = {
404
- name: fm.name || dir.split('-').slice(1).join(' ') || 'Unknown',
405
- provides: new Set(),
406
- affects: new Set(),
407
- patterns: new Set(),
408
- };
409
- }
410
-
411
- // Merge provides
412
- if (fm['dependency-graph'] && fm['dependency-graph'].provides) {
413
- fm['dependency-graph'].provides.forEach(p => digest.phases[phaseNum].provides.add(p));
414
- } else if (fm.provides) {
415
- fm.provides.forEach(p => digest.phases[phaseNum].provides.add(p));
416
- }
417
-
418
- // Merge affects
419
- if (fm['dependency-graph'] && fm['dependency-graph'].affects) {
420
- fm['dependency-graph'].affects.forEach(a => digest.phases[phaseNum].affects.add(a));
421
- }
422
-
423
- // Merge patterns
424
- if (fm['patterns-established']) {
425
- fm['patterns-established'].forEach(p => digest.phases[phaseNum].patterns.add(p));
426
- }
427
-
428
- // Merge decisions
429
- if (fm['key-decisions']) {
430
- fm['key-decisions'].forEach(d => {
431
- digest.decisions.push({ phase: phaseNum, decision: d });
432
- });
433
- }
434
-
435
- // Merge tech stack
436
- if (fm['tech-stack'] && fm['tech-stack'].added) {
437
- fm['tech-stack'].added.forEach(t => digest.tech_stack.add(typeof t === 'string' ? t : t.name));
438
- }
439
-
440
- } catch (e) {
441
- // Skip malformed summaries
442
- }
443
- }
444
- }
445
-
446
- // Convert Sets to Arrays for JSON output
447
- Object.keys(digest.phases).forEach(p => {
448
- digest.phases[p].provides = [...digest.phases[p].provides];
449
- digest.phases[p].affects = [...digest.phases[p].affects];
450
- digest.phases[p].patterns = [...digest.phases[p].patterns];
451
- });
452
- digest.tech_stack = [...digest.tech_stack];
453
-
454
- output(digest, raw);
455
- } catch (e) {
456
- error('Failed to generate history digest: ' + e.message);
457
- }
458
- }
459
-
460
- function cmdPhasesList(cwd, options, raw) {
461
- const phasesDir = path.join(cwd, '.planning', 'phases');
462
- const { type, phase } = options;
463
-
464
- // If no phases directory, return empty
465
- if (!fs.existsSync(phasesDir)) {
466
- if (type) {
467
- output({ files: [], count: 0 }, raw, '');
468
- } else {
469
- output({ directories: [], count: 0 }, raw, '');
470
- }
471
- return;
472
- }
473
-
474
- try {
475
- // Get all phase directories
476
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
477
- let dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
478
-
479
- // Sort numerically (handles decimals: 01, 02, 02.1, 02.2, 03)
480
- dirs.sort((a, b) => {
481
- const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
482
- const bNum = parseFloat(b.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
483
- return aNum - bNum;
484
- });
485
-
486
- // If filtering by phase number
487
- if (phase) {
488
- const normalized = normalizePhaseName(phase);
489
- const match = dirs.find(d => d.startsWith(normalized));
490
- if (!match) {
491
- output({ files: [], count: 0, phase_dir: null, error: 'Phase not found' }, raw, '');
492
- return;
493
- }
494
- dirs = [match];
495
- }
496
-
497
- // If listing files of a specific type
498
- if (type) {
499
- const files = [];
500
- for (const dir of dirs) {
501
- const dirPath = path.join(phasesDir, dir);
502
- const dirFiles = fs.readdirSync(dirPath);
503
-
504
- let filtered;
505
- if (type === 'plans') {
506
- filtered = dirFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
507
- } else if (type === 'summaries') {
508
- filtered = dirFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
509
- } else {
510
- filtered = dirFiles;
511
- }
512
-
513
- files.push(...filtered.sort());
514
- }
515
-
516
- const result = {
517
- files,
518
- count: files.length,
519
- phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)?-?/, '') : null,
520
- };
521
- output(result, raw, files.join('\n'));
522
- return;
523
- }
524
-
525
- // Default: list directories
526
- output({ directories: dirs, count: dirs.length }, raw, dirs.join('\n'));
527
- } catch (e) {
528
- error('Failed to list phases: ' + e.message);
529
- }
530
- }
531
-
532
- function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
533
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
534
-
535
- if (!fs.existsSync(roadmapPath)) {
536
- output({ found: false, error: 'ROADMAP.md not found' }, raw, '');
537
- return;
538
- }
539
-
540
- try {
541
- const content = fs.readFileSync(roadmapPath, 'utf-8');
542
-
543
- // Escape special regex chars in phase number, handle decimal
544
- const escapedPhase = phaseNum.replace(/\./g, '\\.');
545
-
546
- // Match "### Phase X:" or "### Phase X.Y:" with optional name
547
- const phasePattern = new RegExp(
548
- `###\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
549
- 'i'
550
- );
551
- const headerMatch = content.match(phasePattern);
552
-
553
- if (!headerMatch) {
554
- output({ found: false, phase_number: phaseNum }, raw, '');
555
- return;
556
- }
557
-
558
- const phaseName = headerMatch[1].trim();
559
- const headerIndex = headerMatch.index;
560
-
561
- // Find the end of this section (next ### or end of file)
562
- const restOfContent = content.slice(headerIndex);
563
- const nextHeaderMatch = restOfContent.match(/\n###\s+Phase\s+\d/i);
564
- const sectionEnd = nextHeaderMatch
565
- ? headerIndex + nextHeaderMatch.index
566
- : content.length;
567
-
568
- const section = content.slice(headerIndex, sectionEnd).trim();
569
-
570
- // Extract goal if present
571
- const goalMatch = section.match(/\*\*Goal:\*\*\s*([^\n]+)/i);
572
- const goal = goalMatch ? goalMatch[1].trim() : null;
573
-
574
- output(
575
- {
576
- found: true,
577
- phase_number: phaseNum,
578
- phase_name: phaseName,
579
- goal,
580
- section,
581
- },
582
- raw,
583
- section
584
- );
585
- } catch (e) {
586
- error('Failed to read ROADMAP.md: ' + e.message);
587
- }
588
- }
589
-
590
- function cmdPhaseNextDecimal(cwd, basePhase, raw) {
591
- const phasesDir = path.join(cwd, '.planning', 'phases');
592
- const normalized = normalizePhaseName(basePhase);
593
-
594
- // Check if phases directory exists
595
- if (!fs.existsSync(phasesDir)) {
596
- output(
597
- {
598
- found: false,
599
- base_phase: normalized,
600
- next: `${normalized}.1`,
601
- existing: [],
602
- },
603
- raw,
604
- `${normalized}.1`
605
- );
606
- return;
607
- }
608
-
609
- try {
610
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
611
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
612
-
613
- // Check if base phase exists
614
- const baseExists = dirs.some(d => d.startsWith(normalized + '-') || d === normalized);
615
-
616
- // Find existing decimal phases for this base
617
- const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
618
- const existingDecimals = [];
619
-
620
- for (const dir of dirs) {
621
- const match = dir.match(decimalPattern);
622
- if (match) {
623
- existingDecimals.push(`${normalized}.${match[1]}`);
624
- }
625
- }
626
-
627
- // Sort numerically
628
- existingDecimals.sort((a, b) => {
629
- const aNum = parseFloat(a);
630
- const bNum = parseFloat(b);
631
- return aNum - bNum;
632
- });
633
-
634
- // Calculate next decimal
635
- let nextDecimal;
636
- if (existingDecimals.length === 0) {
637
- nextDecimal = `${normalized}.1`;
638
- } else {
639
- const lastDecimal = existingDecimals[existingDecimals.length - 1];
640
- const lastNum = parseInt(lastDecimal.split('.')[1], 10);
641
- nextDecimal = `${normalized}.${lastNum + 1}`;
642
- }
643
-
644
- output(
645
- {
646
- found: baseExists,
647
- base_phase: normalized,
648
- next: nextDecimal,
649
- existing: existingDecimals,
650
- },
651
- raw,
652
- nextDecimal
653
- );
654
- } catch (e) {
655
- error('Failed to calculate next decimal phase: ' + e.message);
656
- }
657
- }
658
-
659
- function cmdStateLoad(cwd, raw) {
660
- const config = loadConfig(cwd);
661
- const planningDir = path.join(cwd, '.planning');
662
-
663
- let stateRaw = '';
664
- try {
665
- stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8');
666
- } catch {}
667
-
668
- const configExists = fs.existsSync(path.join(planningDir, 'config.json'));
669
- const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
670
- const stateExists = stateRaw.length > 0;
671
-
672
- const result = {
673
- config,
674
- state_raw: stateRaw,
675
- state_exists: stateExists,
676
- roadmap_exists: roadmapExists,
677
- config_exists: configExists,
678
- };
679
-
680
- // For --raw, output a condensed key=value format
681
- if (raw) {
682
- const c = config;
683
- const lines = [
684
- `model_profile=${c.model_profile}`,
685
- `commit_docs=${c.commit_docs}`,
686
- `branching_strategy=${c.branching_strategy}`,
687
- `phase_branch_template=${c.phase_branch_template}`,
688
- `milestone_branch_template=${c.milestone_branch_template}`,
689
- `parallelization=${c.parallelization}`,
690
- `research=${c.research}`,
691
- `plan_checker=${c.plan_checker}`,
692
- `verifier=${c.verifier}`,
693
- `config_exists=${configExists}`,
694
- `roadmap_exists=${roadmapExists}`,
695
- `state_exists=${stateExists}`,
696
- ];
697
- process.stdout.write(lines.join('\n'));
698
- process.exit(0);
699
- }
700
-
701
- output(result);
702
- }
703
-
704
- function cmdStateGet(cwd, section, raw) {
705
- const statePath = path.join(cwd, '.planning', 'STATE.md');
706
- try {
707
- const content = fs.readFileSync(statePath, 'utf-8');
708
-
709
- if (!section) {
710
- output({ content }, raw, content);
711
- return;
712
- }
713
-
714
- // Try to find markdown section or field
715
- const fieldEscaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
716
-
717
- // Check for **field:** value
718
- const fieldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
719
- const fieldMatch = content.match(fieldPattern);
720
- if (fieldMatch) {
721
- output({ [section]: fieldMatch[1].trim() }, raw, fieldMatch[1].trim());
722
- return;
723
- }
724
-
725
- // Check for ## Section
726
- const sectionPattern = new RegExp(`##\\s*${fieldEscaped}\\s*\n([\\s\\S]*?)(?=\\n##|$)`, 'i');
727
- const sectionMatch = content.match(sectionPattern);
728
- if (sectionMatch) {
729
- output({ [section]: sectionMatch[1].trim() }, raw, sectionMatch[1].trim());
730
- return;
731
- }
732
-
733
- output({ error: `Section or field "${section}" not found` }, raw, '');
734
- } catch {
735
- error('STATE.md not found');
736
- }
737
- }
738
-
739
- function cmdStatePatch(cwd, patches, raw) {
740
- const statePath = path.join(cwd, '.planning', 'STATE.md');
741
- try {
742
- let content = fs.readFileSync(statePath, 'utf-8');
743
- const results = { updated: [], failed: [] };
744
-
745
- for (const [field, value] of Object.entries(patches)) {
746
- const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
747
- const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
748
-
749
- if (pattern.test(content)) {
750
- content = content.replace(pattern, `$1${value}`);
751
- results.updated.push(field);
752
- } else {
753
- results.failed.push(field);
754
- }
755
- }
756
-
757
- if (results.updated.length > 0) {
758
- fs.writeFileSync(statePath, content, 'utf-8');
759
- }
760
-
761
- output(results, raw, results.updated.length > 0 ? 'true' : 'false');
762
- } catch {
763
- error('STATE.md not found');
764
- }
765
- }
766
-
767
- function cmdStateUpdate(cwd, field, value) {
768
- if (!field || value === undefined) {
769
- error('field and value required for state update');
770
- }
771
-
772
- const statePath = path.join(cwd, '.planning', 'STATE.md');
773
- try {
774
- let content = fs.readFileSync(statePath, 'utf-8');
775
- const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
776
- const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
777
- if (pattern.test(content)) {
778
- content = content.replace(pattern, `$1${value}`);
779
- fs.writeFileSync(statePath, content, 'utf-8');
780
- output({ updated: true });
781
- } else {
782
- output({ updated: false, reason: `Field "${field}" not found in STATE.md` });
783
- }
784
- } catch {
785
- output({ updated: false, reason: 'STATE.md not found' });
786
- }
787
- }
788
-
789
- function cmdResolveModel(cwd, agentType, raw) {
790
- if (!agentType) {
791
- error('agent-type required');
792
- }
793
-
794
- const config = loadConfig(cwd);
795
- const profile = config.model_profile || 'balanced';
796
-
797
- const agentModels = MODEL_PROFILES[agentType];
798
- if (!agentModels) {
799
- const result = { model: 'sonnet', profile, unknown_agent: true };
800
- output(result, raw, 'sonnet');
801
- return;
802
- }
803
-
804
- const model = agentModels[profile] || agentModels['balanced'] || 'sonnet';
805
- const result = { model, profile };
806
- output(result, raw, model);
807
- }
808
-
809
- function cmdFindPhase(cwd, phase, raw) {
810
- if (!phase) {
811
- error('phase identifier required');
812
- }
813
-
814
- const phasesDir = path.join(cwd, '.planning', 'phases');
815
- const normalized = normalizePhaseName(phase);
816
-
817
- const notFound = { found: false, directory: null, phase_number: null, phase_name: null, plans: [], summaries: [] };
818
-
819
- try {
820
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
821
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
822
-
823
- const match = dirs.find(d => d.startsWith(normalized));
824
- if (!match) {
825
- output(notFound, raw, '');
826
- return;
827
- }
828
-
829
- const dirMatch = match.match(/^(\d+(?:\.\d+)?)-?(.*)/);
830
- const phaseNumber = dirMatch ? dirMatch[1] : normalized;
831
- const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
832
-
833
- const phaseDir = path.join(phasesDir, match);
834
- const phaseFiles = fs.readdirSync(phaseDir);
835
- const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
836
- const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').sort();
837
-
838
- const result = {
839
- found: true,
840
- directory: path.join('.planning', 'phases', match),
841
- phase_number: phaseNumber,
842
- phase_name: phaseName,
843
- plans,
844
- summaries,
845
- };
846
-
847
- output(result, raw, result.directory);
848
- } catch {
849
- output(notFound, raw, '');
850
- }
851
- }
852
-
853
- function cmdCommit(cwd, message, files, raw) {
854
- if (!message) {
855
- error('commit message required');
856
- }
857
-
858
- const config = loadConfig(cwd);
859
-
860
- // Check commit_docs config
861
- if (!config.commit_docs) {
862
- const result = { committed: false, hash: null, reason: 'skipped_commit_docs_false' };
863
- output(result, raw, 'skipped');
864
- return;
865
- }
866
-
867
- // Check if .planning is gitignored
868
- if (isGitIgnored(cwd, '.planning')) {
869
- const result = { committed: false, hash: null, reason: 'skipped_gitignored' };
870
- output(result, raw, 'skipped');
871
- return;
872
- }
873
-
874
- // Stage files
875
- const filesToStage = files && files.length > 0 ? files : ['.planning/'];
876
- for (const file of filesToStage) {
877
- execGit(cwd, ['add', file]);
878
- }
879
-
880
- // Commit
881
- const commitResult = execGit(cwd, ['commit', '-m', message]);
882
- if (commitResult.exitCode !== 0) {
883
- if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
884
- const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
885
- output(result, raw, 'nothing');
886
- return;
887
- }
888
- const result = { committed: false, hash: null, reason: 'commit_failed', error: commitResult.stderr };
889
- output(result, raw, 'COMMIT FAILED: ' + (commitResult.stderr || 'unknown error'));
890
- return;
891
- }
892
-
893
- // Get short hash
894
- const hashResult = execGit(cwd, ['rev-parse', '--short', 'HEAD']);
895
- const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
896
- const result = { committed: true, hash, reason: 'committed' };
897
- output(result, raw, hash || 'committed');
898
- }
899
-
900
- function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
901
- if (!summaryPath) {
902
- error('summary-path required');
903
- }
904
-
905
- const fullPath = path.join(cwd, summaryPath);
906
- const checkCount = checkFileCount || 2;
907
-
908
- // Check 1: Summary exists
909
- if (!fs.existsSync(fullPath)) {
910
- const result = {
911
- passed: false,
912
- checks: {
913
- summary_exists: false,
914
- files_created: { checked: 0, found: 0, missing: [] },
915
- commits_exist: false,
916
- self_check: 'not_found',
917
- },
918
- errors: ['SUMMARY.md not found'],
919
- };
920
- output(result, raw, 'failed');
921
- return;
922
- }
923
-
924
- const content = fs.readFileSync(fullPath, 'utf-8');
925
- const errors = [];
926
-
927
- // Check 2: Spot-check files mentioned in summary
928
- const mentionedFiles = new Set();
929
- const patterns = [
930
- /`([^`]+\.[a-zA-Z]+)`/g,
931
- /(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`]+\.[a-zA-Z]+)`?/gi,
932
- ];
933
-
934
- for (const pattern of patterns) {
935
- let m;
936
- while ((m = pattern.exec(content)) !== null) {
937
- const filePath = m[1];
938
- if (filePath && !filePath.startsWith('http') && filePath.includes('/')) {
939
- mentionedFiles.add(filePath);
940
- }
941
- }
942
- }
943
-
944
- const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
945
- const missing = [];
946
- for (const file of filesToCheck) {
947
- if (!fs.existsSync(path.join(cwd, file))) {
948
- missing.push(file);
949
- }
950
- }
951
-
952
- // Check 3: Commits exist
953
- const commitHashPattern = /\b[0-9a-f]{7,40}\b/g;
954
- const hashes = content.match(commitHashPattern) || [];
955
- let commitsExist = false;
956
- if (hashes.length > 0) {
957
- for (const hash of hashes.slice(0, 3)) {
958
- const result = execGit(cwd, ['cat-file', '-t', hash]);
959
- if (result.exitCode === 0 && result.stdout === 'commit') {
960
- commitsExist = true;
961
- break;
962
- }
963
- }
964
- }
965
-
966
- // Check 4: Self-check section
967
- let selfCheck = 'not_found';
968
- const selfCheckPattern = /##\s*(?:Self[- ]?Check|Verification|Quality Check)/i;
969
- if (selfCheckPattern.test(content)) {
970
- const passPattern = /(?:all\s+)?(?:pass|✓|✅|complete|succeeded)/i;
971
- const failPattern = /(?:fail|✗|❌|incomplete|blocked)/i;
972
- const checkSection = content.slice(content.search(selfCheckPattern));
973
- if (failPattern.test(checkSection)) {
974
- selfCheck = 'failed';
975
- } else if (passPattern.test(checkSection)) {
976
- selfCheck = 'passed';
977
- }
978
- }
979
-
980
- if (missing.length > 0) errors.push('Missing files: ' + missing.join(', '));
981
- if (!commitsExist && hashes.length > 0) errors.push('Referenced commit hashes not found in git history');
982
- if (selfCheck === 'failed') errors.push('Self-check section indicates failure');
983
-
984
- const checks = {
985
- summary_exists: true,
986
- files_created: { checked: filesToCheck.length, found: filesToCheck.length - missing.length, missing },
987
- commits_exist: commitsExist,
988
- self_check: selfCheck,
989
- };
990
-
991
- const passed = missing.length === 0 && selfCheck !== 'failed';
992
- const result = { passed, checks, errors };
993
- output(result, raw, passed ? 'passed' : 'failed');
994
- }
995
-
996
- function cmdTemplateSelect(cwd, planPath, raw) {
997
- if (!planPath) {
998
- error('plan-path required');
999
- }
1000
-
1001
- try {
1002
- const fullPath = path.join(cwd, planPath);
1003
- const content = fs.readFileSync(fullPath, 'utf-8');
1004
-
1005
- // Simple heuristics
1006
- const taskMatch = content.match(/###\s*Task\s*\d+/g) || [];
1007
- const taskCount = taskMatch.length;
1008
-
1009
- const decisionMatch = content.match(/decision/gi) || [];
1010
- const hasDecisions = decisionMatch.length > 0;
1011
-
1012
- // Count file mentions
1013
- const fileMentions = new Set();
1014
- const filePattern = /`([^`]+\.[a-zA-Z]+)`/g;
1015
- let m;
1016
- while ((m = filePattern.exec(content)) !== null) {
1017
- if (m[1].includes('/') && !m[1].startsWith('http')) {
1018
- fileMentions.add(m[1]);
1019
- }
1020
- }
1021
- const fileCount = fileMentions.size;
1022
-
1023
- let template = 'templates/summary-standard.md';
1024
- let type = 'standard';
1025
-
1026
- if (taskCount <= 2 && fileCount <= 3 && !hasDecisions) {
1027
- template = 'templates/summary-minimal.md';
1028
- type = 'minimal';
1029
- } else if (hasDecisions || fileCount > 6 || taskCount > 5) {
1030
- template = 'templates/summary-complex.md';
1031
- type = 'complex';
1032
- }
1033
-
1034
- const result = { template, type, taskCount, fileCount, hasDecisions };
1035
- output(result, raw, template);
1036
- } catch (e) {
1037
- // Fallback to standard
1038
- output({ template: 'templates/summary-standard.md', type: 'standard', error: e.message }, raw, 'templates/summary-standard.md');
1039
- }
1040
- }
1041
-
1042
- function cmdPhasePlanIndex(cwd, phase, raw) {
1043
- if (!phase) {
1044
- error('phase required for phase-plan-index');
1045
- }
1046
-
1047
- const phasesDir = path.join(cwd, '.planning', 'phases');
1048
- const normalized = normalizePhaseName(phase);
1049
-
1050
- // Find phase directory
1051
- let phaseDir = null;
1052
- let phaseDirName = null;
1053
- try {
1054
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
1055
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
1056
- const match = dirs.find(d => d.startsWith(normalized));
1057
- if (match) {
1058
- phaseDir = path.join(phasesDir, match);
1059
- phaseDirName = match;
1060
- }
1061
- } catch {
1062
- // phases dir doesn't exist
1063
- }
1064
-
1065
- if (!phaseDir) {
1066
- output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], has_checkpoints: false }, raw);
1067
- return;
1068
- }
1069
-
1070
- // Get all files in phase directory
1071
- const phaseFiles = fs.readdirSync(phaseDir);
1072
- const planFiles = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
1073
- const summaryFiles = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
1074
-
1075
- // Build set of plan IDs with summaries
1076
- const completedPlanIds = new Set(
1077
- summaryFiles.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
1078
- );
1079
-
1080
- const plans = [];
1081
- const waves = {};
1082
- const incomplete = [];
1083
- let hasCheckpoints = false;
1084
-
1085
- for (const planFile of planFiles) {
1086
- const planId = planFile.replace('-PLAN.md', '').replace('PLAN.md', '');
1087
- const planPath = path.join(phaseDir, planFile);
1088
- const content = fs.readFileSync(planPath, 'utf-8');
1089
- const fm = extractFrontmatter(content);
1090
-
1091
- // Count tasks (## Task N patterns)
1092
- const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
1093
- const taskCount = taskMatches.length;
1094
-
1095
- // Parse wave as integer
1096
- const wave = parseInt(fm.wave, 10) || 1;
1097
-
1098
- // Parse autonomous (default true if not specified)
1099
- let autonomous = true;
1100
- if (fm.autonomous !== undefined) {
1101
- autonomous = fm.autonomous === 'true' || fm.autonomous === true;
1102
- }
1103
-
1104
- if (!autonomous) {
1105
- hasCheckpoints = true;
1106
- }
1107
-
1108
- // Parse files-modified
1109
- let filesModified = [];
1110
- if (fm['files-modified']) {
1111
- filesModified = Array.isArray(fm['files-modified']) ? fm['files-modified'] : [fm['files-modified']];
1112
- }
1113
-
1114
- const hasSummary = completedPlanIds.has(planId);
1115
- if (!hasSummary) {
1116
- incomplete.push(planId);
1117
- }
1118
-
1119
- const plan = {
1120
- id: planId,
1121
- wave,
1122
- autonomous,
1123
- objective: fm.objective || null,
1124
- files_modified: filesModified,
1125
- task_count: taskCount,
1126
- has_summary: hasSummary,
1127
- };
1128
-
1129
- plans.push(plan);
1130
-
1131
- // Group by wave
1132
- const waveKey = String(wave);
1133
- if (!waves[waveKey]) {
1134
- waves[waveKey] = [];
1135
- }
1136
- waves[waveKey].push(planId);
1137
- }
1138
-
1139
- const result = {
1140
- phase: normalized,
1141
- plans,
1142
- waves,
1143
- incomplete,
1144
- has_checkpoints: hasCheckpoints,
1145
- };
1146
-
1147
- output(result, raw);
1148
- }
1149
-
1150
- function cmdStateSnapshot(cwd, raw) {
1151
- const statePath = path.join(cwd, '.planning', 'STATE.md');
1152
-
1153
- if (!fs.existsSync(statePath)) {
1154
- output({ error: 'STATE.md not found' }, raw);
1155
- return;
1156
- }
1157
-
1158
- const content = fs.readFileSync(statePath, 'utf-8');
1159
-
1160
- // Parse "Phase: X of Y (Name)" or "Phase: X of Y ([Name])"
1161
- let currentPhase = null, totalPhases = null, currentPhaseName = null;
1162
- const phaseMatch = content.match(/^Phase:\s*(\d+(?:\.\d+)?)\s*of\s*(\d+)\s*\(([^)]+)\)/m);
1163
- if (phaseMatch) {
1164
- currentPhase = phaseMatch[1];
1165
- totalPhases = parseInt(phaseMatch[2], 10);
1166
- currentPhaseName = phaseMatch[3].trim();
1167
- }
1168
-
1169
- // Parse "Plan: A of B in current phase" or "Plan: Not started" or "Plan: All plans complete"
1170
- let currentPlan = null, totalPlansInPhase = null;
1171
- const planMatch = content.match(/^Plan:\s*(\d+)\s*of\s*(\d+)/m);
1172
- if (planMatch) {
1173
- currentPlan = planMatch[1];
1174
- totalPlansInPhase = parseInt(planMatch[2], 10);
1175
- } else {
1176
- const planAlt = content.match(/^Plan:\s*(.+)/m);
1177
- if (planAlt) currentPlan = planAlt[1].trim();
1178
- }
1179
-
1180
- // Parse "Status: ..."
1181
- let status = null;
1182
- const statusMatch = content.match(/^Status:\s*(.+)/m);
1183
- if (statusMatch) status = statusMatch[1].trim();
1184
-
1185
- // Parse "Last activity: DATE — Description"
1186
- let lastActivity = null, lastActivityDesc = null;
1187
- const activityMatch = content.match(/^Last activity:\s*(\S+)\s*(?:—|--)\s*(.+)/m);
1188
- if (activityMatch) {
1189
- lastActivity = activityMatch[1].trim();
1190
- lastActivityDesc = activityMatch[2].trim();
1191
- }
1192
-
1193
- // Parse "Progress: [...] N%" — extract percentage
1194
- let progressPercent = null;
1195
- const progressMatch = content.match(/^Progress:.*?(\d+)%/m);
1196
- if (progressMatch) progressPercent = parseInt(progressMatch[1], 10);
1197
-
1198
- // Parse "Assigned to: ..."
1199
- let assignedTo = null;
1200
- const assignedMatch = content.match(/^Assigned to:\s*(.+)/m);
1201
- if (assignedMatch) assignedTo = assignedMatch[1].trim();
1202
-
1203
- // Extract decisions from "### Decisions" section
1204
- // Format: "- [Phase X]: decision summary" or "- [Pre-roadmap]: decision"
1205
- const decisions = [];
1206
- const decisionsSection = content.match(/###\s*Decisions\s*\n([\s\S]*?)(?=\n###|\n##|$)/i);
1207
- if (decisionsSection) {
1208
- const lines = decisionsSection[1].split('\n');
1209
- for (const line of lines) {
1210
- const decMatch = line.match(/^-\s+\[([^\]]+)\]:\s*(.+)/);
1211
- if (decMatch) {
1212
- decisions.push({
1213
- phase: decMatch[1].trim(),
1214
- summary: decMatch[2].trim(),
1215
- rationale: '',
1216
- });
1217
- }
1218
- }
1219
- }
1220
-
1221
- // Extract blockers from "### Blockers/Concerns" section
1222
- const blockers = [];
1223
- const blockersSection = content.match(/###\s*Blockers\/Concerns\s*\n([\s\S]*?)(?=\n###|\n##|$)/i);
1224
- if (blockersSection) {
1225
- const items = blockersSection[1].match(/^-\s+(.+)$/gm) || [];
1226
- for (const item of items) {
1227
- const text = item.replace(/^-\s+/, '').trim();
1228
- if (text && !text.match(/^none/i)) blockers.push(text);
1229
- }
1230
- }
1231
-
1232
- // Extract session info from "## Session Continuity"
1233
- const session = { last_date: null, stopped_at: null, resume_file: null };
1234
- const sessionSection = content.match(/##\s*Session Continuity\s*\n([\s\S]*?)(?=\n##|$)/i);
1235
- if (sessionSection) {
1236
- const sec = sessionSection[1];
1237
- const lastDateMatch = sec.match(/^Last session:\s*(.+)/m);
1238
- const stoppedAtMatch = sec.match(/^Stopped at:\s*(.+)/m);
1239
- const resumeFileMatch = sec.match(/^Resume file:\s*(.+)/m);
1240
- if (lastDateMatch) session.last_date = lastDateMatch[1].trim();
1241
- if (stoppedAtMatch) session.stopped_at = stoppedAtMatch[1].trim();
1242
- if (resumeFileMatch) {
1243
- const rf = resumeFileMatch[1].trim();
1244
- session.resume_file = rf.toLowerCase() === 'none' ? null : rf;
1245
- }
1246
- }
1247
-
1248
- output({
1249
- current_phase: currentPhase,
1250
- current_phase_name: currentPhaseName,
1251
- total_phases: totalPhases,
1252
- current_plan: currentPlan,
1253
- total_plans_in_phase: totalPlansInPhase,
1254
- status,
1255
- progress_percent: progressPercent,
1256
- last_activity: lastActivity,
1257
- last_activity_desc: lastActivityDesc,
1258
- assigned_to: assignedTo,
1259
- decisions,
1260
- blockers,
1261
- session,
1262
- }, raw);
1263
- }
1264
-
1265
- function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
1266
- if (!summaryPath) {
1267
- error('summary-path required for summary-extract');
1268
- }
1269
-
1270
- const fullPath = path.join(cwd, summaryPath);
1271
-
1272
- if (!fs.existsSync(fullPath)) {
1273
- output({ error: 'File not found', path: summaryPath }, raw);
1274
- return;
1275
- }
1276
-
1277
- const content = fs.readFileSync(fullPath, 'utf-8');
1278
- const fm = extractFrontmatter(content);
1279
-
1280
- // Parse key-decisions into structured format
1281
- const parseDecisions = (decisionsList) => {
1282
- if (!decisionsList || !Array.isArray(decisionsList)) return [];
1283
- return decisionsList.map(d => {
1284
- const colonIdx = d.indexOf(':');
1285
- if (colonIdx > 0) {
1286
- return {
1287
- summary: d.substring(0, colonIdx).trim(),
1288
- rationale: d.substring(colonIdx + 1).trim(),
1289
- };
1290
- }
1291
- return { summary: d, rationale: null };
1292
- });
1293
- };
1294
-
1295
- // Build full result
1296
- const fullResult = {
1297
- path: summaryPath,
1298
- one_liner: fm['one-liner'] || null,
1299
- key_files: fm['key-files'] || [],
1300
- tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
1301
- patterns: fm['patterns-established'] || [],
1302
- decisions: parseDecisions(fm['key-decisions']),
1303
- };
1304
-
1305
- // If fields specified, filter to only those fields
1306
- if (fields && fields.length > 0) {
1307
- const filtered = { path: summaryPath };
1308
- for (const field of fields) {
1309
- if (fullResult[field] !== undefined) {
1310
- filtered[field] = fullResult[field];
1311
- }
1312
- }
1313
- output(filtered, raw);
1314
- return;
1315
- }
1316
-
1317
- output(fullResult, raw);
1318
- }
1319
-
1320
- // ─── Compound Commands ────────────────────────────────────────────────────────
1321
-
1322
- function resolveModelInternal(cwd, agentType) {
1323
- const config = loadConfig(cwd);
1324
- const profile = config.model_profile || 'balanced';
1325
- const agentModels = MODEL_PROFILES[agentType];
1326
- if (!agentModels) return 'sonnet';
1327
- return agentModels[profile] || agentModels['balanced'] || 'sonnet';
1328
- }
1329
-
1330
- function findPhaseInternal(cwd, phase) {
1331
- if (!phase) return null;
1332
-
1333
- const phasesDir = path.join(cwd, '.planning', 'phases');
1334
- const normalized = normalizePhaseName(phase);
1335
-
1336
- try {
1337
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
1338
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
1339
- const match = dirs.find(d => d.startsWith(normalized));
1340
- if (!match) return null;
1341
-
1342
- const dirMatch = match.match(/^(\d+(?:\.\d+)?)-?(.*)/);
1343
- const phaseNumber = dirMatch ? dirMatch[1] : normalized;
1344
- const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
1345
- const phaseDir = path.join(phasesDir, match);
1346
- const phaseFiles = fs.readdirSync(phaseDir);
1347
-
1348
- const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
1349
- const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').sort();
1350
- const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
1351
- const hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
1352
- const hasVerification = phaseFiles.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
1353
-
1354
- // Determine incomplete plans (plans without matching summaries)
1355
- const completedPlanIds = new Set(
1356
- summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
1357
- );
1358
- const incompletePlans = plans.filter(p => {
1359
- const planId = p.replace('-PLAN.md', '').replace('PLAN.md', '');
1360
- return !completedPlanIds.has(planId);
1361
- });
1362
-
1363
- return {
1364
- found: true,
1365
- directory: path.join('.planning', 'phases', match),
1366
- phase_number: phaseNumber,
1367
- phase_name: phaseName,
1368
- phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
1369
- plans,
1370
- summaries,
1371
- incomplete_plans: incompletePlans,
1372
- has_research: hasResearch,
1373
- has_context: hasContext,
1374
- has_verification: hasVerification,
1375
- };
1376
- } catch {
1377
- return null;
1378
- }
1379
- }
1380
-
1381
- function pathExistsInternal(cwd, targetPath) {
1382
- const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
1383
- try {
1384
- fs.statSync(fullPath);
1385
- return true;
1386
- } catch {
1387
- return false;
1388
- }
1389
- }
1390
-
1391
- function generateSlugInternal(text) {
1392
- if (!text) return null;
1393
- return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
1394
- }
1395
-
1396
- function getMilestoneInfo(cwd) {
1397
- try {
1398
- const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
1399
- const versionMatch = roadmap.match(/v(\d+\.\d+)/);
1400
- const nameMatch = roadmap.match(/## .*v\d+\.\d+[:\s]+([^\n(]+)/);
1401
- return {
1402
- version: versionMatch ? versionMatch[0] : 'v1.0',
1403
- name: nameMatch ? nameMatch[1].trim() : 'milestone',
1404
- };
1405
- } catch {
1406
- return { version: 'v1.0', name: 'milestone' };
1407
- }
1408
- }
1409
-
1410
- function cmdInitExecutePhase(cwd, phase, raw) {
1411
- if (!phase) {
1412
- error('phase required for init execute-phase');
1413
- }
1414
-
1415
- const config = loadConfig(cwd);
1416
- const phaseInfo = findPhaseInternal(cwd, phase);
1417
- const milestone = getMilestoneInfo(cwd);
1418
-
1419
- const result = {
1420
- // Models
1421
- executor_model: resolveModelInternal(cwd, 'qualia-executor'),
1422
- verifier_model: resolveModelInternal(cwd, 'qualia-verifier'),
1423
-
1424
- // Config flags
1425
- commit_docs: config.commit_docs,
1426
- parallelization: config.parallelization,
1427
- branching_strategy: config.branching_strategy,
1428
- phase_branch_template: config.phase_branch_template,
1429
- milestone_branch_template: config.milestone_branch_template,
1430
- verifier_enabled: config.verifier,
1431
-
1432
- // Phase info
1433
- phase_found: !!phaseInfo,
1434
- phase_dir: phaseInfo?.directory || null,
1435
- phase_number: phaseInfo?.phase_number || null,
1436
- phase_name: phaseInfo?.phase_name || null,
1437
- phase_slug: phaseInfo?.phase_slug || null,
1438
-
1439
- // Plan inventory
1440
- plans: phaseInfo?.plans || [],
1441
- summaries: phaseInfo?.summaries || [],
1442
- incomplete_plans: phaseInfo?.incomplete_plans || [],
1443
- plan_count: phaseInfo?.plans?.length || 0,
1444
- incomplete_count: phaseInfo?.incomplete_plans?.length || 0,
1445
-
1446
- // Branch name (pre-computed)
1447
- branch_name: config.branching_strategy === 'phase' && phaseInfo
1448
- ? config.phase_branch_template
1449
- .replace('{phase}', phaseInfo.phase_number)
1450
- .replace('{slug}', phaseInfo.phase_slug || 'phase')
1451
- : config.branching_strategy === 'milestone'
1452
- ? config.milestone_branch_template
1453
- .replace('{milestone}', milestone.version)
1454
- .replace('{slug}', generateSlugInternal(milestone.name) || 'milestone')
1455
- : null,
1456
-
1457
- // Milestone info
1458
- milestone_version: milestone.version,
1459
- milestone_name: milestone.name,
1460
- milestone_slug: generateSlugInternal(milestone.name),
1461
-
1462
- // File existence
1463
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
1464
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
1465
- config_exists: pathExistsInternal(cwd, '.planning/config.json'),
1466
- };
1467
-
1468
- output(result, raw);
1469
- }
1470
-
1471
- function cmdInitPlanPhase(cwd, phase, raw) {
1472
- if (!phase) {
1473
- error('phase required for init plan-phase');
1474
- }
1475
-
1476
- const config = loadConfig(cwd);
1477
- const phaseInfo = findPhaseInternal(cwd, phase);
1478
-
1479
- const result = {
1480
- // Models
1481
- researcher_model: resolveModelInternal(cwd, 'qualia-phase-researcher'),
1482
- planner_model: resolveModelInternal(cwd, 'qualia-planner'),
1483
- checker_model: resolveModelInternal(cwd, 'qualia-plan-checker'),
1484
-
1485
- // Workflow flags
1486
- research_enabled: config.research,
1487
- plan_checker_enabled: config.plan_checker,
1488
- commit_docs: config.commit_docs,
1489
-
1490
- // Phase info
1491
- phase_found: !!phaseInfo,
1492
- phase_dir: phaseInfo?.directory || null,
1493
- phase_number: phaseInfo?.phase_number || null,
1494
- phase_name: phaseInfo?.phase_name || null,
1495
- phase_slug: phaseInfo?.phase_slug || null,
1496
- padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
1497
-
1498
- // Existing artifacts
1499
- has_research: phaseInfo?.has_research || false,
1500
- has_context: phaseInfo?.has_context || false,
1501
- has_plans: (phaseInfo?.plans?.length || 0) > 0,
1502
- plan_count: phaseInfo?.plans?.length || 0,
1503
-
1504
- // Environment
1505
- planning_exists: pathExistsInternal(cwd, '.planning'),
1506
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
1507
- };
1508
-
1509
- output(result, raw);
1510
- }
1511
-
1512
- function cmdInitNewProject(cwd, raw) {
1513
- const config = loadConfig(cwd);
1514
-
1515
- // Detect existing code
1516
- let hasCode = false;
1517
- let hasPackageFile = false;
1518
- try {
1519
- const files = execSync('find . -maxdepth 3 \\( -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" -o -name "*.java" \\) 2>/dev/null | grep -v node_modules | grep -v .git | head -5', {
1520
- cwd,
1521
- encoding: 'utf-8',
1522
- stdio: ['pipe', 'pipe', 'pipe'],
1523
- });
1524
- hasCode = files.trim().length > 0;
1525
- } catch {}
1526
-
1527
- hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
1528
- pathExistsInternal(cwd, 'requirements.txt') ||
1529
- pathExistsInternal(cwd, 'Cargo.toml') ||
1530
- pathExistsInternal(cwd, 'go.mod') ||
1531
- pathExistsInternal(cwd, 'Package.swift');
1532
-
1533
- const result = {
1534
- // Models
1535
- researcher_model: resolveModelInternal(cwd, 'qualia-project-researcher'),
1536
- synthesizer_model: resolveModelInternal(cwd, 'qualia-research-synthesizer'),
1537
- roadmapper_model: resolveModelInternal(cwd, 'qualia-roadmapper'),
1538
-
1539
- // Config
1540
- commit_docs: config.commit_docs,
1541
-
1542
- // Existing state
1543
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1544
- has_codebase_map: pathExistsInternal(cwd, '.planning/codebase'),
1545
- planning_exists: pathExistsInternal(cwd, '.planning'),
1546
-
1547
- // Brownfield detection
1548
- has_existing_code: hasCode,
1549
- has_package_file: hasPackageFile,
1550
- is_brownfield: hasCode || hasPackageFile,
1551
- needs_codebase_map: (hasCode || hasPackageFile) && !pathExistsInternal(cwd, '.planning/codebase'),
1552
-
1553
- // Git state
1554
- has_git: pathExistsInternal(cwd, '.git'),
1555
- };
1556
-
1557
- output(result, raw);
1558
- }
1559
-
1560
- function cmdInitNewMilestone(cwd, raw) {
1561
- const config = loadConfig(cwd);
1562
- const milestone = getMilestoneInfo(cwd);
1563
-
1564
- const result = {
1565
- // Models
1566
- researcher_model: resolveModelInternal(cwd, 'qualia-project-researcher'),
1567
- synthesizer_model: resolveModelInternal(cwd, 'qualia-research-synthesizer'),
1568
- roadmapper_model: resolveModelInternal(cwd, 'qualia-roadmapper'),
1569
-
1570
- // Config
1571
- commit_docs: config.commit_docs,
1572
- research_enabled: config.research,
1573
-
1574
- // Current milestone
1575
- current_milestone: milestone.version,
1576
- current_milestone_name: milestone.name,
1577
-
1578
- // File existence
1579
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1580
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
1581
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
1582
- };
1583
-
1584
- output(result, raw);
1585
- }
1586
-
1587
- function cmdInitQuick(cwd, description, raw) {
1588
- const config = loadConfig(cwd);
1589
- const now = new Date();
1590
- const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
1591
-
1592
- // Find next quick task number
1593
- const quickDir = path.join(cwd, '.planning', 'quick');
1594
- let nextNum = 1;
1595
- try {
1596
- const existing = fs.readdirSync(quickDir)
1597
- .filter(f => /^\d+-/.test(f))
1598
- .map(f => parseInt(f.split('-')[0], 10))
1599
- .filter(n => !isNaN(n));
1600
- if (existing.length > 0) {
1601
- nextNum = Math.max(...existing) + 1;
1602
- }
1603
- } catch {}
1604
-
1605
- const result = {
1606
- // Models
1607
- planner_model: resolveModelInternal(cwd, 'qualia-planner'),
1608
- executor_model: resolveModelInternal(cwd, 'qualia-executor'),
1609
-
1610
- // Config
1611
- commit_docs: config.commit_docs,
1612
-
1613
- // Quick task info
1614
- next_num: nextNum,
1615
- slug: slug,
1616
- description: description || null,
1617
-
1618
- // Timestamps
1619
- date: now.toISOString().split('T')[0],
1620
- timestamp: now.toISOString(),
1621
-
1622
- // Paths
1623
- quick_dir: '.planning/quick',
1624
- task_dir: slug ? `.planning/quick/${nextNum}-${slug}` : null,
1625
-
1626
- // File existence
1627
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
1628
- planning_exists: pathExistsInternal(cwd, '.planning'),
1629
- };
1630
-
1631
- output(result, raw);
1632
- }
1633
-
1634
- function cmdInitResume(cwd, raw) {
1635
- const config = loadConfig(cwd);
1636
-
1637
- // Check for interrupted agent
1638
- let interruptedAgentId = null;
1639
- try {
1640
- interruptedAgentId = fs.readFileSync(path.join(cwd, '.planning', 'current-agent-id.txt'), 'utf-8').trim();
1641
- } catch {}
1642
-
1643
- const result = {
1644
- // File existence
1645
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
1646
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
1647
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1648
- planning_exists: pathExistsInternal(cwd, '.planning'),
1649
-
1650
- // Agent state
1651
- has_interrupted_agent: !!interruptedAgentId,
1652
- interrupted_agent_id: interruptedAgentId,
1653
-
1654
- // Config
1655
- commit_docs: config.commit_docs,
1656
- };
1657
-
1658
- output(result, raw);
1659
- }
1660
-
1661
- function cmdInitVerifyWork(cwd, phase, raw) {
1662
- if (!phase) {
1663
- error('phase required for init verify-work');
1664
- }
1665
-
1666
- const config = loadConfig(cwd);
1667
- const phaseInfo = findPhaseInternal(cwd, phase);
1668
-
1669
- const result = {
1670
- // Models
1671
- planner_model: resolveModelInternal(cwd, 'qualia-planner'),
1672
- checker_model: resolveModelInternal(cwd, 'qualia-plan-checker'),
1673
-
1674
- // Config
1675
- commit_docs: config.commit_docs,
1676
-
1677
- // Phase info
1678
- phase_found: !!phaseInfo,
1679
- phase_dir: phaseInfo?.directory || null,
1680
- phase_number: phaseInfo?.phase_number || null,
1681
- phase_name: phaseInfo?.phase_name || null,
1682
-
1683
- // Existing artifacts
1684
- has_verification: phaseInfo?.has_verification || false,
1685
- };
1686
-
1687
- output(result, raw);
1688
- }
1689
-
1690
- function cmdInitPhaseOp(cwd, phase, raw) {
1691
- const config = loadConfig(cwd);
1692
- const phaseInfo = findPhaseInternal(cwd, phase);
1693
-
1694
- const result = {
1695
- // Config
1696
- commit_docs: config.commit_docs,
1697
-
1698
- // Phase info
1699
- phase_found: !!phaseInfo,
1700
- phase_dir: phaseInfo?.directory || null,
1701
- phase_number: phaseInfo?.phase_number || null,
1702
- phase_name: phaseInfo?.phase_name || null,
1703
- phase_slug: phaseInfo?.phase_slug || null,
1704
- padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
1705
-
1706
- // Existing artifacts
1707
- has_research: phaseInfo?.has_research || false,
1708
- has_context: phaseInfo?.has_context || false,
1709
- has_plans: (phaseInfo?.plans?.length || 0) > 0,
1710
- has_verification: phaseInfo?.has_verification || false,
1711
- plan_count: phaseInfo?.plans?.length || 0,
1712
-
1713
- // File existence
1714
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
1715
- planning_exists: pathExistsInternal(cwd, '.planning'),
1716
- };
1717
-
1718
- output(result, raw);
1719
- }
1720
-
1721
- function cmdInitTodos(cwd, area, raw) {
1722
- const config = loadConfig(cwd);
1723
- const now = new Date();
1724
-
1725
- // List todos (reuse existing logic)
1726
- const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
1727
- let count = 0;
1728
- const todos = [];
1729
-
1730
- try {
1731
- const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
1732
- for (const file of files) {
1733
- try {
1734
- const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
1735
- const createdMatch = content.match(/^created:\s*(.+)$/m);
1736
- const titleMatch = content.match(/^title:\s*(.+)$/m);
1737
- const areaMatch = content.match(/^area:\s*(.+)$/m);
1738
- const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
1739
-
1740
- if (area && todoArea !== area) continue;
1741
-
1742
- count++;
1743
- todos.push({
1744
- file,
1745
- created: createdMatch ? createdMatch[1].trim() : 'unknown',
1746
- title: titleMatch ? titleMatch[1].trim() : 'Untitled',
1747
- area: todoArea,
1748
- path: path.join('.planning', 'todos', 'pending', file),
1749
- });
1750
- } catch {}
1751
- }
1752
- } catch {}
1753
-
1754
- const result = {
1755
- // Config
1756
- commit_docs: config.commit_docs,
1757
-
1758
- // Timestamps
1759
- date: now.toISOString().split('T')[0],
1760
- timestamp: now.toISOString(),
1761
-
1762
- // Todo inventory
1763
- todo_count: count,
1764
- todos,
1765
- area_filter: area || null,
1766
-
1767
- // Paths
1768
- pending_dir: '.planning/todos/pending',
1769
- completed_dir: '.planning/todos/completed',
1770
-
1771
- // File existence
1772
- planning_exists: pathExistsInternal(cwd, '.planning'),
1773
- todos_dir_exists: pathExistsInternal(cwd, '.planning/todos'),
1774
- pending_dir_exists: pathExistsInternal(cwd, '.planning/todos/pending'),
1775
- };
1776
-
1777
- output(result, raw);
1778
- }
1779
-
1780
- function cmdInitMilestoneOp(cwd, raw) {
1781
- const config = loadConfig(cwd);
1782
- const milestone = getMilestoneInfo(cwd);
1783
-
1784
- // Count phases
1785
- let phaseCount = 0;
1786
- let completedPhases = 0;
1787
- const phasesDir = path.join(cwd, '.planning', 'phases');
1788
- try {
1789
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
1790
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
1791
- phaseCount = dirs.length;
1792
-
1793
- // Count phases with summaries (completed)
1794
- for (const dir of dirs) {
1795
- try {
1796
- const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
1797
- const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
1798
- if (hasSummary) completedPhases++;
1799
- } catch {}
1800
- }
1801
- } catch {}
1802
-
1803
- // Check archive
1804
- const archiveDir = path.join(cwd, '.planning', 'archive');
1805
- let archivedMilestones = [];
1806
- try {
1807
- archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
1808
- .filter(e => e.isDirectory())
1809
- .map(e => e.name);
1810
- } catch {}
1811
-
1812
- const result = {
1813
- // Config
1814
- commit_docs: config.commit_docs,
1815
-
1816
- // Current milestone
1817
- milestone_version: milestone.version,
1818
- milestone_name: milestone.name,
1819
- milestone_slug: generateSlugInternal(milestone.name),
1820
-
1821
- // Phase counts
1822
- phase_count: phaseCount,
1823
- completed_phases: completedPhases,
1824
- all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
1825
-
1826
- // Archive
1827
- archived_milestones: archivedMilestones,
1828
- archive_count: archivedMilestones.length,
1829
-
1830
- // File existence
1831
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1832
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
1833
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
1834
- archive_exists: pathExistsInternal(cwd, '.planning/archive'),
1835
- phases_dir_exists: pathExistsInternal(cwd, '.planning/phases'),
1836
- };
1837
-
1838
- output(result, raw);
1839
- }
1840
-
1841
- function cmdInitMapCodebase(cwd, raw) {
1842
- const config = loadConfig(cwd);
1843
-
1844
- // Check for existing codebase maps
1845
- const codebaseDir = path.join(cwd, '.planning', 'codebase');
1846
- let existingMaps = [];
1847
- try {
1848
- existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
1849
- } catch {}
1850
-
1851
- const result = {
1852
- // Models
1853
- mapper_model: resolveModelInternal(cwd, 'qualia-codebase-mapper'),
1854
-
1855
- // Config
1856
- commit_docs: config.commit_docs,
1857
- search_gitignored: config.search_gitignored,
1858
- parallelization: config.parallelization,
1859
-
1860
- // Paths
1861
- codebase_dir: '.planning/codebase',
1862
-
1863
- // Existing maps
1864
- existing_maps: existingMaps,
1865
- has_maps: existingMaps.length > 0,
1866
-
1867
- // File existence
1868
- planning_exists: pathExistsInternal(cwd, '.planning'),
1869
- codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'),
1870
- };
1871
-
1872
- output(result, raw);
1873
- }
1874
-
1875
- function cmdInitProgress(cwd, raw) {
1876
- const config = loadConfig(cwd);
1877
- const milestone = getMilestoneInfo(cwd);
1878
-
1879
- // Analyze phases
1880
- const phasesDir = path.join(cwd, '.planning', 'phases');
1881
- const phases = [];
1882
- let currentPhase = null;
1883
- let nextPhase = null;
1884
-
1885
- try {
1886
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
1887
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
1888
-
1889
- for (const dir of dirs) {
1890
- const match = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
1891
- const phaseNumber = match ? match[1] : dir;
1892
- const phaseName = match && match[2] ? match[2] : null;
1893
-
1894
- const phasePath = path.join(phasesDir, dir);
1895
- const phaseFiles = fs.readdirSync(phasePath);
1896
-
1897
- const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
1898
- const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
1899
- const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
1900
-
1901
- const status = summaries.length >= plans.length && plans.length > 0 ? 'complete' :
1902
- plans.length > 0 ? 'in_progress' :
1903
- hasResearch ? 'researched' : 'pending';
1904
-
1905
- const phaseInfo = {
1906
- number: phaseNumber,
1907
- name: phaseName,
1908
- directory: path.join('.planning', 'phases', dir),
1909
- status,
1910
- plan_count: plans.length,
1911
- summary_count: summaries.length,
1912
- has_research: hasResearch,
1913
- };
1914
-
1915
- phases.push(phaseInfo);
1916
-
1917
- // Find current (first incomplete with plans) and next (first pending)
1918
- if (!currentPhase && (status === 'in_progress' || status === 'researched')) {
1919
- currentPhase = phaseInfo;
1920
- }
1921
- if (!nextPhase && status === 'pending') {
1922
- nextPhase = phaseInfo;
1923
- }
1924
- }
1925
- } catch {}
1926
-
1927
- // Check for paused work
1928
- let pausedAt = null;
1929
- try {
1930
- const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8');
1931
- const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
1932
- if (pauseMatch) pausedAt = pauseMatch[1].trim();
1933
- } catch {}
1934
-
1935
- const result = {
1936
- // Models
1937
- executor_model: resolveModelInternal(cwd, 'qualia-executor'),
1938
- planner_model: resolveModelInternal(cwd, 'qualia-planner'),
1939
-
1940
- // Config
1941
- commit_docs: config.commit_docs,
1942
-
1943
- // Milestone
1944
- milestone_version: milestone.version,
1945
- milestone_name: milestone.name,
1946
-
1947
- // Phase overview
1948
- phases,
1949
- phase_count: phases.length,
1950
- completed_count: phases.filter(p => p.status === 'complete').length,
1951
- in_progress_count: phases.filter(p => p.status === 'in_progress').length,
1952
-
1953
- // Current state
1954
- current_phase: currentPhase,
1955
- next_phase: nextPhase,
1956
- paused_at: pausedAt,
1957
- has_work_in_progress: !!currentPhase,
1958
-
1959
- // File existence
1960
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
1961
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
1962
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
1963
- };
1964
-
1965
- output(result, raw);
1966
- }
1967
-
1968
- // ─── CLI Router ───────────────────────────────────────────────────────────────
1969
-
1970
- function main() {
1971
- const args = process.argv.slice(2);
1972
- const rawIndex = args.indexOf('--raw');
1973
- const raw = rawIndex !== -1;
1974
- if (rawIndex !== -1) args.splice(rawIndex, 1);
1975
-
1976
- const command = args[0];
1977
- const cwd = process.cwd();
1978
-
1979
- if (!command) {
1980
- error('Usage: qualia-tools <command> [args] [--raw]\nCommands: state, resolve-model, find-phase, commit, verify-summary, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init');
1981
- }
1982
-
1983
- switch (command) {
1984
- case 'state': {
1985
- const subcommand = args[1];
1986
- if (subcommand === 'update') {
1987
- cmdStateUpdate(cwd, args[2], args[3]);
1988
- } else if (subcommand === 'get') {
1989
- cmdStateGet(cwd, args[2], raw);
1990
- } else if (subcommand === 'patch') {
1991
- const patches = {};
1992
- for (let i = 2; i < args.length; i += 2) {
1993
- const key = args[i].replace(/^--/, '');
1994
- const value = args[i + 1];
1995
- if (key && value !== undefined) {
1996
- patches[key] = value;
1997
- }
1998
- }
1999
- cmdStatePatch(cwd, patches, raw);
2000
- } else {
2001
- cmdStateLoad(cwd, raw);
2002
- }
2003
- break;
2004
- }
2005
-
2006
- case 'resolve-model': {
2007
- cmdResolveModel(cwd, args[1], raw);
2008
- break;
2009
- }
2010
-
2011
- case 'find-phase': {
2012
- cmdFindPhase(cwd, args[1], raw);
2013
- break;
2014
- }
2015
-
2016
- case 'commit': {
2017
- const message = args[1];
2018
- // Parse --files flag
2019
- const filesIndex = args.indexOf('--files');
2020
- const files = filesIndex !== -1 ? args.slice(filesIndex + 1) : [];
2021
- cmdCommit(cwd, message, files, raw);
2022
- break;
2023
- }
2024
-
2025
- case 'verify-summary': {
2026
- const summaryPath = args[1];
2027
- const countIndex = args.indexOf('--check-count');
2028
- const checkCount = countIndex !== -1 ? parseInt(args[countIndex + 1], 10) : 2;
2029
- cmdVerifySummary(cwd, summaryPath, checkCount, raw);
2030
- break;
2031
- }
2032
-
2033
- case 'template': {
2034
- const subcommand = args[1];
2035
- if (subcommand === 'select') {
2036
- cmdTemplateSelect(cwd, args[2], raw);
2037
- }
2038
- break;
2039
- }
2040
-
2041
- case 'generate-slug': {
2042
- cmdGenerateSlug(args[1], raw);
2043
- break;
2044
- }
2045
-
2046
- case 'current-timestamp': {
2047
- cmdCurrentTimestamp(args[1] || 'full', raw);
2048
- break;
2049
- }
2050
-
2051
- case 'list-todos': {
2052
- cmdListTodos(cwd, args[1], raw);
2053
- break;
2054
- }
2055
-
2056
- case 'verify-path-exists': {
2057
- cmdVerifyPathExists(cwd, args[1], raw);
2058
- break;
2059
- }
2060
-
2061
- case 'config-ensure-section': {
2062
- cmdConfigEnsureSection(cwd, raw);
2063
- break;
2064
- }
2065
-
2066
- case 'history-digest': {
2067
- cmdHistoryDigest(cwd, raw);
2068
- break;
2069
- }
2070
-
2071
- case 'phases': {
2072
- const subcommand = args[1];
2073
- if (subcommand === 'list') {
2074
- const typeIndex = args.indexOf('--type');
2075
- const phaseIndex = args.indexOf('--phase');
2076
- const options = {
2077
- type: typeIndex !== -1 ? args[typeIndex + 1] : null,
2078
- phase: phaseIndex !== -1 ? args[phaseIndex + 1] : null,
2079
- };
2080
- cmdPhasesList(cwd, options, raw);
2081
- } else {
2082
- error('Unknown phases subcommand. Available: list');
2083
- }
2084
- break;
2085
- }
2086
-
2087
- case 'roadmap': {
2088
- const subcommand = args[1];
2089
- if (subcommand === 'get-phase') {
2090
- cmdRoadmapGetPhase(cwd, args[2], raw);
2091
- } else {
2092
- error('Unknown roadmap subcommand. Available: get-phase');
2093
- }
2094
- break;
2095
- }
2096
-
2097
- case 'phase': {
2098
- const subcommand = args[1];
2099
- if (subcommand === 'next-decimal') {
2100
- cmdPhaseNextDecimal(cwd, args[2], raw);
2101
- } else {
2102
- error('Unknown phase subcommand. Available: next-decimal');
2103
- }
2104
- break;
2105
- }
2106
-
2107
- case 'init': {
2108
- const workflow = args[1];
2109
- switch (workflow) {
2110
- case 'execute-phase':
2111
- cmdInitExecutePhase(cwd, args[2], raw);
2112
- break;
2113
- case 'plan-phase':
2114
- cmdInitPlanPhase(cwd, args[2], raw);
2115
- break;
2116
- case 'new-project':
2117
- cmdInitNewProject(cwd, raw);
2118
- break;
2119
- case 'new-milestone':
2120
- cmdInitNewMilestone(cwd, raw);
2121
- break;
2122
- case 'quick':
2123
- cmdInitQuick(cwd, args.slice(2).join(' '), raw);
2124
- break;
2125
- case 'resume':
2126
- cmdInitResume(cwd, raw);
2127
- break;
2128
- case 'verify-work':
2129
- cmdInitVerifyWork(cwd, args[2], raw);
2130
- break;
2131
- case 'phase-op':
2132
- cmdInitPhaseOp(cwd, args[2], raw);
2133
- break;
2134
- case 'todos':
2135
- cmdInitTodos(cwd, args[2], raw);
2136
- break;
2137
- case 'milestone-op':
2138
- cmdInitMilestoneOp(cwd, raw);
2139
- break;
2140
- case 'map-codebase':
2141
- cmdInitMapCodebase(cwd, raw);
2142
- break;
2143
- case 'progress':
2144
- cmdInitProgress(cwd, raw);
2145
- break;
2146
- default:
2147
- error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress`);
2148
- }
2149
- break;
2150
- }
2151
-
2152
- case 'phase-plan-index': {
2153
- cmdPhasePlanIndex(cwd, args[1], raw);
2154
- break;
2155
- }
2156
-
2157
- case 'state-snapshot': {
2158
- cmdStateSnapshot(cwd, raw);
2159
- break;
2160
- }
2161
-
2162
- case 'summary-extract': {
2163
- const summaryPath = args[1];
2164
- const fieldsIndex = args.indexOf('--fields');
2165
- const fields = fieldsIndex !== -1 ? args[fieldsIndex + 1].split(',') : null;
2166
- cmdSummaryExtract(cwd, summaryPath, fields, raw);
2167
- break;
2168
- }
2169
-
2170
- case '--help':
2171
- case 'help': {
2172
- const cmds = [
2173
- 'state load — Load project state + config as JSON',
2174
- 'state-snapshot — Structured snapshot of STATE.md fields',
2175
- 'resolve-model <agent> — Resolve model for agent from profile',
2176
- 'find-phase <number> — Find phase directory path',
2177
- 'commit <msg> --files .. — Commit with planning-aware logic',
2178
- 'verify-summary <path> — Verify SUMMARY.md completeness',
2179
- 'generate-slug <text> — Generate URL-safe slug',
2180
- 'current-timestamp — ISO timestamp',
2181
- 'list-todos [area] — List pending todos',
2182
- 'verify-path-exists <path> — Check if path exists',
2183
- 'config-ensure-section — Ensure config section exists',
2184
- 'init <workflow> [args] — Initialize workflow context',
2185
- 'roadmap get-phase <N> — Get phase details from ROADMAP.md',
2186
- 'phase-plan-index <N> — Get plan inventory with wave grouping',
2187
- 'summary-extract <path> — Extract fields from SUMMARY.md',
2188
- ];
2189
- console.log('qualia-tools v2.4.1\n');
2190
- console.log('Usage: qualia-tools <command> [args] [--raw]\n');
2191
- console.log('Commands:');
2192
- cmds.forEach(c => console.log(' ' + c));
2193
- process.exit(0);
2194
- }
2195
-
2196
- default:
2197
- error(`Unknown command: ${command}`);
2198
- }
2199
- }
2200
-
2201
- main();