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.
- package/README.md +50 -0
- package/bin/cli.js +519 -0
- package/framework/agents/architecture-strategist.md +53 -0
- package/framework/agents/backend-agent.md +150 -0
- package/framework/agents/code-simplicity-reviewer.md +86 -0
- package/framework/agents/frontend-agent.md +111 -0
- package/framework/agents/kieran-typescript-reviewer.md +96 -0
- package/framework/agents/performance-oracle.md +111 -0
- package/framework/agents/qualia-codebase-mapper.md +760 -0
- package/framework/agents/qualia-debugger.md +1203 -0
- package/framework/agents/qualia-executor.md +881 -0
- package/framework/agents/qualia-integration-checker.md +423 -0
- package/framework/agents/qualia-phase-researcher.md +453 -0
- package/framework/agents/qualia-plan-checker.md +699 -0
- package/framework/agents/qualia-planner.md +1241 -0
- package/framework/agents/qualia-project-researcher.md +602 -0
- package/framework/agents/qualia-research-synthesizer.md +236 -0
- package/framework/agents/qualia-roadmapper.md +605 -0
- package/framework/agents/qualia-verifier.md +685 -0
- package/framework/agents/team-orchestrator.md +228 -0
- package/framework/agents/teams/full-stack-team.md +48 -0
- package/framework/agents/teams/optimize-team.md +53 -0
- package/framework/agents/teams/review-team.md +62 -0
- package/framework/agents/teams/ship-team.md +86 -0
- package/framework/agents/test-agent.md +182 -0
- package/framework/askpass.sh +2 -0
- package/framework/commands/design.md +53 -0
- package/framework/commands/quick-db.md +22 -0
- package/framework/config/retention.json +35 -0
- package/framework/core/PRINCIPLES.md +77 -0
- package/framework/hooks/auto-format.sh +45 -0
- package/framework/hooks/block-env-edit.sh +42 -0
- package/framework/hooks/branch-guard.sh +46 -0
- package/framework/hooks/confirm-delete.sh +56 -0
- package/framework/hooks/migration-validate.sh +68 -0
- package/framework/hooks/notification-speak.sh +15 -0
- package/framework/hooks/pre-commit.sh +80 -0
- package/framework/hooks/pre-compact.sh +55 -0
- package/framework/hooks/pre-deploy-gate.sh +151 -0
- package/framework/hooks/qualia-colors.sh +32 -0
- package/framework/hooks/retention-cleanup.sh +43 -0
- package/framework/hooks/save-session-state.sh +153 -0
- package/framework/hooks/session-context-loader.sh +28 -0
- package/framework/hooks/session-learn.sh +30 -0
- package/framework/knowledge/claudecode-bible.md +1384 -0
- package/framework/knowledge/client-prefs.md +22 -0
- package/framework/knowledge/common-fixes.md +25 -0
- package/framework/knowledge/deployment-map.md +35 -0
- package/framework/knowledge/email-signature.html +1 -0
- package/framework/knowledge/employees.md +8 -0
- package/framework/knowledge/learned-patterns.md +51 -0
- package/framework/knowledge/optimization-research-2026.md +137 -0
- package/framework/knowledge/qualia-context.md +67 -0
- package/framework/knowledge/supabase-patterns.md +50 -0
- package/framework/knowledge/voice-agent-patterns.md +46 -0
- package/framework/qualia-engine/VERSION +1 -0
- package/framework/qualia-engine/bin/qualia-tools.js +2160 -0
- package/framework/qualia-engine/bin/qualia-tools.test.js +1054 -0
- package/framework/qualia-engine/references/checkpoints.md +775 -0
- package/framework/qualia-engine/references/continuation-format.md +249 -0
- package/framework/qualia-engine/references/decimal-phase-calculation.md +65 -0
- package/framework/qualia-engine/references/design-quality.md +56 -0
- package/framework/qualia-engine/references/git-integration.md +254 -0
- package/framework/qualia-engine/references/git-planning-commit.md +50 -0
- package/framework/qualia-engine/references/model-profile-resolution.md +32 -0
- package/framework/qualia-engine/references/model-profiles.md +73 -0
- package/framework/qualia-engine/references/phase-argument-parsing.md +61 -0
- package/framework/qualia-engine/references/planning-config.md +195 -0
- package/framework/qualia-engine/references/questioning.md +141 -0
- package/framework/qualia-engine/references/tdd.md +263 -0
- package/framework/qualia-engine/references/ui-brand.md +160 -0
- package/framework/qualia-engine/references/verification-patterns.md +612 -0
- package/framework/qualia-engine/templates/DEBUG.md +159 -0
- package/framework/qualia-engine/templates/DESIGN.md +81 -0
- package/framework/qualia-engine/templates/UAT.md +247 -0
- package/framework/qualia-engine/templates/codebase/architecture.md +255 -0
- package/framework/qualia-engine/templates/codebase/concerns.md +310 -0
- package/framework/qualia-engine/templates/codebase/conventions.md +307 -0
- package/framework/qualia-engine/templates/codebase/integrations.md +280 -0
- package/framework/qualia-engine/templates/codebase/stack.md +186 -0
- package/framework/qualia-engine/templates/codebase/structure.md +285 -0
- package/framework/qualia-engine/templates/codebase/testing.md +480 -0
- package/framework/qualia-engine/templates/config.json +35 -0
- package/framework/qualia-engine/templates/context.md +283 -0
- package/framework/qualia-engine/templates/continue-here.md +78 -0
- package/framework/qualia-engine/templates/debug-subagent-prompt.md +91 -0
- package/framework/qualia-engine/templates/discovery.md +146 -0
- package/framework/qualia-engine/templates/milestone-archive.md +123 -0
- package/framework/qualia-engine/templates/milestone.md +115 -0
- package/framework/qualia-engine/templates/phase-prompt.md +567 -0
- package/framework/qualia-engine/templates/planner-subagent-prompt.md +117 -0
- package/framework/qualia-engine/templates/project.md +184 -0
- package/framework/qualia-engine/templates/projects/ai-agent.md +156 -0
- package/framework/qualia-engine/templates/projects/mobile-app.md +181 -0
- package/framework/qualia-engine/templates/projects/voice-agent.md +134 -0
- package/framework/qualia-engine/templates/projects/website.md +137 -0
- package/framework/qualia-engine/templates/requirements.md +231 -0
- package/framework/qualia-engine/templates/research-project/ARCHITECTURE.md +204 -0
- package/framework/qualia-engine/templates/research-project/FEATURES.md +147 -0
- package/framework/qualia-engine/templates/research-project/PITFALLS.md +200 -0
- package/framework/qualia-engine/templates/research-project/STACK.md +120 -0
- package/framework/qualia-engine/templates/research-project/SUMMARY.md +170 -0
- package/framework/qualia-engine/templates/research.md +552 -0
- package/framework/qualia-engine/templates/roadmap.md +202 -0
- package/framework/qualia-engine/templates/state.md +176 -0
- package/framework/qualia-engine/templates/summary-complex.md +59 -0
- package/framework/qualia-engine/templates/summary-minimal.md +41 -0
- package/framework/qualia-engine/templates/summary-standard.md +48 -0
- package/framework/qualia-engine/templates/summary.md +246 -0
- package/framework/qualia-engine/templates/user-setup.md +311 -0
- package/framework/qualia-engine/templates/verification-report.md +322 -0
- package/framework/qualia-engine/workflows/add-phase.md +179 -0
- package/framework/qualia-engine/workflows/add-todo.md +157 -0
- package/framework/qualia-engine/workflows/audit-milestone.md +241 -0
- package/framework/qualia-engine/workflows/check-todos.md +176 -0
- package/framework/qualia-engine/workflows/complete-milestone.md +858 -0
- package/framework/qualia-engine/workflows/diagnose-issues.md +219 -0
- package/framework/qualia-engine/workflows/discovery-phase.md +289 -0
- package/framework/qualia-engine/workflows/discuss-phase.md +534 -0
- package/framework/qualia-engine/workflows/execute-phase.md +559 -0
- package/framework/qualia-engine/workflows/execute-plan.md +438 -0
- package/framework/qualia-engine/workflows/help.md +470 -0
- package/framework/qualia-engine/workflows/insert-phase.md +220 -0
- package/framework/qualia-engine/workflows/list-phase-assumptions.md +178 -0
- package/framework/qualia-engine/workflows/map-codebase.md +327 -0
- package/framework/qualia-engine/workflows/new-milestone.md +363 -0
- package/framework/qualia-engine/workflows/new-project.md +1037 -0
- package/framework/qualia-engine/workflows/pause-work.md +122 -0
- package/framework/qualia-engine/workflows/plan-milestone-gaps.md +256 -0
- package/framework/qualia-engine/workflows/plan-phase.md +422 -0
- package/framework/qualia-engine/workflows/progress.md +354 -0
- package/framework/qualia-engine/workflows/quick.md +252 -0
- package/framework/qualia-engine/workflows/remove-phase.md +326 -0
- package/framework/qualia-engine/workflows/research-phase.md +74 -0
- package/framework/qualia-engine/workflows/resume-project.md +306 -0
- package/framework/qualia-engine/workflows/set-profile.md +80 -0
- package/framework/qualia-engine/workflows/settings.md +145 -0
- package/framework/qualia-engine/workflows/transition.md +556 -0
- package/framework/qualia-engine/workflows/update.md +197 -0
- package/framework/qualia-engine/workflows/verify-phase.md +195 -0
- package/framework/qualia-engine/workflows/verify-work.md +625 -0
- package/framework/rules/context7.md +11 -0
- package/framework/rules/deployment.md +29 -0
- package/framework/rules/frontend.md +33 -0
- package/framework/rules/security.md +12 -0
- package/framework/rules/speed.md +20 -0
- package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
- package/framework/scripts/apply-retention.sh +120 -0
- package/framework/scripts/bootstrap-pop-os.sh +354 -0
- package/framework/scripts/claude-voice +13 -0
- package/framework/scripts/cleanup.sh +131 -0
- package/framework/scripts/cowork-mode.sh +141 -0
- package/framework/scripts/generate-project-claude-md.sh +153 -0
- package/framework/scripts/load-test-webhook.js +172 -0
- package/framework/scripts/say.py +236 -0
- package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +167 -0
- package/framework/scripts/showcase-video-recorder/playwright-helpers.js +216 -0
- package/framework/scripts/speak.py +55 -0
- package/framework/scripts/speak.sh +18 -0
- package/framework/scripts/status.sh +138 -0
- package/framework/scripts/sync-to-framework.sh +65 -0
- package/framework/scripts/voice-hotkey.py +227 -0
- package/framework/scripts/voice-input.sh +51 -0
- package/framework/skills/animate/SKILL.md +202 -0
- package/framework/skills/bolder/SKILL.md +144 -0
- package/framework/skills/browser-qa/SKILL.md +536 -0
- package/framework/skills/clarify/SKILL.md +179 -0
- package/framework/skills/colorize/SKILL.md +170 -0
- package/framework/skills/critique/SKILL.md +126 -0
- package/framework/skills/deep-research/SKILL.md +271 -0
- package/framework/skills/delight/SKILL.md +329 -0
- package/framework/skills/deploy/SKILL.md +261 -0
- package/framework/skills/deploy-verify/SKILL.md +377 -0
- package/framework/skills/deploy-verify/scripts/canary-check.sh +206 -0
- package/framework/skills/deploy-verify/scripts/check-console-errors.js +147 -0
- package/framework/skills/deploy-verify/scripts/check-cwv.js +139 -0
- package/framework/skills/deploy-verify/scripts/project-detect.sh +84 -0
- package/framework/skills/deploy-verify/scripts/verify.sh +548 -0
- package/framework/skills/design-quieter/SKILL.md +130 -0
- package/framework/skills/distill/SKILL.md +149 -0
- package/framework/skills/docs-lookup/SKILL.md +78 -0
- package/framework/skills/fcm-notifications/SKILL.md +125 -0
- package/framework/skills/financial-ledger/SKILL.md +1039 -0
- package/framework/skills/frontend-master/NOTICE.md +4 -0
- package/framework/skills/frontend-master/SKILL.md +127 -0
- package/framework/skills/frontend-master/reference/color-and-contrast.md +132 -0
- package/framework/skills/frontend-master/reference/interaction-design.md +123 -0
- package/framework/skills/frontend-master/reference/motion-design.md +99 -0
- package/framework/skills/frontend-master/reference/responsive-design.md +114 -0
- package/framework/skills/frontend-master/reference/spatial-design.md +100 -0
- package/framework/skills/frontend-master/reference/typography.md +131 -0
- package/framework/skills/frontend-master/reference/ux-writing.md +107 -0
- package/framework/skills/harden/SKILL.md +357 -0
- package/framework/skills/i18n-rtl/SKILL.md +752 -0
- package/framework/skills/learn/SKILL.md +71 -0
- package/framework/skills/memory/SKILL.md +50 -0
- package/framework/skills/mobile-expo/SKILL.md +864 -0
- package/framework/skills/mobile-expo/references/store-checklist.md +550 -0
- package/framework/skills/nestjs-backend/README.md +73 -0
- package/framework/skills/nestjs-backend/SKILL.md +446 -0
- package/framework/skills/nestjs-backend/references/templates.md +1173 -0
- package/framework/skills/normalize/SKILL.md +79 -0
- package/framework/skills/onboard/SKILL.md +242 -0
- package/framework/skills/polish/SKILL.md +209 -0
- package/framework/skills/pr/SKILL.md +66 -0
- package/framework/skills/qualia/SKILL.md +153 -0
- package/framework/skills/qualia-add-todo/SKILL.md +68 -0
- package/framework/skills/qualia-audit-milestone/SKILL.md +92 -0
- package/framework/skills/qualia-check-todos/SKILL.md +55 -0
- package/framework/skills/qualia-complete-milestone/SKILL.md +108 -0
- package/framework/skills/qualia-debug/SKILL.md +149 -0
- package/framework/skills/qualia-design/SKILL.md +203 -0
- package/framework/skills/qualia-discuss-phase/SKILL.md +72 -0
- package/framework/skills/qualia-execute-phase/SKILL.md +86 -0
- package/framework/skills/qualia-help/SKILL.md +67 -0
- package/framework/skills/qualia-idk/SKILL.md +352 -0
- package/framework/skills/qualia-list-phase-assumptions/SKILL.md +67 -0
- package/framework/skills/qualia-new-milestone/SKILL.md +72 -0
- package/framework/skills/qualia-new-project/SKILL.md +92 -0
- package/framework/skills/qualia-optimize/SKILL.md +417 -0
- package/framework/skills/qualia-pause-work/SKILL.md +96 -0
- package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +57 -0
- package/framework/skills/qualia-plan-phase/SKILL.md +101 -0
- package/framework/skills/qualia-progress/SKILL.md +53 -0
- package/framework/skills/qualia-quick/SKILL.md +89 -0
- package/framework/skills/qualia-research-phase/SKILL.md +88 -0
- package/framework/skills/qualia-resume-work/SKILL.md +62 -0
- package/framework/skills/qualia-review/SKILL.md +263 -0
- package/framework/skills/qualia-start/SKILL.md +182 -0
- package/framework/skills/qualia-verify-work/SKILL.md +105 -0
- package/framework/skills/qualia-workflow/SKILL.md +130 -0
- package/framework/skills/rag/SKILL.md +750 -0
- package/framework/skills/responsive/SKILL.md +231 -0
- package/framework/skills/retro/SKILL.md +284 -0
- package/framework/skills/sakani-conventions/SKILL.md +136 -0
- package/framework/skills/sakani-conventions/evals/evals.json +23 -0
- package/framework/skills/sakani-conventions/references/entities.md +365 -0
- package/framework/skills/sakani-conventions/references/error-codes.md +95 -0
- package/framework/skills/seo-master/SKILL.md +490 -0
- package/framework/skills/seo-master/references/checklist.md +199 -0
- package/framework/skills/seo-master/references/structured-data.md +609 -0
- package/framework/skills/ship/SKILL.md +202 -0
- package/framework/skills/stack-researcher/SKILL.md +215 -0
- package/framework/skills/status/SKILL.md +154 -0
- package/framework/skills/status/scripts/health-check.sh +562 -0
- package/framework/skills/subscription-payments/SKILL.md +250 -0
- package/framework/skills/supabase/SKILL.md +973 -0
- package/framework/skills/supabase/references/templates.md +159 -0
- package/framework/skills/team/SKILL.md +67 -0
- package/framework/skills/test-runner/SKILL.md +202 -0
- package/framework/skills/voice-agent/SKILL.md +407 -0
- package/framework/skills/zoho-workflow/SKILL.md +51 -0
- package/framework/statusline-command.sh +117 -0
- package/package.json +24 -0
- package/profiles/fawzi.json +16 -0
- package/profiles/hasan.json +16 -0
- package/profiles/moayad.json +16 -0
- package/templates/CLAUDE-owner.md +52 -0
- package/templates/CLAUDE.md.hbs +58 -0
- package/templates/env.claude.template +12 -0
- 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();
|