qualia-framework 2.6.0 → 3.2.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 (321) hide show
  1. package/CLAUDE.md +64 -0
  2. package/README.md +103 -30
  3. package/agents/builder.md +110 -0
  4. package/agents/planner.md +134 -0
  5. package/agents/qa-browser.md +186 -0
  6. package/agents/verifier.md +221 -0
  7. package/bin/cli.js +336 -531
  8. package/bin/install.js +570 -0
  9. package/bin/qualia-ui.js +299 -0
  10. package/bin/state.js +630 -0
  11. package/bin/statusline.js +252 -0
  12. package/guide.md +63 -0
  13. package/hooks/auto-update.js +139 -0
  14. package/hooks/branch-guard.js +47 -0
  15. package/hooks/migration-guard.js +60 -0
  16. package/hooks/pre-compact.js +32 -0
  17. package/hooks/pre-deploy-gate.js +110 -0
  18. package/hooks/pre-push.js +33 -0
  19. package/hooks/session-start.js +170 -0
  20. package/package.json +29 -20
  21. package/rules/design-reference.md +179 -0
  22. package/rules/frontend.md +126 -0
  23. package/skills/qualia/SKILL.md +87 -0
  24. package/skills/qualia-build/SKILL.md +97 -0
  25. package/skills/qualia-debug/SKILL.md +87 -0
  26. package/skills/qualia-design/SKILL.md +93 -0
  27. package/skills/qualia-handoff/SKILL.md +66 -0
  28. package/skills/qualia-idk/SKILL.md +8 -0
  29. package/skills/qualia-learn/SKILL.md +88 -0
  30. package/skills/qualia-new/SKILL.md +323 -0
  31. package/{framework/skills → skills}/qualia-optimize/SKILL.md +1 -1
  32. package/skills/qualia-pause/SKILL.md +63 -0
  33. package/skills/qualia-plan/SKILL.md +101 -0
  34. package/skills/qualia-polish/SKILL.md +157 -0
  35. package/skills/qualia-quick/SKILL.md +37 -0
  36. package/skills/qualia-report/SKILL.md +105 -0
  37. package/skills/qualia-resume/SKILL.md +49 -0
  38. package/skills/qualia-review/SKILL.md +76 -0
  39. package/skills/qualia-ship/SKILL.md +90 -0
  40. package/skills/qualia-skill-new/SKILL.md +167 -0
  41. package/skills/qualia-task/SKILL.md +91 -0
  42. package/skills/qualia-verify/SKILL.md +113 -0
  43. package/templates/DESIGN.md +137 -0
  44. package/templates/plan.md +28 -0
  45. package/templates/project.md +22 -0
  46. package/templates/state.md +27 -0
  47. package/templates/tracking.json +20 -0
  48. package/tests/bin.test.sh +673 -0
  49. package/tests/hooks.test.sh +315 -0
  50. package/tests/state.test.sh +535 -0
  51. package/tests/statusline.test.sh +243 -0
  52. package/bin/collect-metrics.sh +0 -62
  53. package/framework/.claudeignore +0 -51
  54. package/framework/CLAUDE.md +0 -51
  55. package/framework/MCP_SETUP.md +0 -229
  56. package/framework/agents/architecture-strategist.md +0 -53
  57. package/framework/agents/backend-agent.md +0 -150
  58. package/framework/agents/code-simplicity-reviewer.md +0 -86
  59. package/framework/agents/frontend-agent.md +0 -111
  60. package/framework/agents/kieran-typescript-reviewer.md +0 -96
  61. package/framework/agents/performance-oracle.md +0 -111
  62. package/framework/agents/qualia-codebase-mapper.md +0 -761
  63. package/framework/agents/qualia-debugger.md +0 -1204
  64. package/framework/agents/qualia-executor.md +0 -882
  65. package/framework/agents/qualia-integration-checker.md +0 -424
  66. package/framework/agents/qualia-phase-researcher.md +0 -457
  67. package/framework/agents/qualia-plan-checker.md +0 -700
  68. package/framework/agents/qualia-planner.md +0 -1245
  69. package/framework/agents/qualia-project-researcher.md +0 -603
  70. package/framework/agents/qualia-research-synthesizer.md +0 -200
  71. package/framework/agents/qualia-roadmapper.md +0 -606
  72. package/framework/agents/qualia-verifier.md +0 -686
  73. package/framework/agents/red-team-qa.md +0 -130
  74. package/framework/agents/security-auditor.md +0 -72
  75. package/framework/agents/team-orchestrator.md +0 -229
  76. package/framework/agents/teams/framework-audit-team.md +0 -66
  77. package/framework/agents/teams/full-stack-team.md +0 -48
  78. package/framework/agents/teams/optimize-team.md +0 -53
  79. package/framework/agents/teams/review-team.md +0 -70
  80. package/framework/agents/teams/ship-team.md +0 -86
  81. package/framework/agents/test-agent.md +0 -182
  82. package/framework/hooks/auto-format.sh +0 -54
  83. package/framework/hooks/block-env-edit.sh +0 -42
  84. package/framework/hooks/branch-guard.sh +0 -43
  85. package/framework/hooks/confirm-delete.sh +0 -59
  86. package/framework/hooks/migration-validate.sh +0 -77
  87. package/framework/hooks/notification-speak.sh +0 -16
  88. package/framework/hooks/pre-commit.sh +0 -100
  89. package/framework/hooks/pre-compact.sh +0 -56
  90. package/framework/hooks/pre-deploy-gate.sh +0 -160
  91. package/framework/hooks/qualia-colors.sh +0 -32
  92. package/framework/hooks/retention-cleanup.sh +0 -62
  93. package/framework/hooks/save-session-state.sh +0 -185
  94. package/framework/hooks/session-context-loader.sh +0 -96
  95. package/framework/hooks/session-learn.sh +0 -32
  96. package/framework/hooks/skill-announce.sh +0 -123
  97. package/framework/hooks/tool-error-announce.sh +0 -27
  98. package/framework/install.ps1 +0 -323
  99. package/framework/install.sh +0 -313
  100. package/framework/qualia-framework/VERSION +0 -1
  101. package/framework/qualia-framework/assets/qualia-logo.png +0 -0
  102. package/framework/qualia-framework/bin/collect-metrics.sh +0 -67
  103. package/framework/qualia-framework/bin/generate-report-docx.py +0 -429
  104. package/framework/qualia-framework/bin/qualia-tools.js +0 -2201
  105. package/framework/qualia-framework/bin/qualia-tools.test.js +0 -1054
  106. package/framework/qualia-framework/references/checkpoints.md +0 -775
  107. package/framework/qualia-framework/references/completion-checklists.md +0 -359
  108. package/framework/qualia-framework/references/continuation-format.md +0 -249
  109. package/framework/qualia-framework/references/continuation-prompt.md +0 -97
  110. package/framework/qualia-framework/references/decimal-phase-calculation.md +0 -65
  111. package/framework/qualia-framework/references/design-quality.md +0 -56
  112. package/framework/qualia-framework/references/employee-guide.md +0 -167
  113. package/framework/qualia-framework/references/git-integration.md +0 -254
  114. package/framework/qualia-framework/references/git-planning-commit.md +0 -50
  115. package/framework/qualia-framework/references/model-profile-resolution.md +0 -32
  116. package/framework/qualia-framework/references/model-profiles.md +0 -73
  117. package/framework/qualia-framework/references/phase-argument-parsing.md +0 -61
  118. package/framework/qualia-framework/references/planning-config.md +0 -195
  119. package/framework/qualia-framework/references/questioning.md +0 -141
  120. package/framework/qualia-framework/references/tdd.md +0 -263
  121. package/framework/qualia-framework/references/ui-brand.md +0 -160
  122. package/framework/qualia-framework/references/verification-patterns.md +0 -612
  123. package/framework/qualia-framework/templates/DEBUG.md +0 -159
  124. package/framework/qualia-framework/templates/DESIGN.md +0 -81
  125. package/framework/qualia-framework/templates/UAT.md +0 -247
  126. package/framework/qualia-framework/templates/codebase/architecture.md +0 -255
  127. package/framework/qualia-framework/templates/codebase/concerns.md +0 -310
  128. package/framework/qualia-framework/templates/codebase/conventions.md +0 -307
  129. package/framework/qualia-framework/templates/codebase/integrations.md +0 -280
  130. package/framework/qualia-framework/templates/codebase/stack.md +0 -186
  131. package/framework/qualia-framework/templates/codebase/structure.md +0 -285
  132. package/framework/qualia-framework/templates/codebase/testing.md +0 -480
  133. package/framework/qualia-framework/templates/config.json +0 -35
  134. package/framework/qualia-framework/templates/context.md +0 -283
  135. package/framework/qualia-framework/templates/continue-here.md +0 -78
  136. package/framework/qualia-framework/templates/debug-subagent-prompt.md +0 -91
  137. package/framework/qualia-framework/templates/discovery.md +0 -146
  138. package/framework/qualia-framework/templates/lab-notes.md +0 -16
  139. package/framework/qualia-framework/templates/milestone-archive.md +0 -123
  140. package/framework/qualia-framework/templates/milestone.md +0 -115
  141. package/framework/qualia-framework/templates/phase-prompt.md +0 -567
  142. package/framework/qualia-framework/templates/planner-subagent-prompt.md +0 -117
  143. package/framework/qualia-framework/templates/project.md +0 -184
  144. package/framework/qualia-framework/templates/projects/ai-agent.md +0 -156
  145. package/framework/qualia-framework/templates/projects/mobile-app.md +0 -181
  146. package/framework/qualia-framework/templates/projects/voice-agent.md +0 -134
  147. package/framework/qualia-framework/templates/projects/website.md +0 -137
  148. package/framework/qualia-framework/templates/requirements.md +0 -231
  149. package/framework/qualia-framework/templates/research-project/ARCHITECTURE.md +0 -204
  150. package/framework/qualia-framework/templates/research-project/FEATURES.md +0 -147
  151. package/framework/qualia-framework/templates/research-project/PITFALLS.md +0 -200
  152. package/framework/qualia-framework/templates/research-project/STACK.md +0 -120
  153. package/framework/qualia-framework/templates/research-project/SUMMARY.md +0 -170
  154. package/framework/qualia-framework/templates/research.md +0 -552
  155. package/framework/qualia-framework/templates/roadmap.md +0 -206
  156. package/framework/qualia-framework/templates/state.md +0 -179
  157. package/framework/qualia-framework/templates/summary-complex.md +0 -59
  158. package/framework/qualia-framework/templates/summary-minimal.md +0 -41
  159. package/framework/qualia-framework/templates/summary-standard.md +0 -48
  160. package/framework/qualia-framework/templates/summary.md +0 -246
  161. package/framework/qualia-framework/templates/user-setup.md +0 -311
  162. package/framework/qualia-framework/templates/verification-report.md +0 -322
  163. package/framework/qualia-framework/workflows/add-phase.md +0 -179
  164. package/framework/qualia-framework/workflows/add-todo.md +0 -157
  165. package/framework/qualia-framework/workflows/audit-milestone.md +0 -241
  166. package/framework/qualia-framework/workflows/check-todos.md +0 -176
  167. package/framework/qualia-framework/workflows/complete-milestone.md +0 -858
  168. package/framework/qualia-framework/workflows/diagnose-issues.md +0 -219
  169. package/framework/qualia-framework/workflows/discovery-phase.md +0 -289
  170. package/framework/qualia-framework/workflows/discuss-phase.md +0 -534
  171. package/framework/qualia-framework/workflows/execute-phase.md +0 -559
  172. package/framework/qualia-framework/workflows/execute-plan.md +0 -438
  173. package/framework/qualia-framework/workflows/help.md +0 -470
  174. package/framework/qualia-framework/workflows/insert-phase.md +0 -220
  175. package/framework/qualia-framework/workflows/list-phase-assumptions.md +0 -178
  176. package/framework/qualia-framework/workflows/map-codebase.md +0 -327
  177. package/framework/qualia-framework/workflows/new-milestone.md +0 -363
  178. package/framework/qualia-framework/workflows/new-project.md +0 -982
  179. package/framework/qualia-framework/workflows/pause-work.md +0 -122
  180. package/framework/qualia-framework/workflows/plan-milestone-gaps.md +0 -256
  181. package/framework/qualia-framework/workflows/plan-phase.md +0 -422
  182. package/framework/qualia-framework/workflows/progress.md +0 -389
  183. package/framework/qualia-framework/workflows/quick.md +0 -252
  184. package/framework/qualia-framework/workflows/remove-phase.md +0 -326
  185. package/framework/qualia-framework/workflows/research-phase.md +0 -74
  186. package/framework/qualia-framework/workflows/resume-project.md +0 -306
  187. package/framework/qualia-framework/workflows/set-profile.md +0 -80
  188. package/framework/qualia-framework/workflows/settings.md +0 -145
  189. package/framework/qualia-framework/workflows/transition.md +0 -556
  190. package/framework/qualia-framework/workflows/update.md +0 -197
  191. package/framework/qualia-framework/workflows/verify-phase.md +0 -195
  192. package/framework/qualia-framework/workflows/verify-work.md +0 -625
  193. package/framework/rules/context7.md +0 -14
  194. package/framework/rules/frontend.md +0 -33
  195. package/framework/rules/speed.md +0 -23
  196. package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
  197. package/framework/scripts/apply-retention.sh +0 -120
  198. package/framework/scripts/bootstrap-pop-os.sh +0 -354
  199. package/framework/scripts/claude-voice +0 -13
  200. package/framework/scripts/cleanup.sh +0 -131
  201. package/framework/scripts/cowork-mode.sh +0 -141
  202. package/framework/scripts/generate-project-claude-md.sh +0 -153
  203. package/framework/scripts/load-test-webhook.js +0 -172
  204. package/framework/scripts/say.py +0 -236
  205. package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +0 -167
  206. package/framework/scripts/showcase-video-recorder/playwright-helpers.js +0 -216
  207. package/framework/scripts/speak.py +0 -55
  208. package/framework/scripts/speak.sh +0 -18
  209. package/framework/scripts/status.sh +0 -138
  210. package/framework/scripts/sync-to-framework.sh +0 -65
  211. package/framework/scripts/voice-hotkey.py +0 -227
  212. package/framework/scripts/voice-input.sh +0 -51
  213. package/framework/skills/animate/SKILL.md +0 -202
  214. package/framework/skills/bolder/SKILL.md +0 -144
  215. package/framework/skills/browser-qa/SKILL.md +0 -536
  216. package/framework/skills/clarify/SKILL.md +0 -179
  217. package/framework/skills/client-handoff/SKILL.md +0 -135
  218. package/framework/skills/collab-onboard/SKILL.md +0 -111
  219. package/framework/skills/colorize/SKILL.md +0 -170
  220. package/framework/skills/critique/SKILL.md +0 -126
  221. package/framework/skills/deep-research/SKILL.md +0 -240
  222. package/framework/skills/delight/SKILL.md +0 -329
  223. package/framework/skills/deploy/SKILL.md +0 -261
  224. package/framework/skills/deploy-verify/SKILL.md +0 -377
  225. package/framework/skills/deploy-verify/scripts/canary-check.sh +0 -206
  226. package/framework/skills/deploy-verify/scripts/check-console-errors.js +0 -147
  227. package/framework/skills/deploy-verify/scripts/check-cwv.js +0 -139
  228. package/framework/skills/deploy-verify/scripts/project-detect.sh +0 -84
  229. package/framework/skills/deploy-verify/scripts/verify.sh +0 -548
  230. package/framework/skills/design-quieter/SKILL.md +0 -130
  231. package/framework/skills/distill/SKILL.md +0 -149
  232. package/framework/skills/docs-lookup/SKILL.md +0 -79
  233. package/framework/skills/fcm-notifications/SKILL.md +0 -125
  234. package/framework/skills/financial-ledger/SKILL.md +0 -1039
  235. package/framework/skills/frontend-master/NOTICE.md +0 -4
  236. package/framework/skills/frontend-master/SKILL.md +0 -127
  237. package/framework/skills/frontend-master/reference/color-and-contrast.md +0 -132
  238. package/framework/skills/frontend-master/reference/interaction-design.md +0 -123
  239. package/framework/skills/frontend-master/reference/motion-design.md +0 -99
  240. package/framework/skills/frontend-master/reference/responsive-design.md +0 -114
  241. package/framework/skills/frontend-master/reference/spatial-design.md +0 -100
  242. package/framework/skills/frontend-master/reference/typography.md +0 -131
  243. package/framework/skills/frontend-master/reference/ux-writing.md +0 -107
  244. package/framework/skills/harden/SKILL.md +0 -357
  245. package/framework/skills/i18n-rtl/SKILL.md +0 -752
  246. package/framework/skills/learn/SKILL.md +0 -95
  247. package/framework/skills/memory/SKILL.md +0 -50
  248. package/framework/skills/mobile-expo/SKILL.md +0 -977
  249. package/framework/skills/mobile-expo/references/store-checklist.md +0 -550
  250. package/framework/skills/nestjs-backend/README.md +0 -73
  251. package/framework/skills/nestjs-backend/SKILL.md +0 -446
  252. package/framework/skills/nestjs-backend/references/templates.md +0 -1173
  253. package/framework/skills/normalize/SKILL.md +0 -79
  254. package/framework/skills/onboard/SKILL.md +0 -242
  255. package/framework/skills/openrouter-agent/SKILL.md +0 -922
  256. package/framework/skills/polish/SKILL.md +0 -209
  257. package/framework/skills/pr/SKILL.md +0 -66
  258. package/framework/skills/qualia/SKILL.md +0 -199
  259. package/framework/skills/qualia-add-todo/SKILL.md +0 -68
  260. package/framework/skills/qualia-audit-milestone/SKILL.md +0 -95
  261. package/framework/skills/qualia-check-todos/SKILL.md +0 -55
  262. package/framework/skills/qualia-complete-milestone/SKILL.md +0 -134
  263. package/framework/skills/qualia-debug/SKILL.md +0 -149
  264. package/framework/skills/qualia-design/SKILL.md +0 -203
  265. package/framework/skills/qualia-discuss-phase/SKILL.md +0 -72
  266. package/framework/skills/qualia-evolve/SKILL.md +0 -200
  267. package/framework/skills/qualia-execute-phase/SKILL.md +0 -89
  268. package/framework/skills/qualia-framework-audit/SKILL.md +0 -604
  269. package/framework/skills/qualia-guide/SKILL.md +0 -32
  270. package/framework/skills/qualia-help/SKILL.md +0 -114
  271. package/framework/skills/qualia-idk/SKILL.md +0 -352
  272. package/framework/skills/qualia-list-phase-assumptions/SKILL.md +0 -67
  273. package/framework/skills/qualia-new-milestone/SKILL.md +0 -72
  274. package/framework/skills/qualia-new-project/SKILL.md +0 -232
  275. package/framework/skills/qualia-pause-work/SKILL.md +0 -96
  276. package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +0 -57
  277. package/framework/skills/qualia-plan-phase/SKILL.md +0 -104
  278. package/framework/skills/qualia-production-check/SKILL.md +0 -0
  279. package/framework/skills/qualia-progress/SKILL.md +0 -53
  280. package/framework/skills/qualia-quick/SKILL.md +0 -89
  281. package/framework/skills/qualia-report/SKILL.md +0 -166
  282. package/framework/skills/qualia-research-phase/SKILL.md +0 -88
  283. package/framework/skills/qualia-resume-work/SKILL.md +0 -62
  284. package/framework/skills/qualia-review/SKILL.md +0 -263
  285. package/framework/skills/qualia-start/SKILL.md +0 -161
  286. package/framework/skills/qualia-verify-work/SKILL.md +0 -132
  287. package/framework/skills/rag/SKILL.md +0 -750
  288. package/framework/skills/responsive/SKILL.md +0 -231
  289. package/framework/skills/retro/SKILL.md +0 -284
  290. package/framework/skills/sakani-conventions/SKILL.md +0 -136
  291. package/framework/skills/sakani-conventions/evals/evals.json +0 -23
  292. package/framework/skills/sakani-conventions/references/entities.md +0 -365
  293. package/framework/skills/sakani-conventions/references/error-codes.md +0 -95
  294. package/framework/skills/seo-master/SKILL.md +0 -490
  295. package/framework/skills/seo-master/references/checklist.md +0 -199
  296. package/framework/skills/seo-master/references/structured-data.md +0 -609
  297. package/framework/skills/ship/SKILL.md +0 -239
  298. package/framework/skills/stack-researcher/SKILL.md +0 -215
  299. package/framework/skills/status/SKILL.md +0 -154
  300. package/framework/skills/status/scripts/health-check.sh +0 -562
  301. package/framework/skills/subscription-payments/SKILL.md +0 -250
  302. package/framework/skills/supabase/SKILL.md +0 -973
  303. package/framework/skills/supabase/references/templates.md +0 -159
  304. package/framework/skills/team/SKILL.md +0 -67
  305. package/framework/skills/test-runner/SKILL.md +0 -202
  306. package/framework/skills/voice-agent/SKILL.md +0 -1312
  307. package/framework/skills/zoho-workflow/SKILL.md +0 -51
  308. package/framework/statusline-command.sh +0 -117
  309. package/framework/teams/default/inboxes/plan-04.json +0 -9
  310. package/framework/teams/review-team.md +0 -75
  311. package/framework/teams/ship-team.md +0 -86
  312. package/profiles/fawzi.json +0 -16
  313. package/profiles/hasan.json +0 -16
  314. package/profiles/moayad.json +0 -16
  315. package/templates/CLAUDE-owner.md +0 -52
  316. package/templates/CLAUDE.md.hbs +0 -58
  317. package/templates/env.claude.template +0 -12
  318. package/templates/settings.json +0 -172
  319. package/uninstall.sh +0 -90
  320. /package/{framework/rules → rules}/deployment.md +0 -0
  321. /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();