qualia-framework 2.1.0

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