@lipter7/blueprint 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +626 -0
- package/agents/bp-codebase-mapper.md +761 -0
- package/agents/bp-debugger.md +1198 -0
- package/agents/bp-executor.md +403 -0
- package/agents/bp-integration-checker.md +423 -0
- package/agents/bp-phase-researcher.md +469 -0
- package/agents/bp-plan-checker.md +622 -0
- package/agents/bp-planner.md +1157 -0
- package/agents/bp-project-researcher.md +618 -0
- package/agents/bp-research-synthesizer.md +236 -0
- package/agents/bp-roadmapper.md +605 -0
- package/agents/bp-verifier.md +523 -0
- package/bin/install.js +1754 -0
- package/blueprint/bin/blueprint-tools.js +4597 -0
- package/blueprint/bin/blueprint-tools.test.js +2033 -0
- package/blueprint/references/checkpoints.md +775 -0
- package/blueprint/references/continuation-format.md +249 -0
- package/blueprint/references/decimal-phase-calculation.md +65 -0
- package/blueprint/references/git-integration.md +248 -0
- package/blueprint/references/git-planning-commit.md +38 -0
- package/blueprint/references/model-profile-resolution.md +32 -0
- package/blueprint/references/model-profiles.md +73 -0
- package/blueprint/references/phase-argument-parsing.md +61 -0
- package/blueprint/references/planning-config.md +194 -0
- package/blueprint/references/questioning.md +141 -0
- package/blueprint/references/tdd.md +263 -0
- package/blueprint/references/ui-brand.md +160 -0
- package/blueprint/references/verification-patterns.md +612 -0
- package/blueprint/templates/DEBUG.md +159 -0
- package/blueprint/templates/UAT.md +247 -0
- package/blueprint/templates/codebase/architecture.md +255 -0
- package/blueprint/templates/codebase/concerns.md +310 -0
- package/blueprint/templates/codebase/conventions.md +307 -0
- package/blueprint/templates/codebase/integrations.md +280 -0
- package/blueprint/templates/codebase/stack.md +186 -0
- package/blueprint/templates/codebase/structure.md +285 -0
- package/blueprint/templates/codebase/testing.md +480 -0
- package/blueprint/templates/config.json +35 -0
- package/blueprint/templates/context.md +283 -0
- package/blueprint/templates/continue-here.md +78 -0
- package/blueprint/templates/debug-subagent-prompt.md +91 -0
- package/blueprint/templates/discovery.md +146 -0
- package/blueprint/templates/milestone-archive.md +123 -0
- package/blueprint/templates/milestone.md +115 -0
- package/blueprint/templates/phase-prompt.md +567 -0
- package/blueprint/templates/planner-subagent-prompt.md +117 -0
- package/blueprint/templates/project.md +184 -0
- package/blueprint/templates/requirements.md +231 -0
- package/blueprint/templates/research-project/ARCHITECTURE.md +204 -0
- package/blueprint/templates/research-project/FEATURES.md +147 -0
- package/blueprint/templates/research-project/PITFALLS.md +200 -0
- package/blueprint/templates/research-project/STACK.md +120 -0
- package/blueprint/templates/research-project/SUMMARY.md +170 -0
- package/blueprint/templates/research.md +552 -0
- package/blueprint/templates/roadmap.md +202 -0
- package/blueprint/templates/state.md +176 -0
- package/blueprint/templates/summary-complex.md +59 -0
- package/blueprint/templates/summary-minimal.md +41 -0
- package/blueprint/templates/summary-standard.md +48 -0
- package/blueprint/templates/summary.md +246 -0
- package/blueprint/templates/user-setup.md +311 -0
- package/blueprint/templates/verification-report.md +322 -0
- package/blueprint/workflows/add-phase.md +111 -0
- package/blueprint/workflows/add-todo.md +157 -0
- package/blueprint/workflows/audit-milestone.md +241 -0
- package/blueprint/workflows/check-todos.md +176 -0
- package/blueprint/workflows/complete-milestone.md +644 -0
- package/blueprint/workflows/diagnose-issues.md +219 -0
- package/blueprint/workflows/discovery-phase.md +289 -0
- package/blueprint/workflows/discuss-phase.md +408 -0
- package/blueprint/workflows/execute-phase.md +338 -0
- package/blueprint/workflows/execute-plan.md +437 -0
- package/blueprint/workflows/help.md +470 -0
- package/blueprint/workflows/insert-phase.md +129 -0
- package/blueprint/workflows/list-phase-assumptions.md +178 -0
- package/blueprint/workflows/map-codebase.md +327 -0
- package/blueprint/workflows/new-milestone.md +373 -0
- package/blueprint/workflows/new-project.md +958 -0
- package/blueprint/workflows/pause-work.md +122 -0
- package/blueprint/workflows/plan-milestone-gaps.md +256 -0
- package/blueprint/workflows/plan-phase.md +376 -0
- package/blueprint/workflows/progress.md +385 -0
- package/blueprint/workflows/quick.md +230 -0
- package/blueprint/workflows/remove-phase.md +154 -0
- package/blueprint/workflows/research-phase.md +74 -0
- package/blueprint/workflows/resume-project.md +306 -0
- package/blueprint/workflows/set-profile.md +80 -0
- package/blueprint/workflows/settings.md +145 -0
- package/blueprint/workflows/transition.md +493 -0
- package/blueprint/workflows/update.md +212 -0
- package/blueprint/workflows/verify-phase.md +226 -0
- package/blueprint/workflows/verify-work.md +570 -0
- package/commands/bp/add-phase.md +39 -0
- package/commands/bp/add-todo.md +42 -0
- package/commands/bp/audit-milestone.md +42 -0
- package/commands/bp/check-todos.md +41 -0
- package/commands/bp/complete-milestone.md +136 -0
- package/commands/bp/debug.md +162 -0
- package/commands/bp/discuss-phase.md +86 -0
- package/commands/bp/execute-phase.md +42 -0
- package/commands/bp/help.md +22 -0
- package/commands/bp/insert-phase.md +33 -0
- package/commands/bp/join-discord.md +18 -0
- package/commands/bp/list-phase-assumptions.md +50 -0
- package/commands/bp/map-codebase.md +71 -0
- package/commands/bp/new-milestone.md +51 -0
- package/commands/bp/new-project.md +42 -0
- package/commands/bp/pause-work.md +35 -0
- package/commands/bp/plan-milestone-gaps.md +40 -0
- package/commands/bp/plan-phase.md +44 -0
- package/commands/bp/progress.md +24 -0
- package/commands/bp/quick.md +38 -0
- package/commands/bp/reapply-patches.md +110 -0
- package/commands/bp/remove-phase.md +32 -0
- package/commands/bp/research-phase.md +187 -0
- package/commands/bp/resume-work.md +40 -0
- package/commands/bp/set-profile.md +34 -0
- package/commands/bp/settings.md +36 -0
- package/commands/bp/update.md +37 -0
- package/commands/bp/verify-work.md +39 -0
- package/hooks/dist/bp-check-update.js +62 -0
- package/hooks/dist/bp-statusline.js +91 -0
- package/package.json +48 -0
- package/scripts/build-hooks.js +42 -0
|
@@ -0,0 +1,4597 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Blueprint Tools — CLI utility for Blueprint workflow operations
|
|
5
|
+
*
|
|
6
|
+
* Replaces repetitive inline bash patterns across ~50 Blueprint command/workflow/agent files.
|
|
7
|
+
* Centralizes: config parsing, model resolution, phase lookup, git commits, summary verification.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node blueprint-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
|
+
* state get [section] Get STATE.md content or section
|
|
15
|
+
* state patch --field val ... Batch update STATE.md fields
|
|
16
|
+
* resolve-model <agent-type> Get model for agent based on profile
|
|
17
|
+
* find-phase <phase> Find phase directory by number
|
|
18
|
+
* commit <message> [--files f1 f2] Commit planning docs
|
|
19
|
+
* verify-summary <path> Verify a SUMMARY.md file
|
|
20
|
+
* generate-slug <text> Convert text to URL-safe slug
|
|
21
|
+
* current-timestamp [format] Get timestamp (full|date|filename)
|
|
22
|
+
* list-todos [area] Count and enumerate pending todos
|
|
23
|
+
* verify-path-exists <path> Check file/directory existence
|
|
24
|
+
* config-ensure-section Initialize .blueprint/config.json
|
|
25
|
+
* history-digest Aggregate all SUMMARY.md data
|
|
26
|
+
* summary-extract <path> [--fields] Extract structured data from SUMMARY.md
|
|
27
|
+
* state-snapshot Structured parse of STATE.md
|
|
28
|
+
* phase-plan-index <phase> Index plans with waves and status
|
|
29
|
+
* websearch <query> Search web via Brave API (if configured)
|
|
30
|
+
* [--limit N] [--freshness day|week|month]
|
|
31
|
+
*
|
|
32
|
+
* Phase Operations:
|
|
33
|
+
* phase next-decimal <phase> Calculate next decimal phase number
|
|
34
|
+
* phase add <description> Append new phase to roadmap + create dir
|
|
35
|
+
* phase insert <after> <description> Insert decimal phase after existing
|
|
36
|
+
* phase remove <phase> [--force] Remove phase, renumber all subsequent
|
|
37
|
+
* phase complete <phase> Mark phase done, update state + roadmap
|
|
38
|
+
*
|
|
39
|
+
* Roadmap Operations:
|
|
40
|
+
* roadmap get-phase <phase> Extract phase section from ROADMAP.md
|
|
41
|
+
* roadmap analyze Full roadmap parse with disk status
|
|
42
|
+
*
|
|
43
|
+
* Milestone Operations:
|
|
44
|
+
* milestone complete <version> Archive milestone, create MILESTONES.md
|
|
45
|
+
* [--name <name>]
|
|
46
|
+
*
|
|
47
|
+
* Validation:
|
|
48
|
+
* validate consistency Check phase numbering, disk/roadmap sync
|
|
49
|
+
*
|
|
50
|
+
* Progress:
|
|
51
|
+
* progress [json|table|bar] Render progress in various formats
|
|
52
|
+
*
|
|
53
|
+
* Todos:
|
|
54
|
+
* todo complete <filename> Move todo from pending to completed
|
|
55
|
+
*
|
|
56
|
+
* Scaffolding:
|
|
57
|
+
* scaffold context --phase <N> Create CONTEXT.md template
|
|
58
|
+
* scaffold uat --phase <N> Create UAT.md template
|
|
59
|
+
* scaffold verification --phase <N> Create VERIFICATION.md template
|
|
60
|
+
* scaffold phase-dir --phase <N> Create phase directory
|
|
61
|
+
* --name <name>
|
|
62
|
+
*
|
|
63
|
+
* Frontmatter CRUD:
|
|
64
|
+
* frontmatter get <file> [--field k] Extract frontmatter as JSON
|
|
65
|
+
* frontmatter set <file> --field k Update single frontmatter field
|
|
66
|
+
* --value jsonVal
|
|
67
|
+
* frontmatter merge <file> Merge JSON into frontmatter
|
|
68
|
+
* --data '{json}'
|
|
69
|
+
* frontmatter validate <file> Validate required fields
|
|
70
|
+
* --schema plan|summary|verification
|
|
71
|
+
*
|
|
72
|
+
* Verification Suite:
|
|
73
|
+
* verify plan-structure <file> Check PLAN.md structure + tasks
|
|
74
|
+
* verify phase-completeness <phase> Check all plans have summaries
|
|
75
|
+
* verify references <file> Check @-refs + paths resolve
|
|
76
|
+
* verify commits <h1> [h2] ... Batch verify commit hashes
|
|
77
|
+
* verify artifacts <plan-file> Check must_haves.artifacts
|
|
78
|
+
* verify key-links <plan-file> Check must_haves.key_links
|
|
79
|
+
*
|
|
80
|
+
* Template Fill:
|
|
81
|
+
* template fill summary --phase N Create pre-filled SUMMARY.md
|
|
82
|
+
* [--plan M] [--name "..."]
|
|
83
|
+
* [--fields '{json}']
|
|
84
|
+
* template fill plan --phase N Create pre-filled PLAN.md
|
|
85
|
+
* [--plan M] [--type execute|tdd]
|
|
86
|
+
* [--wave N] [--fields '{json}']
|
|
87
|
+
* template fill verification Create pre-filled VERIFICATION.md
|
|
88
|
+
* --phase N [--fields '{json}']
|
|
89
|
+
*
|
|
90
|
+
* State Progression:
|
|
91
|
+
* state advance-plan Increment plan counter
|
|
92
|
+
* state record-metric --phase N Record execution metrics
|
|
93
|
+
* --plan M --duration Xmin
|
|
94
|
+
* [--tasks N] [--files N]
|
|
95
|
+
* state update-progress Recalculate progress bar
|
|
96
|
+
* state add-decision --summary "..." Add decision to STATE.md
|
|
97
|
+
* [--phase N] [--rationale "..."]
|
|
98
|
+
* state add-blocker --text "..." Add blocker
|
|
99
|
+
* state resolve-blocker --text "..." Remove blocker
|
|
100
|
+
* state record-session Update session continuity
|
|
101
|
+
* --stopped-at "..."
|
|
102
|
+
* [--resume-file path]
|
|
103
|
+
*
|
|
104
|
+
* Compound Commands (workflow-specific initialization):
|
|
105
|
+
* init execute-phase <phase> All context for execute-phase workflow
|
|
106
|
+
* init plan-phase <phase> All context for plan-phase workflow
|
|
107
|
+
* init new-project All context for new-project workflow
|
|
108
|
+
* init new-milestone All context for new-milestone workflow
|
|
109
|
+
* init quick <description> All context for quick workflow
|
|
110
|
+
* init resume All context for resume-project workflow
|
|
111
|
+
* init verify-work <phase> All context for verify-work workflow
|
|
112
|
+
* init phase-op <phase> Generic phase operation context
|
|
113
|
+
* init todos [area] All context for todo workflows
|
|
114
|
+
* init milestone-op All context for milestone operations
|
|
115
|
+
* init map-codebase All context for map-codebase workflow
|
|
116
|
+
* init progress All context for progress workflow
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
const fs = require('fs');
|
|
120
|
+
const path = require('path');
|
|
121
|
+
const { execSync } = require('child_process');
|
|
122
|
+
|
|
123
|
+
// ─── Model Profile Table ─────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
const MODEL_PROFILES = {
|
|
126
|
+
'bp-planner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
|
|
127
|
+
'bp-roadmapper': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
128
|
+
'bp-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
129
|
+
'bp-phase-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
130
|
+
'bp-project-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
131
|
+
'bp-research-synthesizer': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
132
|
+
'bp-debugger': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
133
|
+
'bp-codebase-mapper': { quality: 'sonnet', balanced: 'haiku', budget: 'haiku' },
|
|
134
|
+
'bp-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
135
|
+
'bp-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
136
|
+
'bp-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
function parseIncludeFlag(args) {
|
|
142
|
+
const includeIndex = args.indexOf('--include');
|
|
143
|
+
if (includeIndex === -1) return new Set();
|
|
144
|
+
const includeValue = args[includeIndex + 1];
|
|
145
|
+
if (!includeValue) return new Set();
|
|
146
|
+
return new Set(includeValue.split(',').map(s => s.trim()));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function safeReadFile(filePath) {
|
|
150
|
+
try {
|
|
151
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function loadConfig(cwd) {
|
|
158
|
+
const configPath = path.join(cwd, '.blueprint', 'config.json');
|
|
159
|
+
const defaults = {
|
|
160
|
+
model_profile: 'balanced',
|
|
161
|
+
commit_docs: true,
|
|
162
|
+
search_gitignored: false,
|
|
163
|
+
branching_strategy: 'none',
|
|
164
|
+
phase_branch_template: 'bp/phase-{phase}-{slug}',
|
|
165
|
+
milestone_branch_template: 'bp/{milestone}-{slug}',
|
|
166
|
+
research: true,
|
|
167
|
+
plan_checker: true,
|
|
168
|
+
verifier: true,
|
|
169
|
+
parallelization: true,
|
|
170
|
+
brave_search: false,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
175
|
+
const parsed = JSON.parse(raw);
|
|
176
|
+
|
|
177
|
+
const get = (key, nested) => {
|
|
178
|
+
if (parsed[key] !== undefined) return parsed[key];
|
|
179
|
+
if (nested && parsed[nested.section] && parsed[nested.section][nested.field] !== undefined) {
|
|
180
|
+
return parsed[nested.section][nested.field];
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const parallelization = (() => {
|
|
186
|
+
const val = get('parallelization');
|
|
187
|
+
if (typeof val === 'boolean') return val;
|
|
188
|
+
if (typeof val === 'object' && val !== null && 'enabled' in val) return val.enabled;
|
|
189
|
+
return defaults.parallelization;
|
|
190
|
+
})();
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
model_profile: get('model_profile') ?? defaults.model_profile,
|
|
194
|
+
commit_docs: get('commit_docs', { section: 'planning', field: 'commit_docs' }) ?? defaults.commit_docs,
|
|
195
|
+
search_gitignored: get('search_gitignored', { section: 'planning', field: 'search_gitignored' }) ?? defaults.search_gitignored,
|
|
196
|
+
branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy,
|
|
197
|
+
phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
|
|
198
|
+
milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
|
|
199
|
+
research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
|
|
200
|
+
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
|
|
201
|
+
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
|
|
202
|
+
parallelization,
|
|
203
|
+
brave_search: get('brave_search') ?? defaults.brave_search,
|
|
204
|
+
};
|
|
205
|
+
} catch {
|
|
206
|
+
return defaults;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isGitIgnored(cwd, targetPath) {
|
|
211
|
+
try {
|
|
212
|
+
execSync('git check-ignore -q -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
|
|
213
|
+
cwd,
|
|
214
|
+
stdio: 'pipe',
|
|
215
|
+
});
|
|
216
|
+
return true;
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function execGit(cwd, args) {
|
|
223
|
+
try {
|
|
224
|
+
const escaped = args.map(a => {
|
|
225
|
+
if (/^[a-zA-Z0-9._\-/=:@]+$/.test(a)) return a;
|
|
226
|
+
return "'" + a.replace(/'/g, "'\\''") + "'";
|
|
227
|
+
});
|
|
228
|
+
const stdout = execSync('git ' + escaped.join(' '), {
|
|
229
|
+
cwd,
|
|
230
|
+
stdio: 'pipe',
|
|
231
|
+
encoding: 'utf-8',
|
|
232
|
+
});
|
|
233
|
+
return { exitCode: 0, stdout: stdout.trim(), stderr: '' };
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return {
|
|
236
|
+
exitCode: err.status ?? 1,
|
|
237
|
+
stdout: (err.stdout ?? '').toString().trim(),
|
|
238
|
+
stderr: (err.stderr ?? '').toString().trim(),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizePhaseName(phase) {
|
|
244
|
+
const match = phase.match(/^(\d+(?:\.\d+)?)/);
|
|
245
|
+
if (!match) return phase;
|
|
246
|
+
const num = match[1];
|
|
247
|
+
const parts = num.split('.');
|
|
248
|
+
const padded = parts[0].padStart(2, '0');
|
|
249
|
+
return parts.length > 1 ? `${padded}.${parts[1]}` : padded;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function extractFrontmatter(content) {
|
|
253
|
+
const frontmatter = {};
|
|
254
|
+
const match = content.match(/^---\n([\s\S]+?)\n---/);
|
|
255
|
+
if (!match) return frontmatter;
|
|
256
|
+
|
|
257
|
+
const yaml = match[1];
|
|
258
|
+
const lines = yaml.split('\n');
|
|
259
|
+
|
|
260
|
+
// Stack to track nested objects: [{obj, key, indent}]
|
|
261
|
+
// obj = object to write to, key = current key collecting array items, indent = indentation level
|
|
262
|
+
let stack = [{ obj: frontmatter, key: null, indent: -1 }];
|
|
263
|
+
|
|
264
|
+
for (const line of lines) {
|
|
265
|
+
// Skip empty lines
|
|
266
|
+
if (line.trim() === '') continue;
|
|
267
|
+
|
|
268
|
+
// Calculate indentation (number of leading spaces)
|
|
269
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
270
|
+
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
271
|
+
|
|
272
|
+
// Pop stack back to appropriate level
|
|
273
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
274
|
+
stack.pop();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const current = stack[stack.length - 1];
|
|
278
|
+
|
|
279
|
+
// Check for key: value pattern
|
|
280
|
+
const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)/);
|
|
281
|
+
if (keyMatch) {
|
|
282
|
+
const key = keyMatch[2];
|
|
283
|
+
const value = keyMatch[3].trim();
|
|
284
|
+
|
|
285
|
+
if (value === '' || value === '[') {
|
|
286
|
+
// Key with no value or opening bracket — could be nested object or array
|
|
287
|
+
// We'll determine based on next lines, for now create placeholder
|
|
288
|
+
current.obj[key] = value === '[' ? [] : {};
|
|
289
|
+
current.key = null;
|
|
290
|
+
// Push new context for potential nested content
|
|
291
|
+
stack.push({ obj: current.obj[key], key: null, indent });
|
|
292
|
+
} else if (value.startsWith('[') && value.endsWith(']')) {
|
|
293
|
+
// Inline array: key: [a, b, c]
|
|
294
|
+
current.obj[key] = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
295
|
+
current.key = null;
|
|
296
|
+
} else {
|
|
297
|
+
// Simple key: value
|
|
298
|
+
current.obj[key] = value.replace(/^["']|["']$/g, '');
|
|
299
|
+
current.key = null;
|
|
300
|
+
}
|
|
301
|
+
} else if (line.trim().startsWith('- ')) {
|
|
302
|
+
// Array item
|
|
303
|
+
const itemValue = line.trim().slice(2).replace(/^["']|["']$/g, '');
|
|
304
|
+
|
|
305
|
+
// If current context is an empty object, convert to array
|
|
306
|
+
if (typeof current.obj === 'object' && !Array.isArray(current.obj) && Object.keys(current.obj).length === 0) {
|
|
307
|
+
// Find the key in parent that points to this object and convert it
|
|
308
|
+
const parent = stack.length > 1 ? stack[stack.length - 2] : null;
|
|
309
|
+
if (parent) {
|
|
310
|
+
for (const k of Object.keys(parent.obj)) {
|
|
311
|
+
if (parent.obj[k] === current.obj) {
|
|
312
|
+
parent.obj[k] = [itemValue];
|
|
313
|
+
current.obj = parent.obj[k];
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} else if (Array.isArray(current.obj)) {
|
|
319
|
+
current.obj.push(itemValue);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return frontmatter;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function reconstructFrontmatter(obj) {
|
|
328
|
+
const lines = [];
|
|
329
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
330
|
+
if (value === null || value === undefined) continue;
|
|
331
|
+
if (Array.isArray(value)) {
|
|
332
|
+
if (value.length === 0) {
|
|
333
|
+
lines.push(`${key}: []`);
|
|
334
|
+
} else if (value.every(v => typeof v === 'string') && value.length <= 3 && value.join(', ').length < 60) {
|
|
335
|
+
lines.push(`${key}: [${value.join(', ')}]`);
|
|
336
|
+
} else {
|
|
337
|
+
lines.push(`${key}:`);
|
|
338
|
+
for (const item of value) {
|
|
339
|
+
lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} else if (typeof value === 'object') {
|
|
343
|
+
lines.push(`${key}:`);
|
|
344
|
+
for (const [subkey, subval] of Object.entries(value)) {
|
|
345
|
+
if (subval === null || subval === undefined) continue;
|
|
346
|
+
if (Array.isArray(subval)) {
|
|
347
|
+
if (subval.length === 0) {
|
|
348
|
+
lines.push(` ${subkey}: []`);
|
|
349
|
+
} else if (subval.every(v => typeof v === 'string') && subval.length <= 3 && subval.join(', ').length < 60) {
|
|
350
|
+
lines.push(` ${subkey}: [${subval.join(', ')}]`);
|
|
351
|
+
} else {
|
|
352
|
+
lines.push(` ${subkey}:`);
|
|
353
|
+
for (const item of subval) {
|
|
354
|
+
lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} else if (typeof subval === 'object') {
|
|
358
|
+
lines.push(` ${subkey}:`);
|
|
359
|
+
for (const [subsubkey, subsubval] of Object.entries(subval)) {
|
|
360
|
+
if (subsubval === null || subsubval === undefined) continue;
|
|
361
|
+
if (Array.isArray(subsubval)) {
|
|
362
|
+
if (subsubval.length === 0) {
|
|
363
|
+
lines.push(` ${subsubkey}: []`);
|
|
364
|
+
} else {
|
|
365
|
+
lines.push(` ${subsubkey}:`);
|
|
366
|
+
for (const item of subsubval) {
|
|
367
|
+
lines.push(` - ${item}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
lines.push(` ${subsubkey}: ${subsubval}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
const sv = String(subval);
|
|
376
|
+
lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') ? `"${sv}"` : sv}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
const sv = String(value);
|
|
381
|
+
if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{')) {
|
|
382
|
+
lines.push(`${key}: "${sv}"`);
|
|
383
|
+
} else {
|
|
384
|
+
lines.push(`${key}: ${sv}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return lines.join('\n');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function spliceFrontmatter(content, newObj) {
|
|
392
|
+
const yamlStr = reconstructFrontmatter(newObj);
|
|
393
|
+
const match = content.match(/^---\n[\s\S]+?\n---/);
|
|
394
|
+
if (match) {
|
|
395
|
+
return `---\n${yamlStr}\n---` + content.slice(match[0].length);
|
|
396
|
+
}
|
|
397
|
+
return `---\n${yamlStr}\n---\n\n` + content;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function parseMustHavesBlock(content, blockName) {
|
|
401
|
+
// Extract a specific block from must_haves in raw frontmatter YAML
|
|
402
|
+
// Handles 3-level nesting: must_haves > artifacts/key_links > [{path, provides, ...}]
|
|
403
|
+
const fmMatch = content.match(/^---\n([\s\S]+?)\n---/);
|
|
404
|
+
if (!fmMatch) return [];
|
|
405
|
+
|
|
406
|
+
const yaml = fmMatch[1];
|
|
407
|
+
// Find the block (e.g., "truths:", "artifacts:", "key_links:")
|
|
408
|
+
const blockPattern = new RegExp(`^\\s{4}${blockName}:\\s*$`, 'm');
|
|
409
|
+
const blockStart = yaml.search(blockPattern);
|
|
410
|
+
if (blockStart === -1) return [];
|
|
411
|
+
|
|
412
|
+
const afterBlock = yaml.slice(blockStart);
|
|
413
|
+
const blockLines = afterBlock.split('\n').slice(1); // skip the header line
|
|
414
|
+
|
|
415
|
+
const items = [];
|
|
416
|
+
let current = null;
|
|
417
|
+
|
|
418
|
+
for (const line of blockLines) {
|
|
419
|
+
// Stop at same or lower indent level (non-continuation)
|
|
420
|
+
if (line.trim() === '') continue;
|
|
421
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
422
|
+
if (indent <= 4 && line.trim() !== '') break; // back to must_haves level or higher
|
|
423
|
+
|
|
424
|
+
if (line.match(/^\s{6}-\s+/)) {
|
|
425
|
+
// New list item at 6-space indent
|
|
426
|
+
if (current) items.push(current);
|
|
427
|
+
current = {};
|
|
428
|
+
// Check if it's a simple string item
|
|
429
|
+
const simpleMatch = line.match(/^\s{6}-\s+"?([^"]+)"?\s*$/);
|
|
430
|
+
if (simpleMatch && !line.includes(':')) {
|
|
431
|
+
current = simpleMatch[1];
|
|
432
|
+
} else {
|
|
433
|
+
// Key-value on same line as dash: "- path: value"
|
|
434
|
+
const kvMatch = line.match(/^\s{6}-\s+(\w+):\s*"?([^"]*)"?\s*$/);
|
|
435
|
+
if (kvMatch) {
|
|
436
|
+
current = {};
|
|
437
|
+
current[kvMatch[1]] = kvMatch[2];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} else if (current && typeof current === 'object') {
|
|
441
|
+
// Continuation key-value at 8+ space indent
|
|
442
|
+
const kvMatch = line.match(/^\s{8,}(\w+):\s*"?([^"]*)"?\s*$/);
|
|
443
|
+
if (kvMatch) {
|
|
444
|
+
const val = kvMatch[2];
|
|
445
|
+
// Try to parse as number
|
|
446
|
+
current[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
|
|
447
|
+
}
|
|
448
|
+
// Array items under a key
|
|
449
|
+
const arrMatch = line.match(/^\s{10,}-\s+"?([^"]+)"?\s*$/);
|
|
450
|
+
if (arrMatch) {
|
|
451
|
+
// Find the last key added and convert to array
|
|
452
|
+
const keys = Object.keys(current);
|
|
453
|
+
const lastKey = keys[keys.length - 1];
|
|
454
|
+
if (lastKey && !Array.isArray(current[lastKey])) {
|
|
455
|
+
current[lastKey] = current[lastKey] ? [current[lastKey]] : [];
|
|
456
|
+
}
|
|
457
|
+
if (lastKey) current[lastKey].push(arrMatch[1]);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (current) items.push(current);
|
|
462
|
+
|
|
463
|
+
return items;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function output(result, raw, rawValue) {
|
|
467
|
+
if (raw && rawValue !== undefined) {
|
|
468
|
+
process.stdout.write(String(rawValue));
|
|
469
|
+
} else {
|
|
470
|
+
process.stdout.write(JSON.stringify(result, null, 2));
|
|
471
|
+
}
|
|
472
|
+
process.exit(0);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function error(message) {
|
|
476
|
+
process.stderr.write('Error: ' + message + '\n');
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
function cmdGenerateSlug(text, raw) {
|
|
483
|
+
if (!text) {
|
|
484
|
+
error('text required for slug generation');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const slug = text
|
|
488
|
+
.toLowerCase()
|
|
489
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
490
|
+
.replace(/^-+|-+$/g, '');
|
|
491
|
+
|
|
492
|
+
const result = { slug };
|
|
493
|
+
output(result, raw, slug);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function cmdCurrentTimestamp(format, raw) {
|
|
497
|
+
const now = new Date();
|
|
498
|
+
let result;
|
|
499
|
+
|
|
500
|
+
switch (format) {
|
|
501
|
+
case 'date':
|
|
502
|
+
result = now.toISOString().split('T')[0];
|
|
503
|
+
break;
|
|
504
|
+
case 'filename':
|
|
505
|
+
result = now.toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
506
|
+
break;
|
|
507
|
+
case 'full':
|
|
508
|
+
default:
|
|
509
|
+
result = now.toISOString();
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
output({ timestamp: result }, raw, result);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function cmdListTodos(cwd, area, raw) {
|
|
517
|
+
const pendingDir = path.join(cwd, '.blueprint', 'todos', 'pending');
|
|
518
|
+
|
|
519
|
+
let count = 0;
|
|
520
|
+
const todos = [];
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
|
|
524
|
+
|
|
525
|
+
for (const file of files) {
|
|
526
|
+
try {
|
|
527
|
+
const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
|
|
528
|
+
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
|
529
|
+
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
530
|
+
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
|
531
|
+
|
|
532
|
+
const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
|
|
533
|
+
|
|
534
|
+
// Apply area filter if specified
|
|
535
|
+
if (area && todoArea !== area) continue;
|
|
536
|
+
|
|
537
|
+
count++;
|
|
538
|
+
todos.push({
|
|
539
|
+
file,
|
|
540
|
+
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
541
|
+
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
542
|
+
area: todoArea,
|
|
543
|
+
path: path.join('.blueprint', 'todos', 'pending', file),
|
|
544
|
+
});
|
|
545
|
+
} catch {}
|
|
546
|
+
}
|
|
547
|
+
} catch {}
|
|
548
|
+
|
|
549
|
+
const result = { count, todos };
|
|
550
|
+
output(result, raw, count.toString());
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function cmdVerifyPathExists(cwd, targetPath, raw) {
|
|
554
|
+
if (!targetPath) {
|
|
555
|
+
error('path required for verification');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
const stats = fs.statSync(fullPath);
|
|
562
|
+
const type = stats.isDirectory() ? 'directory' : stats.isFile() ? 'file' : 'other';
|
|
563
|
+
const result = { exists: true, type };
|
|
564
|
+
output(result, raw, 'true');
|
|
565
|
+
} catch {
|
|
566
|
+
const result = { exists: false, type: null };
|
|
567
|
+
output(result, raw, 'false');
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function cmdConfigEnsureSection(cwd, raw) {
|
|
572
|
+
const configPath = path.join(cwd, '.blueprint', 'config.json');
|
|
573
|
+
const planningDir = path.join(cwd, '.blueprint');
|
|
574
|
+
|
|
575
|
+
// Ensure .blueprint directory exists
|
|
576
|
+
try {
|
|
577
|
+
if (!fs.existsSync(planningDir)) {
|
|
578
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
579
|
+
}
|
|
580
|
+
} catch (err) {
|
|
581
|
+
error('Failed to create .blueprint directory: ' + err.message);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Check if config already exists
|
|
585
|
+
if (fs.existsSync(configPath)) {
|
|
586
|
+
const result = { created: false, reason: 'already_exists' };
|
|
587
|
+
output(result, raw, 'exists');
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Detect Brave Search API key availability
|
|
592
|
+
const homedir = require('os').homedir();
|
|
593
|
+
const braveKeyFile = path.join(homedir, '.blueprint', 'brave_api_key');
|
|
594
|
+
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
|
|
595
|
+
|
|
596
|
+
// Create default config
|
|
597
|
+
const defaults = {
|
|
598
|
+
model_profile: 'balanced',
|
|
599
|
+
commit_docs: true,
|
|
600
|
+
search_gitignored: false,
|
|
601
|
+
branching_strategy: 'none',
|
|
602
|
+
phase_branch_template: 'bp/phase-{phase}-{slug}',
|
|
603
|
+
milestone_branch_template: 'bp/{milestone}-{slug}',
|
|
604
|
+
workflow: {
|
|
605
|
+
research: true,
|
|
606
|
+
plan_check: true,
|
|
607
|
+
verifier: true,
|
|
608
|
+
},
|
|
609
|
+
parallelization: true,
|
|
610
|
+
brave_search: hasBraveSearch,
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
|
|
615
|
+
const result = { created: true, path: '.blueprint/config.json' };
|
|
616
|
+
output(result, raw, 'created');
|
|
617
|
+
} catch (err) {
|
|
618
|
+
error('Failed to create config.json: ' + err.message);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
623
|
+
const configPath = path.join(cwd, '.blueprint', 'config.json');
|
|
624
|
+
|
|
625
|
+
if (!keyPath) {
|
|
626
|
+
error('Usage: config-set <key.path> <value>');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Parse value (handle booleans and numbers)
|
|
630
|
+
let parsedValue = value;
|
|
631
|
+
if (value === 'true') parsedValue = true;
|
|
632
|
+
else if (value === 'false') parsedValue = false;
|
|
633
|
+
else if (!isNaN(value) && value !== '') parsedValue = Number(value);
|
|
634
|
+
|
|
635
|
+
// Load existing config or start with empty object
|
|
636
|
+
let config = {};
|
|
637
|
+
try {
|
|
638
|
+
if (fs.existsSync(configPath)) {
|
|
639
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
640
|
+
}
|
|
641
|
+
} catch (err) {
|
|
642
|
+
error('Failed to read config.json: ' + err.message);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Set nested value using dot notation (e.g., "workflow.research")
|
|
646
|
+
const keys = keyPath.split('.');
|
|
647
|
+
let current = config;
|
|
648
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
649
|
+
const key = keys[i];
|
|
650
|
+
if (current[key] === undefined || typeof current[key] !== 'object') {
|
|
651
|
+
current[key] = {};
|
|
652
|
+
}
|
|
653
|
+
current = current[key];
|
|
654
|
+
}
|
|
655
|
+
current[keys[keys.length - 1]] = parsedValue;
|
|
656
|
+
|
|
657
|
+
// Write back
|
|
658
|
+
try {
|
|
659
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
660
|
+
const result = { updated: true, key: keyPath, value: parsedValue };
|
|
661
|
+
output(result, raw, `${keyPath}=${parsedValue}`);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
error('Failed to write config.json: ' + err.message);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function cmdHistoryDigest(cwd, raw) {
|
|
668
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
669
|
+
const digest = { phases: {}, decisions: [], tech_stack: new Set() };
|
|
670
|
+
|
|
671
|
+
if (!fs.existsSync(phasesDir)) {
|
|
672
|
+
digest.tech_stack = [];
|
|
673
|
+
output(digest, raw);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
679
|
+
.filter(e => e.isDirectory())
|
|
680
|
+
.map(e => e.name)
|
|
681
|
+
.sort();
|
|
682
|
+
|
|
683
|
+
for (const dir of phaseDirs) {
|
|
684
|
+
const dirPath = path.join(phasesDir, dir);
|
|
685
|
+
const summaries = fs.readdirSync(dirPath).filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
686
|
+
|
|
687
|
+
for (const summary of summaries) {
|
|
688
|
+
try {
|
|
689
|
+
const content = fs.readFileSync(path.join(dirPath, summary), 'utf-8');
|
|
690
|
+
const fm = extractFrontmatter(content);
|
|
691
|
+
|
|
692
|
+
const phaseNum = fm.phase || dir.split('-')[0];
|
|
693
|
+
|
|
694
|
+
if (!digest.phases[phaseNum]) {
|
|
695
|
+
digest.phases[phaseNum] = {
|
|
696
|
+
name: fm.name || dir.split('-').slice(1).join(' ') || 'Unknown',
|
|
697
|
+
provides: new Set(),
|
|
698
|
+
affects: new Set(),
|
|
699
|
+
patterns: new Set(),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Merge provides
|
|
704
|
+
if (fm['dependency-graph'] && fm['dependency-graph'].provides) {
|
|
705
|
+
fm['dependency-graph'].provides.forEach(p => digest.phases[phaseNum].provides.add(p));
|
|
706
|
+
} else if (fm.provides) {
|
|
707
|
+
fm.provides.forEach(p => digest.phases[phaseNum].provides.add(p));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Merge affects
|
|
711
|
+
if (fm['dependency-graph'] && fm['dependency-graph'].affects) {
|
|
712
|
+
fm['dependency-graph'].affects.forEach(a => digest.phases[phaseNum].affects.add(a));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Merge patterns
|
|
716
|
+
if (fm['patterns-established']) {
|
|
717
|
+
fm['patterns-established'].forEach(p => digest.phases[phaseNum].patterns.add(p));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Merge decisions
|
|
721
|
+
if (fm['key-decisions']) {
|
|
722
|
+
fm['key-decisions'].forEach(d => {
|
|
723
|
+
digest.decisions.push({ phase: phaseNum, decision: d });
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Merge tech stack
|
|
728
|
+
if (fm['tech-stack'] && fm['tech-stack'].added) {
|
|
729
|
+
fm['tech-stack'].added.forEach(t => digest.tech_stack.add(typeof t === 'string' ? t : t.name));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
} catch (e) {
|
|
733
|
+
// Skip malformed summaries
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Convert Sets to Arrays for JSON output
|
|
739
|
+
Object.keys(digest.phases).forEach(p => {
|
|
740
|
+
digest.phases[p].provides = [...digest.phases[p].provides];
|
|
741
|
+
digest.phases[p].affects = [...digest.phases[p].affects];
|
|
742
|
+
digest.phases[p].patterns = [...digest.phases[p].patterns];
|
|
743
|
+
});
|
|
744
|
+
digest.tech_stack = [...digest.tech_stack];
|
|
745
|
+
|
|
746
|
+
output(digest, raw);
|
|
747
|
+
} catch (e) {
|
|
748
|
+
error('Failed to generate history digest: ' + e.message);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function cmdPhasesList(cwd, options, raw) {
|
|
753
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
754
|
+
const { type, phase } = options;
|
|
755
|
+
|
|
756
|
+
// If no phases directory, return empty
|
|
757
|
+
if (!fs.existsSync(phasesDir)) {
|
|
758
|
+
if (type) {
|
|
759
|
+
output({ files: [], count: 0 }, raw, '');
|
|
760
|
+
} else {
|
|
761
|
+
output({ directories: [], count: 0 }, raw, '');
|
|
762
|
+
}
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
// Get all phase directories
|
|
768
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
769
|
+
let dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
770
|
+
|
|
771
|
+
// Sort numerically (handles decimals: 01, 02, 02.1, 02.2, 03)
|
|
772
|
+
dirs.sort((a, b) => {
|
|
773
|
+
const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
774
|
+
const bNum = parseFloat(b.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
775
|
+
return aNum - bNum;
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// If filtering by phase number
|
|
779
|
+
if (phase) {
|
|
780
|
+
const normalized = normalizePhaseName(phase);
|
|
781
|
+
const match = dirs.find(d => d.startsWith(normalized));
|
|
782
|
+
if (!match) {
|
|
783
|
+
output({ files: [], count: 0, phase_dir: null, error: 'Phase not found' }, raw, '');
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
dirs = [match];
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// If listing files of a specific type
|
|
790
|
+
if (type) {
|
|
791
|
+
const files = [];
|
|
792
|
+
for (const dir of dirs) {
|
|
793
|
+
const dirPath = path.join(phasesDir, dir);
|
|
794
|
+
const dirFiles = fs.readdirSync(dirPath);
|
|
795
|
+
|
|
796
|
+
let filtered;
|
|
797
|
+
if (type === 'plans') {
|
|
798
|
+
filtered = dirFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
799
|
+
} else if (type === 'summaries') {
|
|
800
|
+
filtered = dirFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
801
|
+
} else {
|
|
802
|
+
filtered = dirFiles;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
files.push(...filtered.sort());
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const result = {
|
|
809
|
+
files,
|
|
810
|
+
count: files.length,
|
|
811
|
+
phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)?-?/, '') : null,
|
|
812
|
+
};
|
|
813
|
+
output(result, raw, files.join('\n'));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Default: list directories
|
|
818
|
+
output({ directories: dirs, count: dirs.length }, raw, dirs.join('\n'));
|
|
819
|
+
} catch (e) {
|
|
820
|
+
error('Failed to list phases: ' + e.message);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
825
|
+
const roadmapPath = path.join(cwd, '.blueprint', 'ROADMAP.md');
|
|
826
|
+
|
|
827
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
828
|
+
output({ found: false, error: 'ROADMAP.md not found' }, raw, '');
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
834
|
+
|
|
835
|
+
// Escape special regex chars in phase number, handle decimal
|
|
836
|
+
const escapedPhase = phaseNum.replace(/\./g, '\\.');
|
|
837
|
+
|
|
838
|
+
// Match "### Phase X:" or "### Phase X.Y:" with optional name
|
|
839
|
+
const phasePattern = new RegExp(
|
|
840
|
+
`###\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
|
|
841
|
+
'i'
|
|
842
|
+
);
|
|
843
|
+
const headerMatch = content.match(phasePattern);
|
|
844
|
+
|
|
845
|
+
if (!headerMatch) {
|
|
846
|
+
output({ found: false, phase_number: phaseNum }, raw, '');
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const phaseName = headerMatch[1].trim();
|
|
851
|
+
const headerIndex = headerMatch.index;
|
|
852
|
+
|
|
853
|
+
// Find the end of this section (next ### or end of file)
|
|
854
|
+
const restOfContent = content.slice(headerIndex);
|
|
855
|
+
const nextHeaderMatch = restOfContent.match(/\n###\s+Phase\s+\d/i);
|
|
856
|
+
const sectionEnd = nextHeaderMatch
|
|
857
|
+
? headerIndex + nextHeaderMatch.index
|
|
858
|
+
: content.length;
|
|
859
|
+
|
|
860
|
+
const section = content.slice(headerIndex, sectionEnd).trim();
|
|
861
|
+
|
|
862
|
+
// Extract goal if present
|
|
863
|
+
const goalMatch = section.match(/\*\*Goal:\*\*\s*([^\n]+)/i);
|
|
864
|
+
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
865
|
+
|
|
866
|
+
output(
|
|
867
|
+
{
|
|
868
|
+
found: true,
|
|
869
|
+
phase_number: phaseNum,
|
|
870
|
+
phase_name: phaseName,
|
|
871
|
+
goal,
|
|
872
|
+
section,
|
|
873
|
+
},
|
|
874
|
+
raw,
|
|
875
|
+
section
|
|
876
|
+
);
|
|
877
|
+
} catch (e) {
|
|
878
|
+
error('Failed to read ROADMAP.md: ' + e.message);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
883
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
884
|
+
const normalized = normalizePhaseName(basePhase);
|
|
885
|
+
|
|
886
|
+
// Check if phases directory exists
|
|
887
|
+
if (!fs.existsSync(phasesDir)) {
|
|
888
|
+
output(
|
|
889
|
+
{
|
|
890
|
+
found: false,
|
|
891
|
+
base_phase: normalized,
|
|
892
|
+
next: `${normalized}.1`,
|
|
893
|
+
existing: [],
|
|
894
|
+
},
|
|
895
|
+
raw,
|
|
896
|
+
`${normalized}.1`
|
|
897
|
+
);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
try {
|
|
902
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
903
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
904
|
+
|
|
905
|
+
// Check if base phase exists
|
|
906
|
+
const baseExists = dirs.some(d => d.startsWith(normalized + '-') || d === normalized);
|
|
907
|
+
|
|
908
|
+
// Find existing decimal phases for this base
|
|
909
|
+
const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
|
|
910
|
+
const existingDecimals = [];
|
|
911
|
+
|
|
912
|
+
for (const dir of dirs) {
|
|
913
|
+
const match = dir.match(decimalPattern);
|
|
914
|
+
if (match) {
|
|
915
|
+
existingDecimals.push(`${normalized}.${match[1]}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Sort numerically
|
|
920
|
+
existingDecimals.sort((a, b) => {
|
|
921
|
+
const aNum = parseFloat(a);
|
|
922
|
+
const bNum = parseFloat(b);
|
|
923
|
+
return aNum - bNum;
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// Calculate next decimal
|
|
927
|
+
let nextDecimal;
|
|
928
|
+
if (existingDecimals.length === 0) {
|
|
929
|
+
nextDecimal = `${normalized}.1`;
|
|
930
|
+
} else {
|
|
931
|
+
const lastDecimal = existingDecimals[existingDecimals.length - 1];
|
|
932
|
+
const lastNum = parseInt(lastDecimal.split('.')[1], 10);
|
|
933
|
+
nextDecimal = `${normalized}.${lastNum + 1}`;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
output(
|
|
937
|
+
{
|
|
938
|
+
found: baseExists,
|
|
939
|
+
base_phase: normalized,
|
|
940
|
+
next: nextDecimal,
|
|
941
|
+
existing: existingDecimals,
|
|
942
|
+
},
|
|
943
|
+
raw,
|
|
944
|
+
nextDecimal
|
|
945
|
+
);
|
|
946
|
+
} catch (e) {
|
|
947
|
+
error('Failed to calculate next decimal phase: ' + e.message);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function cmdStateLoad(cwd, raw) {
|
|
952
|
+
const config = loadConfig(cwd);
|
|
953
|
+
const planningDir = path.join(cwd, '.blueprint');
|
|
954
|
+
|
|
955
|
+
let stateRaw = '';
|
|
956
|
+
try {
|
|
957
|
+
stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8');
|
|
958
|
+
} catch {}
|
|
959
|
+
|
|
960
|
+
const configExists = fs.existsSync(path.join(planningDir, 'config.json'));
|
|
961
|
+
const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
|
|
962
|
+
const stateExists = stateRaw.length > 0;
|
|
963
|
+
|
|
964
|
+
const result = {
|
|
965
|
+
config,
|
|
966
|
+
state_raw: stateRaw,
|
|
967
|
+
state_exists: stateExists,
|
|
968
|
+
roadmap_exists: roadmapExists,
|
|
969
|
+
config_exists: configExists,
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// For --raw, output a condensed key=value format
|
|
973
|
+
if (raw) {
|
|
974
|
+
const c = config;
|
|
975
|
+
const lines = [
|
|
976
|
+
`model_profile=${c.model_profile}`,
|
|
977
|
+
`commit_docs=${c.commit_docs}`,
|
|
978
|
+
`branching_strategy=${c.branching_strategy}`,
|
|
979
|
+
`phase_branch_template=${c.phase_branch_template}`,
|
|
980
|
+
`milestone_branch_template=${c.milestone_branch_template}`,
|
|
981
|
+
`parallelization=${c.parallelization}`,
|
|
982
|
+
`research=${c.research}`,
|
|
983
|
+
`plan_checker=${c.plan_checker}`,
|
|
984
|
+
`verifier=${c.verifier}`,
|
|
985
|
+
`config_exists=${configExists}`,
|
|
986
|
+
`roadmap_exists=${roadmapExists}`,
|
|
987
|
+
`state_exists=${stateExists}`,
|
|
988
|
+
];
|
|
989
|
+
process.stdout.write(lines.join('\n'));
|
|
990
|
+
process.exit(0);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
output(result);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function cmdStateGet(cwd, section, raw) {
|
|
997
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
998
|
+
try {
|
|
999
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
1000
|
+
|
|
1001
|
+
if (!section) {
|
|
1002
|
+
output({ content }, raw, content);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Try to find markdown section or field
|
|
1007
|
+
const fieldEscaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1008
|
+
|
|
1009
|
+
// Check for **field:** value
|
|
1010
|
+
const fieldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
|
|
1011
|
+
const fieldMatch = content.match(fieldPattern);
|
|
1012
|
+
if (fieldMatch) {
|
|
1013
|
+
output({ [section]: fieldMatch[1].trim() }, raw, fieldMatch[1].trim());
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Check for ## Section
|
|
1018
|
+
const sectionPattern = new RegExp(`##\\s*${fieldEscaped}\\s*\n([\\s\\S]*?)(?=\\n##|$)`, 'i');
|
|
1019
|
+
const sectionMatch = content.match(sectionPattern);
|
|
1020
|
+
if (sectionMatch) {
|
|
1021
|
+
output({ [section]: sectionMatch[1].trim() }, raw, sectionMatch[1].trim());
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
output({ error: `Section or field "${section}" not found` }, raw, '');
|
|
1026
|
+
} catch {
|
|
1027
|
+
error('STATE.md not found');
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function cmdStatePatch(cwd, patches, raw) {
|
|
1032
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
1033
|
+
try {
|
|
1034
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1035
|
+
const results = { updated: [], failed: [] };
|
|
1036
|
+
|
|
1037
|
+
for (const [field, value] of Object.entries(patches)) {
|
|
1038
|
+
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1039
|
+
const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
1040
|
+
|
|
1041
|
+
if (pattern.test(content)) {
|
|
1042
|
+
content = content.replace(pattern, `$1${value}`);
|
|
1043
|
+
results.updated.push(field);
|
|
1044
|
+
} else {
|
|
1045
|
+
results.failed.push(field);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (results.updated.length > 0) {
|
|
1050
|
+
fs.writeFileSync(statePath, content, 'utf-8');
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
output(results, raw, results.updated.length > 0 ? 'true' : 'false');
|
|
1054
|
+
} catch {
|
|
1055
|
+
error('STATE.md not found');
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function cmdStateUpdate(cwd, field, value) {
|
|
1060
|
+
if (!field || value === undefined) {
|
|
1061
|
+
error('field and value required for state update');
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
1065
|
+
try {
|
|
1066
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1067
|
+
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1068
|
+
const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
1069
|
+
if (pattern.test(content)) {
|
|
1070
|
+
content = content.replace(pattern, `$1${value}`);
|
|
1071
|
+
fs.writeFileSync(statePath, content, 'utf-8');
|
|
1072
|
+
output({ updated: true });
|
|
1073
|
+
} else {
|
|
1074
|
+
output({ updated: false, reason: `Field "${field}" not found in STATE.md` });
|
|
1075
|
+
}
|
|
1076
|
+
} catch {
|
|
1077
|
+
output({ updated: false, reason: 'STATE.md not found' });
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// ─── State Progression Engine ────────────────────────────────────────────────
|
|
1082
|
+
|
|
1083
|
+
function stateExtractField(content, fieldName) {
|
|
1084
|
+
const pattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, 'i');
|
|
1085
|
+
const match = content.match(pattern);
|
|
1086
|
+
return match ? match[1].trim() : null;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function stateReplaceField(content, fieldName, newValue) {
|
|
1090
|
+
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1091
|
+
const pattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
1092
|
+
if (pattern.test(content)) {
|
|
1093
|
+
return content.replace(pattern, `$1${newValue}`);
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function cmdStateAdvancePlan(cwd, raw) {
|
|
1099
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
1100
|
+
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
1101
|
+
|
|
1102
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1103
|
+
const currentPlan = parseInt(stateExtractField(content, 'Current Plan'), 10);
|
|
1104
|
+
const totalPlans = parseInt(stateExtractField(content, 'Total Plans in Phase'), 10);
|
|
1105
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1106
|
+
|
|
1107
|
+
if (isNaN(currentPlan) || isNaN(totalPlans)) {
|
|
1108
|
+
output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (currentPlan >= totalPlans) {
|
|
1113
|
+
content = stateReplaceField(content, 'Status', 'Phase complete — ready for verification') || content;
|
|
1114
|
+
content = stateReplaceField(content, 'Last Activity', today) || content;
|
|
1115
|
+
fs.writeFileSync(statePath, content, 'utf-8');
|
|
1116
|
+
output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
|
|
1117
|
+
} else {
|
|
1118
|
+
const newPlan = currentPlan + 1;
|
|
1119
|
+
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
|
|
1120
|
+
content = stateReplaceField(content, 'Status', 'Ready to execute') || content;
|
|
1121
|
+
content = stateReplaceField(content, 'Last Activity', today) || content;
|
|
1122
|
+
fs.writeFileSync(statePath, content, 'utf-8');
|
|
1123
|
+
output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true');
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function cmdStateRecordMetric(cwd, options, raw) {
|
|
1128
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
1129
|
+
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
1130
|
+
|
|
1131
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1132
|
+
const { phase, plan, duration, tasks, files } = options;
|
|
1133
|
+
|
|
1134
|
+
if (!phase || !plan || !duration) {
|
|
1135
|
+
output({ error: 'phase, plan, and duration required' }, raw);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Find Performance Metrics section and its table
|
|
1140
|
+
const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i;
|
|
1141
|
+
const metricsMatch = content.match(metricsPattern);
|
|
1142
|
+
|
|
1143
|
+
if (metricsMatch) {
|
|
1144
|
+
const tableHeader = metricsMatch[1];
|
|
1145
|
+
let tableBody = metricsMatch[2].trimEnd();
|
|
1146
|
+
const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
|
|
1147
|
+
|
|
1148
|
+
if (tableBody.trim() === '' || tableBody.includes('None yet')) {
|
|
1149
|
+
tableBody = newRow;
|
|
1150
|
+
} else {
|
|
1151
|
+
tableBody = tableBody + '\n' + newRow;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
content = content.replace(metricsPattern, `${tableHeader}${tableBody}\n`);
|
|
1155
|
+
fs.writeFileSync(statePath, content, 'utf-8');
|
|
1156
|
+
output({ recorded: true, phase, plan, duration }, raw, 'true');
|
|
1157
|
+
} else {
|
|
1158
|
+
output({ recorded: false, reason: 'Performance Metrics section not found in STATE.md' }, raw, 'false');
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function cmdStateUpdateProgress(cwd, raw) {
|
|
1163
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
1164
|
+
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
1165
|
+
|
|
1166
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1167
|
+
|
|
1168
|
+
// Count summaries across all phases
|
|
1169
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
1170
|
+
let totalPlans = 0;
|
|
1171
|
+
let totalSummaries = 0;
|
|
1172
|
+
|
|
1173
|
+
if (fs.existsSync(phasesDir)) {
|
|
1174
|
+
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
1175
|
+
.filter(e => e.isDirectory()).map(e => e.name);
|
|
1176
|
+
for (const dir of phaseDirs) {
|
|
1177
|
+
const files = fs.readdirSync(path.join(phasesDir, dir));
|
|
1178
|
+
totalPlans += files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
1179
|
+
totalSummaries += files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const percent = totalPlans > 0 ? Math.round(totalSummaries / totalPlans * 100) : 0;
|
|
1184
|
+
const barWidth = 10;
|
|
1185
|
+
const filled = Math.round(percent / 100 * barWidth);
|
|
1186
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
1187
|
+
const progressStr = `[${bar}] ${percent}%`;
|
|
1188
|
+
|
|
1189
|
+
const progressPattern = /(\*\*Progress:\*\*\s*).*/i;
|
|
1190
|
+
if (progressPattern.test(content)) {
|
|
1191
|
+
content = content.replace(progressPattern, `$1${progressStr}`);
|
|
1192
|
+
fs.writeFileSync(statePath, content, 'utf-8');
|
|
1193
|
+
output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
|
|
1194
|
+
} else {
|
|
1195
|
+
output({ updated: false, reason: 'Progress field not found in STATE.md' }, raw, 'false');
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function cmdStateAddDecision(cwd, options, raw) {
|
|
1200
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
1201
|
+
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
1202
|
+
|
|
1203
|
+
const { phase, summary, rationale } = options;
|
|
1204
|
+
if (!summary) { output({ error: 'summary required' }, raw); return; }
|
|
1205
|
+
|
|
1206
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1207
|
+
const entry = `- [Phase ${phase || '?'}]: ${summary}${rationale ? ` — ${rationale}` : ''}`;
|
|
1208
|
+
|
|
1209
|
+
// Find Decisions section (various heading patterns)
|
|
1210
|
+
const sectionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
1211
|
+
const match = content.match(sectionPattern);
|
|
1212
|
+
|
|
1213
|
+
if (match) {
|
|
1214
|
+
let sectionBody = match[2];
|
|
1215
|
+
// Remove placeholders
|
|
1216
|
+
sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/No decisions yet\.?\s*\n?/gi, '');
|
|
1217
|
+
sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
|
|
1218
|
+
content = content.replace(sectionPattern, `${match[1]}${sectionBody}`);
|
|
1219
|
+
fs.writeFileSync(statePath, content, 'utf-8');
|
|
1220
|
+
output({ added: true, decision: entry }, raw, 'true');
|
|
1221
|
+
} else {
|
|
1222
|
+
output({ added: false, reason: 'Decisions section not found in STATE.md' }, raw, 'false');
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function cmdStateAddBlocker(cwd, text, raw) {
|
|
1227
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
1228
|
+
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
1229
|
+
if (!text) { output({ error: 'text required' }, raw); return; }
|
|
1230
|
+
|
|
1231
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1232
|
+
const entry = `- ${text}`;
|
|
1233
|
+
|
|
1234
|
+
const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
1235
|
+
const match = content.match(sectionPattern);
|
|
1236
|
+
|
|
1237
|
+
if (match) {
|
|
1238
|
+
let sectionBody = match[2];
|
|
1239
|
+
sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, '').replace(/None yet\.?\s*\n?/gi, '');
|
|
1240
|
+
sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
|
|
1241
|
+
content = content.replace(sectionPattern, `${match[1]}${sectionBody}`);
|
|
1242
|
+
fs.writeFileSync(statePath, content, 'utf-8');
|
|
1243
|
+
output({ added: true, blocker: text }, raw, 'true');
|
|
1244
|
+
} else {
|
|
1245
|
+
output({ added: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function cmdStateResolveBlocker(cwd, text, raw) {
|
|
1250
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
1251
|
+
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
1252
|
+
if (!text) { output({ error: 'text required' }, raw); return; }
|
|
1253
|
+
|
|
1254
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1255
|
+
|
|
1256
|
+
const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
1257
|
+
const match = content.match(sectionPattern);
|
|
1258
|
+
|
|
1259
|
+
if (match) {
|
|
1260
|
+
const sectionBody = match[2];
|
|
1261
|
+
const lines = sectionBody.split('\n');
|
|
1262
|
+
const filtered = lines.filter(line => {
|
|
1263
|
+
if (!line.startsWith('- ')) return true;
|
|
1264
|
+
return !line.toLowerCase().includes(text.toLowerCase());
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
let newBody = filtered.join('\n');
|
|
1268
|
+
// If section is now empty, add placeholder
|
|
1269
|
+
if (!newBody.trim() || !newBody.includes('- ')) {
|
|
1270
|
+
newBody = 'None\n';
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
content = content.replace(sectionPattern, `${match[1]}${newBody}`);
|
|
1274
|
+
fs.writeFileSync(statePath, content, 'utf-8');
|
|
1275
|
+
output({ resolved: true, blocker: text }, raw, 'true');
|
|
1276
|
+
} else {
|
|
1277
|
+
output({ resolved: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function cmdStateRecordSession(cwd, options, raw) {
|
|
1282
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
1283
|
+
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
1284
|
+
|
|
1285
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1286
|
+
const now = new Date().toISOString();
|
|
1287
|
+
const updated = [];
|
|
1288
|
+
|
|
1289
|
+
// Update Last session / Last Date
|
|
1290
|
+
let result = stateReplaceField(content, 'Last session', now);
|
|
1291
|
+
if (result) { content = result; updated.push('Last session'); }
|
|
1292
|
+
result = stateReplaceField(content, 'Last Date', now);
|
|
1293
|
+
if (result) { content = result; updated.push('Last Date'); }
|
|
1294
|
+
|
|
1295
|
+
// Update Stopped at
|
|
1296
|
+
if (options.stopped_at) {
|
|
1297
|
+
result = stateReplaceField(content, 'Stopped At', options.stopped_at);
|
|
1298
|
+
if (!result) result = stateReplaceField(content, 'Stopped at', options.stopped_at);
|
|
1299
|
+
if (result) { content = result; updated.push('Stopped At'); }
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Update Resume file
|
|
1303
|
+
const resumeFile = options.resume_file || 'None';
|
|
1304
|
+
result = stateReplaceField(content, 'Resume File', resumeFile);
|
|
1305
|
+
if (!result) result = stateReplaceField(content, 'Resume file', resumeFile);
|
|
1306
|
+
if (result) { content = result; updated.push('Resume File'); }
|
|
1307
|
+
|
|
1308
|
+
if (updated.length > 0) {
|
|
1309
|
+
fs.writeFileSync(statePath, content, 'utf-8');
|
|
1310
|
+
output({ recorded: true, updated }, raw, 'true');
|
|
1311
|
+
} else {
|
|
1312
|
+
output({ recorded: false, reason: 'No session fields found in STATE.md' }, raw, 'false');
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function cmdResolveModel(cwd, agentType, raw) {
|
|
1317
|
+
if (!agentType) {
|
|
1318
|
+
error('agent-type required');
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const config = loadConfig(cwd);
|
|
1322
|
+
const profile = config.model_profile || 'balanced';
|
|
1323
|
+
|
|
1324
|
+
const agentModels = MODEL_PROFILES[agentType];
|
|
1325
|
+
if (!agentModels) {
|
|
1326
|
+
const result = { model: 'sonnet', profile, unknown_agent: true };
|
|
1327
|
+
output(result, raw, 'sonnet');
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const model = agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
|
1332
|
+
const result = { model, profile };
|
|
1333
|
+
output(result, raw, model);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function cmdFindPhase(cwd, phase, raw) {
|
|
1337
|
+
if (!phase) {
|
|
1338
|
+
error('phase identifier required');
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
1342
|
+
const normalized = normalizePhaseName(phase);
|
|
1343
|
+
|
|
1344
|
+
const notFound = { found: false, directory: null, phase_number: null, phase_name: null, plans: [], summaries: [] };
|
|
1345
|
+
|
|
1346
|
+
try {
|
|
1347
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
1348
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
1349
|
+
|
|
1350
|
+
const match = dirs.find(d => d.startsWith(normalized));
|
|
1351
|
+
if (!match) {
|
|
1352
|
+
output(notFound, raw, '');
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const dirMatch = match.match(/^(\d+(?:\.\d+)?)-?(.*)/);
|
|
1357
|
+
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
1358
|
+
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
1359
|
+
|
|
1360
|
+
const phaseDir = path.join(phasesDir, match);
|
|
1361
|
+
const phaseFiles = fs.readdirSync(phaseDir);
|
|
1362
|
+
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
|
|
1363
|
+
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').sort();
|
|
1364
|
+
|
|
1365
|
+
const result = {
|
|
1366
|
+
found: true,
|
|
1367
|
+
directory: path.join('.blueprint', 'phases', match),
|
|
1368
|
+
phase_number: phaseNumber,
|
|
1369
|
+
phase_name: phaseName,
|
|
1370
|
+
plans,
|
|
1371
|
+
summaries,
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
output(result, raw, result.directory);
|
|
1375
|
+
} catch {
|
|
1376
|
+
output(notFound, raw, '');
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function cmdCommit(cwd, message, files, raw, amend) {
|
|
1381
|
+
if (!message && !amend) {
|
|
1382
|
+
error('commit message required');
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const config = loadConfig(cwd);
|
|
1386
|
+
|
|
1387
|
+
// Check commit_docs config
|
|
1388
|
+
if (!config.commit_docs) {
|
|
1389
|
+
const result = { committed: false, hash: null, reason: 'skipped_commit_docs_false' };
|
|
1390
|
+
output(result, raw, 'skipped');
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Check if .blueprint is gitignored
|
|
1395
|
+
if (isGitIgnored(cwd, '.blueprint')) {
|
|
1396
|
+
const result = { committed: false, hash: null, reason: 'skipped_gitignored' };
|
|
1397
|
+
output(result, raw, 'skipped');
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// Stage files
|
|
1402
|
+
const filesToStage = files && files.length > 0 ? files : ['.blueprint/'];
|
|
1403
|
+
for (const file of filesToStage) {
|
|
1404
|
+
execGit(cwd, ['add', file]);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Commit
|
|
1408
|
+
const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
|
|
1409
|
+
const commitResult = execGit(cwd, commitArgs);
|
|
1410
|
+
if (commitResult.exitCode !== 0) {
|
|
1411
|
+
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
1412
|
+
const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
|
|
1413
|
+
output(result, raw, 'nothing');
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const result = { committed: false, hash: null, reason: 'nothing_to_commit', error: commitResult.stderr };
|
|
1417
|
+
output(result, raw, 'nothing');
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Get short hash
|
|
1422
|
+
const hashResult = execGit(cwd, ['rev-parse', '--short', 'HEAD']);
|
|
1423
|
+
const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
1424
|
+
const result = { committed: true, hash, reason: 'committed' };
|
|
1425
|
+
output(result, raw, hash || 'committed');
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
|
|
1429
|
+
if (!summaryPath) {
|
|
1430
|
+
error('summary-path required');
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const fullPath = path.join(cwd, summaryPath);
|
|
1434
|
+
const checkCount = checkFileCount || 2;
|
|
1435
|
+
|
|
1436
|
+
// Check 1: Summary exists
|
|
1437
|
+
if (!fs.existsSync(fullPath)) {
|
|
1438
|
+
const result = {
|
|
1439
|
+
passed: false,
|
|
1440
|
+
checks: {
|
|
1441
|
+
summary_exists: false,
|
|
1442
|
+
files_created: { checked: 0, found: 0, missing: [] },
|
|
1443
|
+
commits_exist: false,
|
|
1444
|
+
self_check: 'not_found',
|
|
1445
|
+
},
|
|
1446
|
+
errors: ['SUMMARY.md not found'],
|
|
1447
|
+
};
|
|
1448
|
+
output(result, raw, 'failed');
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
1453
|
+
const errors = [];
|
|
1454
|
+
|
|
1455
|
+
// Check 2: Spot-check files mentioned in summary
|
|
1456
|
+
const mentionedFiles = new Set();
|
|
1457
|
+
const patterns = [
|
|
1458
|
+
/`([^`]+\.[a-zA-Z]+)`/g,
|
|
1459
|
+
/(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`]+\.[a-zA-Z]+)`?/gi,
|
|
1460
|
+
];
|
|
1461
|
+
|
|
1462
|
+
for (const pattern of patterns) {
|
|
1463
|
+
let m;
|
|
1464
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
1465
|
+
const filePath = m[1];
|
|
1466
|
+
if (filePath && !filePath.startsWith('http') && filePath.includes('/')) {
|
|
1467
|
+
mentionedFiles.add(filePath);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
|
|
1473
|
+
const missing = [];
|
|
1474
|
+
for (const file of filesToCheck) {
|
|
1475
|
+
if (!fs.existsSync(path.join(cwd, file))) {
|
|
1476
|
+
missing.push(file);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// Check 3: Commits exist
|
|
1481
|
+
const commitHashPattern = /\b[0-9a-f]{7,40}\b/g;
|
|
1482
|
+
const hashes = content.match(commitHashPattern) || [];
|
|
1483
|
+
let commitsExist = false;
|
|
1484
|
+
if (hashes.length > 0) {
|
|
1485
|
+
for (const hash of hashes.slice(0, 3)) {
|
|
1486
|
+
const result = execGit(cwd, ['cat-file', '-t', hash]);
|
|
1487
|
+
if (result.exitCode === 0 && result.stdout === 'commit') {
|
|
1488
|
+
commitsExist = true;
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Check 4: Self-check section
|
|
1495
|
+
let selfCheck = 'not_found';
|
|
1496
|
+
const selfCheckPattern = /##\s*(?:Self[- ]?Check|Verification|Quality Check)/i;
|
|
1497
|
+
if (selfCheckPattern.test(content)) {
|
|
1498
|
+
const passPattern = /(?:all\s+)?(?:pass|✓|✅|complete|succeeded)/i;
|
|
1499
|
+
const failPattern = /(?:fail|✗|❌|incomplete|blocked)/i;
|
|
1500
|
+
const checkSection = content.slice(content.search(selfCheckPattern));
|
|
1501
|
+
if (failPattern.test(checkSection)) {
|
|
1502
|
+
selfCheck = 'failed';
|
|
1503
|
+
} else if (passPattern.test(checkSection)) {
|
|
1504
|
+
selfCheck = 'passed';
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
if (missing.length > 0) errors.push('Missing files: ' + missing.join(', '));
|
|
1509
|
+
if (!commitsExist && hashes.length > 0) errors.push('Referenced commit hashes not found in git history');
|
|
1510
|
+
if (selfCheck === 'failed') errors.push('Self-check section indicates failure');
|
|
1511
|
+
|
|
1512
|
+
const checks = {
|
|
1513
|
+
summary_exists: true,
|
|
1514
|
+
files_created: { checked: filesToCheck.length, found: filesToCheck.length - missing.length, missing },
|
|
1515
|
+
commits_exist: commitsExist,
|
|
1516
|
+
self_check: selfCheck,
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
const passed = missing.length === 0 && selfCheck !== 'failed';
|
|
1520
|
+
const result = { passed, checks, errors };
|
|
1521
|
+
output(result, raw, passed ? 'passed' : 'failed');
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function cmdTemplateSelect(cwd, planPath, raw) {
|
|
1525
|
+
if (!planPath) {
|
|
1526
|
+
error('plan-path required');
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
try {
|
|
1530
|
+
const fullPath = path.join(cwd, planPath);
|
|
1531
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
1532
|
+
|
|
1533
|
+
// Simple heuristics
|
|
1534
|
+
const taskMatch = content.match(/###\s*Task\s*\d+/g) || [];
|
|
1535
|
+
const taskCount = taskMatch.length;
|
|
1536
|
+
|
|
1537
|
+
const decisionMatch = content.match(/decision/gi) || [];
|
|
1538
|
+
const hasDecisions = decisionMatch.length > 0;
|
|
1539
|
+
|
|
1540
|
+
// Count file mentions
|
|
1541
|
+
const fileMentions = new Set();
|
|
1542
|
+
const filePattern = /`([^`]+\.[a-zA-Z]+)`/g;
|
|
1543
|
+
let m;
|
|
1544
|
+
while ((m = filePattern.exec(content)) !== null) {
|
|
1545
|
+
if (m[1].includes('/') && !m[1].startsWith('http')) {
|
|
1546
|
+
fileMentions.add(m[1]);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const fileCount = fileMentions.size;
|
|
1550
|
+
|
|
1551
|
+
let template = 'templates/summary-standard.md';
|
|
1552
|
+
let type = 'standard';
|
|
1553
|
+
|
|
1554
|
+
if (taskCount <= 2 && fileCount <= 3 && !hasDecisions) {
|
|
1555
|
+
template = 'templates/summary-minimal.md';
|
|
1556
|
+
type = 'minimal';
|
|
1557
|
+
} else if (hasDecisions || fileCount > 6 || taskCount > 5) {
|
|
1558
|
+
template = 'templates/summary-complex.md';
|
|
1559
|
+
type = 'complex';
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const result = { template, type, taskCount, fileCount, hasDecisions };
|
|
1563
|
+
output(result, raw, template);
|
|
1564
|
+
} catch (e) {
|
|
1565
|
+
// Fallback to standard
|
|
1566
|
+
output({ template: 'templates/summary-standard.md', type: 'standard', error: e.message }, raw, 'templates/summary-standard.md');
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function cmdTemplateFill(cwd, templateType, options, raw) {
|
|
1571
|
+
if (!templateType) { error('template type required: summary, plan, or verification'); }
|
|
1572
|
+
if (!options.phase) { error('--phase required'); }
|
|
1573
|
+
|
|
1574
|
+
const phaseInfo = findPhaseInternal(cwd, options.phase);
|
|
1575
|
+
if (!phaseInfo || !phaseInfo.found) { output({ error: 'Phase not found', phase: options.phase }, raw); return; }
|
|
1576
|
+
|
|
1577
|
+
const padded = normalizePhaseName(options.phase);
|
|
1578
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1579
|
+
const phaseName = options.name || phaseInfo.phase_name || 'Unnamed';
|
|
1580
|
+
const phaseSlug = phaseInfo.phase_slug || generateSlugInternal(phaseName);
|
|
1581
|
+
const phaseId = `${padded}-${phaseSlug}`;
|
|
1582
|
+
const planNum = (options.plan || '01').padStart(2, '0');
|
|
1583
|
+
const fields = options.fields || {};
|
|
1584
|
+
|
|
1585
|
+
let frontmatter, body, fileName;
|
|
1586
|
+
|
|
1587
|
+
switch (templateType) {
|
|
1588
|
+
case 'summary': {
|
|
1589
|
+
frontmatter = {
|
|
1590
|
+
phase: phaseId,
|
|
1591
|
+
plan: planNum,
|
|
1592
|
+
subsystem: '[primary category]',
|
|
1593
|
+
tags: [],
|
|
1594
|
+
provides: [],
|
|
1595
|
+
affects: [],
|
|
1596
|
+
'tech-stack': { added: [], patterns: [] },
|
|
1597
|
+
'key-files': { created: [], modified: [] },
|
|
1598
|
+
'key-decisions': [],
|
|
1599
|
+
'patterns-established': [],
|
|
1600
|
+
duration: '[X]min',
|
|
1601
|
+
completed: today,
|
|
1602
|
+
...fields,
|
|
1603
|
+
};
|
|
1604
|
+
body = [
|
|
1605
|
+
`# Phase ${options.phase}: ${phaseName} Summary`,
|
|
1606
|
+
'',
|
|
1607
|
+
'**[Substantive one-liner describing outcome]**',
|
|
1608
|
+
'',
|
|
1609
|
+
'## Performance',
|
|
1610
|
+
'- **Duration:** [time]',
|
|
1611
|
+
'- **Tasks:** [count completed]',
|
|
1612
|
+
'- **Files modified:** [count]',
|
|
1613
|
+
'',
|
|
1614
|
+
'## Accomplishments',
|
|
1615
|
+
'- [Key outcome 1]',
|
|
1616
|
+
'- [Key outcome 2]',
|
|
1617
|
+
'',
|
|
1618
|
+
'## Task Commits',
|
|
1619
|
+
'1. **Task 1: [task name]** - `hash`',
|
|
1620
|
+
'',
|
|
1621
|
+
'## Files Created/Modified',
|
|
1622
|
+
'- `path/to/file.ts` - What it does',
|
|
1623
|
+
'',
|
|
1624
|
+
'## Decisions & Deviations',
|
|
1625
|
+
'[Key decisions or "None - followed plan as specified"]',
|
|
1626
|
+
'',
|
|
1627
|
+
'## Next Phase Readiness',
|
|
1628
|
+
'[What\'s ready for next phase]',
|
|
1629
|
+
].join('\n');
|
|
1630
|
+
fileName = `${padded}-${planNum}-SUMMARY.md`;
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1633
|
+
case 'plan': {
|
|
1634
|
+
const planType = options.type || 'execute';
|
|
1635
|
+
const wave = parseInt(options.wave) || 1;
|
|
1636
|
+
frontmatter = {
|
|
1637
|
+
phase: phaseId,
|
|
1638
|
+
plan: planNum,
|
|
1639
|
+
type: planType,
|
|
1640
|
+
wave,
|
|
1641
|
+
depends_on: [],
|
|
1642
|
+
files_modified: [],
|
|
1643
|
+
autonomous: true,
|
|
1644
|
+
user_setup: [],
|
|
1645
|
+
must_haves: { truths: [], artifacts: [], key_links: [] },
|
|
1646
|
+
...fields,
|
|
1647
|
+
};
|
|
1648
|
+
body = [
|
|
1649
|
+
`# Phase ${options.phase} Plan ${planNum}: [Title]`,
|
|
1650
|
+
'',
|
|
1651
|
+
'## Objective',
|
|
1652
|
+
'- **What:** [What this plan builds]',
|
|
1653
|
+
'- **Why:** [Why it matters for the phase goal]',
|
|
1654
|
+
'- **Output:** [Concrete deliverable]',
|
|
1655
|
+
'',
|
|
1656
|
+
'## Context',
|
|
1657
|
+
'@.blueprint/PROJECT.md',
|
|
1658
|
+
'@.blueprint/ROADMAP.md',
|
|
1659
|
+
'@.blueprint/STATE.md',
|
|
1660
|
+
'',
|
|
1661
|
+
'## Tasks',
|
|
1662
|
+
'',
|
|
1663
|
+
'<task type="code">',
|
|
1664
|
+
' <name>[Task name]</name>',
|
|
1665
|
+
' <files>[file paths]</files>',
|
|
1666
|
+
' <action>[What to do]</action>',
|
|
1667
|
+
' <verify>[How to verify]</verify>',
|
|
1668
|
+
' <done>[Definition of done]</done>',
|
|
1669
|
+
'</task>',
|
|
1670
|
+
'',
|
|
1671
|
+
'## Verification',
|
|
1672
|
+
'[How to verify this plan achieved its objective]',
|
|
1673
|
+
'',
|
|
1674
|
+
'## Success Criteria',
|
|
1675
|
+
'- [ ] [Criterion 1]',
|
|
1676
|
+
'- [ ] [Criterion 2]',
|
|
1677
|
+
].join('\n');
|
|
1678
|
+
fileName = `${padded}-${planNum}-PLAN.md`;
|
|
1679
|
+
break;
|
|
1680
|
+
}
|
|
1681
|
+
case 'verification': {
|
|
1682
|
+
frontmatter = {
|
|
1683
|
+
phase: phaseId,
|
|
1684
|
+
verified: new Date().toISOString(),
|
|
1685
|
+
status: 'pending',
|
|
1686
|
+
score: '0/0 must-haves verified',
|
|
1687
|
+
...fields,
|
|
1688
|
+
};
|
|
1689
|
+
body = [
|
|
1690
|
+
`# Phase ${options.phase}: ${phaseName} — Verification`,
|
|
1691
|
+
'',
|
|
1692
|
+
'## Observable Truths',
|
|
1693
|
+
'| # | Truth | Status | Evidence |',
|
|
1694
|
+
'|---|-------|--------|----------|',
|
|
1695
|
+
'| 1 | [Truth] | pending | |',
|
|
1696
|
+
'',
|
|
1697
|
+
'## Required Artifacts',
|
|
1698
|
+
'| Artifact | Expected | Status | Details |',
|
|
1699
|
+
'|----------|----------|--------|---------|',
|
|
1700
|
+
'| [path] | [what] | pending | |',
|
|
1701
|
+
'',
|
|
1702
|
+
'## Key Link Verification',
|
|
1703
|
+
'| From | To | Via | Status | Details |',
|
|
1704
|
+
'|------|----|----|--------|---------|',
|
|
1705
|
+
'| [source] | [target] | [connection] | pending | |',
|
|
1706
|
+
'',
|
|
1707
|
+
'## Requirements Coverage',
|
|
1708
|
+
'| Requirement | Status | Blocking Issue |',
|
|
1709
|
+
'|-------------|--------|----------------|',
|
|
1710
|
+
'| [req] | pending | |',
|
|
1711
|
+
'',
|
|
1712
|
+
'## Result',
|
|
1713
|
+
'[Pending verification]',
|
|
1714
|
+
].join('\n');
|
|
1715
|
+
fileName = `${padded}-VERIFICATION.md`;
|
|
1716
|
+
break;
|
|
1717
|
+
}
|
|
1718
|
+
default:
|
|
1719
|
+
error(`Unknown template type: ${templateType}. Available: summary, plan, verification`);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
const fullContent = `---\n${reconstructFrontmatter(frontmatter)}\n---\n\n${body}\n`;
|
|
1724
|
+
const outPath = path.join(cwd, phaseInfo.directory, fileName);
|
|
1725
|
+
|
|
1726
|
+
if (fs.existsSync(outPath)) {
|
|
1727
|
+
output({ error: 'File already exists', path: path.relative(cwd, outPath) }, raw);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
fs.writeFileSync(outPath, fullContent, 'utf-8');
|
|
1732
|
+
const relPath = path.relative(cwd, outPath);
|
|
1733
|
+
output({ created: true, path: relPath, template: templateType }, raw, relPath);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
1737
|
+
if (!phase) {
|
|
1738
|
+
error('phase required for phase-plan-index');
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
1742
|
+
const normalized = normalizePhaseName(phase);
|
|
1743
|
+
|
|
1744
|
+
// Find phase directory
|
|
1745
|
+
let phaseDir = null;
|
|
1746
|
+
let phaseDirName = null;
|
|
1747
|
+
try {
|
|
1748
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
1749
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
1750
|
+
const match = dirs.find(d => d.startsWith(normalized));
|
|
1751
|
+
if (match) {
|
|
1752
|
+
phaseDir = path.join(phasesDir, match);
|
|
1753
|
+
phaseDirName = match;
|
|
1754
|
+
}
|
|
1755
|
+
} catch {
|
|
1756
|
+
// phases dir doesn't exist
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
if (!phaseDir) {
|
|
1760
|
+
output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], has_checkpoints: false }, raw);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// Get all files in phase directory
|
|
1765
|
+
const phaseFiles = fs.readdirSync(phaseDir);
|
|
1766
|
+
const planFiles = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
|
|
1767
|
+
const summaryFiles = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
1768
|
+
|
|
1769
|
+
// Build set of plan IDs with summaries
|
|
1770
|
+
const completedPlanIds = new Set(
|
|
1771
|
+
summaryFiles.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
|
1772
|
+
);
|
|
1773
|
+
|
|
1774
|
+
const plans = [];
|
|
1775
|
+
const waves = {};
|
|
1776
|
+
const incomplete = [];
|
|
1777
|
+
let hasCheckpoints = false;
|
|
1778
|
+
|
|
1779
|
+
for (const planFile of planFiles) {
|
|
1780
|
+
const planId = planFile.replace('-PLAN.md', '').replace('PLAN.md', '');
|
|
1781
|
+
const planPath = path.join(phaseDir, planFile);
|
|
1782
|
+
const content = fs.readFileSync(planPath, 'utf-8');
|
|
1783
|
+
const fm = extractFrontmatter(content);
|
|
1784
|
+
|
|
1785
|
+
// Count tasks (## Task N patterns)
|
|
1786
|
+
const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
|
|
1787
|
+
const taskCount = taskMatches.length;
|
|
1788
|
+
|
|
1789
|
+
// Parse wave as integer
|
|
1790
|
+
const wave = parseInt(fm.wave, 10) || 1;
|
|
1791
|
+
|
|
1792
|
+
// Parse autonomous (default true if not specified)
|
|
1793
|
+
let autonomous = true;
|
|
1794
|
+
if (fm.autonomous !== undefined) {
|
|
1795
|
+
autonomous = fm.autonomous === 'true' || fm.autonomous === true;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
if (!autonomous) {
|
|
1799
|
+
hasCheckpoints = true;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// Parse files-modified
|
|
1803
|
+
let filesModified = [];
|
|
1804
|
+
if (fm['files-modified']) {
|
|
1805
|
+
filesModified = Array.isArray(fm['files-modified']) ? fm['files-modified'] : [fm['files-modified']];
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const hasSummary = completedPlanIds.has(planId);
|
|
1809
|
+
if (!hasSummary) {
|
|
1810
|
+
incomplete.push(planId);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
const plan = {
|
|
1814
|
+
id: planId,
|
|
1815
|
+
wave,
|
|
1816
|
+
autonomous,
|
|
1817
|
+
objective: fm.objective || null,
|
|
1818
|
+
files_modified: filesModified,
|
|
1819
|
+
task_count: taskCount,
|
|
1820
|
+
has_summary: hasSummary,
|
|
1821
|
+
};
|
|
1822
|
+
|
|
1823
|
+
plans.push(plan);
|
|
1824
|
+
|
|
1825
|
+
// Group by wave
|
|
1826
|
+
const waveKey = String(wave);
|
|
1827
|
+
if (!waves[waveKey]) {
|
|
1828
|
+
waves[waveKey] = [];
|
|
1829
|
+
}
|
|
1830
|
+
waves[waveKey].push(planId);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
const result = {
|
|
1834
|
+
phase: normalized,
|
|
1835
|
+
plans,
|
|
1836
|
+
waves,
|
|
1837
|
+
incomplete,
|
|
1838
|
+
has_checkpoints: hasCheckpoints,
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1841
|
+
output(result, raw);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function cmdStateSnapshot(cwd, raw) {
|
|
1845
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
1846
|
+
|
|
1847
|
+
if (!fs.existsSync(statePath)) {
|
|
1848
|
+
output({ error: 'STATE.md not found' }, raw);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
1853
|
+
|
|
1854
|
+
// Helper to extract **Field:** value patterns
|
|
1855
|
+
const extractField = (fieldName) => {
|
|
1856
|
+
const pattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, 'i');
|
|
1857
|
+
const match = content.match(pattern);
|
|
1858
|
+
return match ? match[1].trim() : null;
|
|
1859
|
+
};
|
|
1860
|
+
|
|
1861
|
+
// Extract basic fields
|
|
1862
|
+
const currentPhase = extractField('Current Phase');
|
|
1863
|
+
const currentPhaseName = extractField('Current Phase Name');
|
|
1864
|
+
const totalPhasesRaw = extractField('Total Phases');
|
|
1865
|
+
const currentPlan = extractField('Current Plan');
|
|
1866
|
+
const totalPlansRaw = extractField('Total Plans in Phase');
|
|
1867
|
+
const status = extractField('Status');
|
|
1868
|
+
const progressRaw = extractField('Progress');
|
|
1869
|
+
const lastActivity = extractField('Last Activity');
|
|
1870
|
+
const lastActivityDesc = extractField('Last Activity Description');
|
|
1871
|
+
const pausedAt = extractField('Paused At');
|
|
1872
|
+
|
|
1873
|
+
// Parse numeric fields
|
|
1874
|
+
const totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
|
1875
|
+
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
1876
|
+
const progressPercent = progressRaw ? parseInt(progressRaw.replace('%', ''), 10) : null;
|
|
1877
|
+
|
|
1878
|
+
// Extract decisions table
|
|
1879
|
+
const decisions = [];
|
|
1880
|
+
const decisionsMatch = content.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
|
1881
|
+
if (decisionsMatch) {
|
|
1882
|
+
const tableBody = decisionsMatch[1];
|
|
1883
|
+
const rows = tableBody.trim().split('\n').filter(r => r.includes('|'));
|
|
1884
|
+
for (const row of rows) {
|
|
1885
|
+
const cells = row.split('|').map(c => c.trim()).filter(Boolean);
|
|
1886
|
+
if (cells.length >= 3) {
|
|
1887
|
+
decisions.push({
|
|
1888
|
+
phase: cells[0],
|
|
1889
|
+
summary: cells[1],
|
|
1890
|
+
rationale: cells[2],
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// Extract blockers list
|
|
1897
|
+
const blockers = [];
|
|
1898
|
+
const blockersMatch = content.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i);
|
|
1899
|
+
if (blockersMatch) {
|
|
1900
|
+
const blockersSection = blockersMatch[1];
|
|
1901
|
+
const items = blockersSection.match(/^-\s+(.+)$/gm) || [];
|
|
1902
|
+
for (const item of items) {
|
|
1903
|
+
blockers.push(item.replace(/^-\s+/, '').trim());
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// Extract session info
|
|
1908
|
+
const session = {
|
|
1909
|
+
last_date: null,
|
|
1910
|
+
stopped_at: null,
|
|
1911
|
+
resume_file: null,
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
const sessionMatch = content.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i);
|
|
1915
|
+
if (sessionMatch) {
|
|
1916
|
+
const sessionSection = sessionMatch[1];
|
|
1917
|
+
const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i);
|
|
1918
|
+
const stoppedAtMatch = sessionSection.match(/\*\*Stopped At:\*\*\s*(.+)/i);
|
|
1919
|
+
const resumeFileMatch = sessionSection.match(/\*\*Resume File:\*\*\s*(.+)/i);
|
|
1920
|
+
|
|
1921
|
+
if (lastDateMatch) session.last_date = lastDateMatch[1].trim();
|
|
1922
|
+
if (stoppedAtMatch) session.stopped_at = stoppedAtMatch[1].trim();
|
|
1923
|
+
if (resumeFileMatch) session.resume_file = resumeFileMatch[1].trim();
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const result = {
|
|
1927
|
+
current_phase: currentPhase,
|
|
1928
|
+
current_phase_name: currentPhaseName,
|
|
1929
|
+
total_phases: totalPhases,
|
|
1930
|
+
current_plan: currentPlan,
|
|
1931
|
+
total_plans_in_phase: totalPlansInPhase,
|
|
1932
|
+
status,
|
|
1933
|
+
progress_percent: progressPercent,
|
|
1934
|
+
last_activity: lastActivity,
|
|
1935
|
+
last_activity_desc: lastActivityDesc,
|
|
1936
|
+
decisions,
|
|
1937
|
+
blockers,
|
|
1938
|
+
paused_at: pausedAt,
|
|
1939
|
+
session,
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
output(result, raw);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
1946
|
+
if (!summaryPath) {
|
|
1947
|
+
error('summary-path required for summary-extract');
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
const fullPath = path.join(cwd, summaryPath);
|
|
1951
|
+
|
|
1952
|
+
if (!fs.existsSync(fullPath)) {
|
|
1953
|
+
output({ error: 'File not found', path: summaryPath }, raw);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
1958
|
+
const fm = extractFrontmatter(content);
|
|
1959
|
+
|
|
1960
|
+
// Parse key-decisions into structured format
|
|
1961
|
+
const parseDecisions = (decisionsList) => {
|
|
1962
|
+
if (!decisionsList || !Array.isArray(decisionsList)) return [];
|
|
1963
|
+
return decisionsList.map(d => {
|
|
1964
|
+
const colonIdx = d.indexOf(':');
|
|
1965
|
+
if (colonIdx > 0) {
|
|
1966
|
+
return {
|
|
1967
|
+
summary: d.substring(0, colonIdx).trim(),
|
|
1968
|
+
rationale: d.substring(colonIdx + 1).trim(),
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
return { summary: d, rationale: null };
|
|
1972
|
+
});
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
// Build full result
|
|
1976
|
+
const fullResult = {
|
|
1977
|
+
path: summaryPath,
|
|
1978
|
+
one_liner: fm['one-liner'] || null,
|
|
1979
|
+
key_files: fm['key-files'] || [],
|
|
1980
|
+
tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
|
|
1981
|
+
patterns: fm['patterns-established'] || [],
|
|
1982
|
+
decisions: parseDecisions(fm['key-decisions']),
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1985
|
+
// If fields specified, filter to only those fields
|
|
1986
|
+
if (fields && fields.length > 0) {
|
|
1987
|
+
const filtered = { path: summaryPath };
|
|
1988
|
+
for (const field of fields) {
|
|
1989
|
+
if (fullResult[field] !== undefined) {
|
|
1990
|
+
filtered[field] = fullResult[field];
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
output(filtered, raw);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
output(fullResult, raw);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// ─── Web Search (Brave API) ──────────────────────────────────────────────────
|
|
2001
|
+
|
|
2002
|
+
async function cmdWebsearch(query, options, raw) {
|
|
2003
|
+
const apiKey = process.env.BRAVE_API_KEY;
|
|
2004
|
+
|
|
2005
|
+
if (!apiKey) {
|
|
2006
|
+
// No key = silent skip, agent falls back to built-in WebSearch
|
|
2007
|
+
output({ available: false, reason: 'BRAVE_API_KEY not set' }, raw, '');
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
if (!query) {
|
|
2012
|
+
output({ available: false, error: 'Query required' }, raw, '');
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
const params = new URLSearchParams({
|
|
2017
|
+
q: query,
|
|
2018
|
+
count: String(options.limit || 10),
|
|
2019
|
+
country: 'us',
|
|
2020
|
+
search_lang: 'en',
|
|
2021
|
+
text_decorations: 'false'
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
if (options.freshness) {
|
|
2025
|
+
params.set('freshness', options.freshness);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
try {
|
|
2029
|
+
const response = await fetch(
|
|
2030
|
+
`https://api.search.brave.com/res/v1/web/search?${params}`,
|
|
2031
|
+
{
|
|
2032
|
+
headers: {
|
|
2033
|
+
'Accept': 'application/json',
|
|
2034
|
+
'X-Subscription-Token': apiKey
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
);
|
|
2038
|
+
|
|
2039
|
+
if (!response.ok) {
|
|
2040
|
+
output({ available: false, error: `API error: ${response.status}` }, raw, '');
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const data = await response.json();
|
|
2045
|
+
|
|
2046
|
+
const results = (data.web?.results || []).map(r => ({
|
|
2047
|
+
title: r.title,
|
|
2048
|
+
url: r.url,
|
|
2049
|
+
description: r.description,
|
|
2050
|
+
age: r.age || null
|
|
2051
|
+
}));
|
|
2052
|
+
|
|
2053
|
+
output({
|
|
2054
|
+
available: true,
|
|
2055
|
+
query,
|
|
2056
|
+
count: results.length,
|
|
2057
|
+
results
|
|
2058
|
+
}, raw, results.map(r => `${r.title}\n${r.url}\n${r.description}`).join('\n\n'));
|
|
2059
|
+
} catch (err) {
|
|
2060
|
+
output({ available: false, error: err.message }, raw, '');
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// ─── Frontmatter CRUD ────────────────────────────────────────────────────────
|
|
2065
|
+
|
|
2066
|
+
function cmdFrontmatterGet(cwd, filePath, field, raw) {
|
|
2067
|
+
if (!filePath) { error('file path required'); }
|
|
2068
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
2069
|
+
const content = safeReadFile(fullPath);
|
|
2070
|
+
if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
|
|
2071
|
+
const fm = extractFrontmatter(content);
|
|
2072
|
+
if (field) {
|
|
2073
|
+
const value = fm[field];
|
|
2074
|
+
if (value === undefined) { output({ error: 'Field not found', field }, raw); return; }
|
|
2075
|
+
output({ [field]: value }, raw, JSON.stringify(value));
|
|
2076
|
+
} else {
|
|
2077
|
+
output(fm, raw);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
|
|
2082
|
+
if (!filePath || !field || value === undefined) { error('file, field, and value required'); }
|
|
2083
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
2084
|
+
if (!fs.existsSync(fullPath)) { output({ error: 'File not found', path: filePath }, raw); return; }
|
|
2085
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
2086
|
+
const fm = extractFrontmatter(content);
|
|
2087
|
+
let parsedValue;
|
|
2088
|
+
try { parsedValue = JSON.parse(value); } catch { parsedValue = value; }
|
|
2089
|
+
fm[field] = parsedValue;
|
|
2090
|
+
const newContent = spliceFrontmatter(content, fm);
|
|
2091
|
+
fs.writeFileSync(fullPath, newContent, 'utf-8');
|
|
2092
|
+
output({ updated: true, field, value: parsedValue }, raw, 'true');
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function cmdFrontmatterMerge(cwd, filePath, data, raw) {
|
|
2096
|
+
if (!filePath || !data) { error('file and data required'); }
|
|
2097
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
2098
|
+
if (!fs.existsSync(fullPath)) { output({ error: 'File not found', path: filePath }, raw); return; }
|
|
2099
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
2100
|
+
const fm = extractFrontmatter(content);
|
|
2101
|
+
let mergeData;
|
|
2102
|
+
try { mergeData = JSON.parse(data); } catch { error('Invalid JSON for --data'); return; }
|
|
2103
|
+
Object.assign(fm, mergeData);
|
|
2104
|
+
const newContent = spliceFrontmatter(content, fm);
|
|
2105
|
+
fs.writeFileSync(fullPath, newContent, 'utf-8');
|
|
2106
|
+
output({ merged: true, fields: Object.keys(mergeData) }, raw, 'true');
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
const FRONTMATTER_SCHEMAS = {
|
|
2110
|
+
plan: { required: ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'] },
|
|
2111
|
+
summary: { required: ['phase', 'plan', 'subsystem', 'tags', 'duration', 'completed'] },
|
|
2112
|
+
verification: { required: ['phase', 'verified', 'status', 'score'] },
|
|
2113
|
+
};
|
|
2114
|
+
|
|
2115
|
+
function cmdFrontmatterValidate(cwd, filePath, schemaName, raw) {
|
|
2116
|
+
if (!filePath || !schemaName) { error('file and schema required'); }
|
|
2117
|
+
const schema = FRONTMATTER_SCHEMAS[schemaName];
|
|
2118
|
+
if (!schema) { error(`Unknown schema: ${schemaName}. Available: ${Object.keys(FRONTMATTER_SCHEMAS).join(', ')}`); }
|
|
2119
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
2120
|
+
const content = safeReadFile(fullPath);
|
|
2121
|
+
if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
|
|
2122
|
+
const fm = extractFrontmatter(content);
|
|
2123
|
+
const missing = schema.required.filter(f => fm[f] === undefined);
|
|
2124
|
+
const present = schema.required.filter(f => fm[f] !== undefined);
|
|
2125
|
+
output({ valid: missing.length === 0, missing, present, schema: schemaName }, raw, missing.length === 0 ? 'valid' : 'invalid');
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// ─── Verification Suite ──────────────────────────────────────────────────────
|
|
2129
|
+
|
|
2130
|
+
function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
2131
|
+
if (!filePath) { error('file path required'); }
|
|
2132
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
2133
|
+
const content = safeReadFile(fullPath);
|
|
2134
|
+
if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
|
|
2135
|
+
|
|
2136
|
+
const fm = extractFrontmatter(content);
|
|
2137
|
+
const errors = [];
|
|
2138
|
+
const warnings = [];
|
|
2139
|
+
|
|
2140
|
+
// Check required frontmatter fields
|
|
2141
|
+
const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
|
|
2142
|
+
for (const field of required) {
|
|
2143
|
+
if (fm[field] === undefined) errors.push(`Missing required frontmatter field: ${field}`);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// Parse and check task elements
|
|
2147
|
+
const taskPattern = /<task[^>]*>([\s\S]*?)<\/task>/g;
|
|
2148
|
+
const tasks = [];
|
|
2149
|
+
let taskMatch;
|
|
2150
|
+
while ((taskMatch = taskPattern.exec(content)) !== null) {
|
|
2151
|
+
const taskContent = taskMatch[1];
|
|
2152
|
+
const nameMatch = taskContent.match(/<name>([\s\S]*?)<\/name>/);
|
|
2153
|
+
const taskName = nameMatch ? nameMatch[1].trim() : 'unnamed';
|
|
2154
|
+
const hasFiles = /<files>/.test(taskContent);
|
|
2155
|
+
const hasAction = /<action>/.test(taskContent);
|
|
2156
|
+
const hasVerify = /<verify>/.test(taskContent);
|
|
2157
|
+
const hasDone = /<done>/.test(taskContent);
|
|
2158
|
+
|
|
2159
|
+
if (!nameMatch) errors.push('Task missing <name> element');
|
|
2160
|
+
if (!hasAction) errors.push(`Task '${taskName}' missing <action>`);
|
|
2161
|
+
if (!hasVerify) warnings.push(`Task '${taskName}' missing <verify>`);
|
|
2162
|
+
if (!hasDone) warnings.push(`Task '${taskName}' missing <done>`);
|
|
2163
|
+
if (!hasFiles) warnings.push(`Task '${taskName}' missing <files>`);
|
|
2164
|
+
|
|
2165
|
+
tasks.push({ name: taskName, hasFiles, hasAction, hasVerify, hasDone });
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
if (tasks.length === 0) warnings.push('No <task> elements found');
|
|
2169
|
+
|
|
2170
|
+
// Wave/depends_on consistency
|
|
2171
|
+
if (fm.wave && parseInt(fm.wave) > 1 && (!fm.depends_on || (Array.isArray(fm.depends_on) && fm.depends_on.length === 0))) {
|
|
2172
|
+
warnings.push('Wave > 1 but depends_on is empty');
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// Autonomous/checkpoint consistency
|
|
2176
|
+
const hasCheckpoints = /<task\s+type=["']?checkpoint/.test(content);
|
|
2177
|
+
if (hasCheckpoints && fm.autonomous !== 'false' && fm.autonomous !== false) {
|
|
2178
|
+
errors.push('Has checkpoint tasks but autonomous is not false');
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
output({
|
|
2182
|
+
valid: errors.length === 0,
|
|
2183
|
+
errors,
|
|
2184
|
+
warnings,
|
|
2185
|
+
task_count: tasks.length,
|
|
2186
|
+
tasks,
|
|
2187
|
+
frontmatter_fields: Object.keys(fm),
|
|
2188
|
+
}, raw, errors.length === 0 ? 'valid' : 'invalid');
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
|
|
2192
|
+
if (!phase) { error('phase required'); }
|
|
2193
|
+
const phaseInfo = findPhaseInternal(cwd, phase);
|
|
2194
|
+
if (!phaseInfo || !phaseInfo.found) {
|
|
2195
|
+
output({ error: 'Phase not found', phase }, raw);
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
const errors = [];
|
|
2200
|
+
const warnings = [];
|
|
2201
|
+
const phaseDir = path.join(cwd, phaseInfo.directory);
|
|
2202
|
+
|
|
2203
|
+
// List plans and summaries
|
|
2204
|
+
let files;
|
|
2205
|
+
try { files = fs.readdirSync(phaseDir); } catch { output({ error: 'Cannot read phase directory' }, raw); return; }
|
|
2206
|
+
|
|
2207
|
+
const plans = files.filter(f => f.match(/-PLAN\.md$/i));
|
|
2208
|
+
const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i));
|
|
2209
|
+
|
|
2210
|
+
// Extract plan IDs (everything before -PLAN.md)
|
|
2211
|
+
const planIds = new Set(plans.map(p => p.replace(/-PLAN\.md$/i, '')));
|
|
2212
|
+
const summaryIds = new Set(summaries.map(s => s.replace(/-SUMMARY\.md$/i, '')));
|
|
2213
|
+
|
|
2214
|
+
// Plans without summaries
|
|
2215
|
+
const incompletePlans = [...planIds].filter(id => !summaryIds.has(id));
|
|
2216
|
+
if (incompletePlans.length > 0) {
|
|
2217
|
+
errors.push(`Plans without summaries: ${incompletePlans.join(', ')}`);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// Summaries without plans (orphans)
|
|
2221
|
+
const orphanSummaries = [...summaryIds].filter(id => !planIds.has(id));
|
|
2222
|
+
if (orphanSummaries.length > 0) {
|
|
2223
|
+
warnings.push(`Summaries without plans: ${orphanSummaries.join(', ')}`);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
output({
|
|
2227
|
+
complete: errors.length === 0,
|
|
2228
|
+
phase: phaseInfo.phase_number,
|
|
2229
|
+
plan_count: plans.length,
|
|
2230
|
+
summary_count: summaries.length,
|
|
2231
|
+
incomplete_plans: incompletePlans,
|
|
2232
|
+
orphan_summaries: orphanSummaries,
|
|
2233
|
+
errors,
|
|
2234
|
+
warnings,
|
|
2235
|
+
}, raw, errors.length === 0 ? 'complete' : 'incomplete');
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function cmdVerifyReferences(cwd, filePath, raw) {
|
|
2239
|
+
if (!filePath) { error('file path required'); }
|
|
2240
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
2241
|
+
const content = safeReadFile(fullPath);
|
|
2242
|
+
if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
|
|
2243
|
+
|
|
2244
|
+
const found = [];
|
|
2245
|
+
const missing = [];
|
|
2246
|
+
|
|
2247
|
+
// Find @-references: @path/to/file (must contain / to be a file path)
|
|
2248
|
+
const atRefs = content.match(/@([^\s\n,)]+\/[^\s\n,)]+)/g) || [];
|
|
2249
|
+
for (const ref of atRefs) {
|
|
2250
|
+
const cleanRef = ref.slice(1); // remove @
|
|
2251
|
+
const resolved = cleanRef.startsWith('~/')
|
|
2252
|
+
? path.join(process.env.HOME || '', cleanRef.slice(2))
|
|
2253
|
+
: path.join(cwd, cleanRef);
|
|
2254
|
+
if (fs.existsSync(resolved)) {
|
|
2255
|
+
found.push(cleanRef);
|
|
2256
|
+
} else {
|
|
2257
|
+
missing.push(cleanRef);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// Find backtick file paths that look like real paths (contain / and have extension)
|
|
2262
|
+
const backtickRefs = content.match(/`([^`]+\/[^`]+\.[a-zA-Z]{1,10})`/g) || [];
|
|
2263
|
+
for (const ref of backtickRefs) {
|
|
2264
|
+
const cleanRef = ref.slice(1, -1); // remove backticks
|
|
2265
|
+
if (cleanRef.startsWith('http') || cleanRef.includes('${') || cleanRef.includes('{{')) continue;
|
|
2266
|
+
if (found.includes(cleanRef) || missing.includes(cleanRef)) continue; // dedup
|
|
2267
|
+
const resolved = path.join(cwd, cleanRef);
|
|
2268
|
+
if (fs.existsSync(resolved)) {
|
|
2269
|
+
found.push(cleanRef);
|
|
2270
|
+
} else {
|
|
2271
|
+
missing.push(cleanRef);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
output({
|
|
2276
|
+
valid: missing.length === 0,
|
|
2277
|
+
found: found.length,
|
|
2278
|
+
missing,
|
|
2279
|
+
total: found.length + missing.length,
|
|
2280
|
+
}, raw, missing.length === 0 ? 'valid' : 'invalid');
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
function cmdVerifyCommits(cwd, hashes, raw) {
|
|
2284
|
+
if (!hashes || hashes.length === 0) { error('At least one commit hash required'); }
|
|
2285
|
+
|
|
2286
|
+
const valid = [];
|
|
2287
|
+
const invalid = [];
|
|
2288
|
+
for (const hash of hashes) {
|
|
2289
|
+
const result = execGit(cwd, ['cat-file', '-t', hash]);
|
|
2290
|
+
if (result.exitCode === 0 && result.stdout.trim() === 'commit') {
|
|
2291
|
+
valid.push(hash);
|
|
2292
|
+
} else {
|
|
2293
|
+
invalid.push(hash);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
output({
|
|
2298
|
+
all_valid: invalid.length === 0,
|
|
2299
|
+
valid,
|
|
2300
|
+
invalid,
|
|
2301
|
+
total: hashes.length,
|
|
2302
|
+
}, raw, invalid.length === 0 ? 'valid' : 'invalid');
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
function cmdVerifyArtifacts(cwd, planFilePath, raw) {
|
|
2306
|
+
if (!planFilePath) { error('plan file path required'); }
|
|
2307
|
+
const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
|
|
2308
|
+
const content = safeReadFile(fullPath);
|
|
2309
|
+
if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
|
|
2310
|
+
|
|
2311
|
+
const artifacts = parseMustHavesBlock(content, 'artifacts');
|
|
2312
|
+
if (artifacts.length === 0) {
|
|
2313
|
+
output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
const results = [];
|
|
2318
|
+
for (const artifact of artifacts) {
|
|
2319
|
+
if (typeof artifact === 'string') continue; // skip simple string items
|
|
2320
|
+
const artPath = artifact.path;
|
|
2321
|
+
if (!artPath) continue;
|
|
2322
|
+
|
|
2323
|
+
const artFullPath = path.join(cwd, artPath);
|
|
2324
|
+
const exists = fs.existsSync(artFullPath);
|
|
2325
|
+
const check = { path: artPath, exists, issues: [], passed: false };
|
|
2326
|
+
|
|
2327
|
+
if (exists) {
|
|
2328
|
+
const fileContent = safeReadFile(artFullPath) || '';
|
|
2329
|
+
const lineCount = fileContent.split('\n').length;
|
|
2330
|
+
|
|
2331
|
+
if (artifact.min_lines && lineCount < artifact.min_lines) {
|
|
2332
|
+
check.issues.push(`Only ${lineCount} lines, need ${artifact.min_lines}`);
|
|
2333
|
+
}
|
|
2334
|
+
if (artifact.contains && !fileContent.includes(artifact.contains)) {
|
|
2335
|
+
check.issues.push(`Missing pattern: ${artifact.contains}`);
|
|
2336
|
+
}
|
|
2337
|
+
if (artifact.exports) {
|
|
2338
|
+
const exports = Array.isArray(artifact.exports) ? artifact.exports : [artifact.exports];
|
|
2339
|
+
for (const exp of exports) {
|
|
2340
|
+
if (!fileContent.includes(exp)) check.issues.push(`Missing export: ${exp}`);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
check.passed = check.issues.length === 0;
|
|
2344
|
+
} else {
|
|
2345
|
+
check.issues.push('File not found');
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
results.push(check);
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
const passed = results.filter(r => r.passed).length;
|
|
2352
|
+
output({
|
|
2353
|
+
all_passed: passed === results.length,
|
|
2354
|
+
passed,
|
|
2355
|
+
total: results.length,
|
|
2356
|
+
artifacts: results,
|
|
2357
|
+
}, raw, passed === results.length ? 'valid' : 'invalid');
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
2361
|
+
if (!planFilePath) { error('plan file path required'); }
|
|
2362
|
+
const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
|
|
2363
|
+
const content = safeReadFile(fullPath);
|
|
2364
|
+
if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
|
|
2365
|
+
|
|
2366
|
+
const keyLinks = parseMustHavesBlock(content, 'key_links');
|
|
2367
|
+
if (keyLinks.length === 0) {
|
|
2368
|
+
output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
const results = [];
|
|
2373
|
+
for (const link of keyLinks) {
|
|
2374
|
+
if (typeof link === 'string') continue;
|
|
2375
|
+
const check = { from: link.from, to: link.to, via: link.via || '', verified: false, detail: '' };
|
|
2376
|
+
|
|
2377
|
+
const sourceContent = safeReadFile(path.join(cwd, link.from || ''));
|
|
2378
|
+
if (!sourceContent) {
|
|
2379
|
+
check.detail = 'Source file not found';
|
|
2380
|
+
} else if (link.pattern) {
|
|
2381
|
+
try {
|
|
2382
|
+
const regex = new RegExp(link.pattern);
|
|
2383
|
+
if (regex.test(sourceContent)) {
|
|
2384
|
+
check.verified = true;
|
|
2385
|
+
check.detail = 'Pattern found in source';
|
|
2386
|
+
} else {
|
|
2387
|
+
const targetContent = safeReadFile(path.join(cwd, link.to || ''));
|
|
2388
|
+
if (targetContent && regex.test(targetContent)) {
|
|
2389
|
+
check.verified = true;
|
|
2390
|
+
check.detail = 'Pattern found in target';
|
|
2391
|
+
} else {
|
|
2392
|
+
check.detail = `Pattern "${link.pattern}" not found in source or target`;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
} catch {
|
|
2396
|
+
check.detail = `Invalid regex pattern: ${link.pattern}`;
|
|
2397
|
+
}
|
|
2398
|
+
} else {
|
|
2399
|
+
// No pattern: just check source references target
|
|
2400
|
+
if (sourceContent.includes(link.to || '')) {
|
|
2401
|
+
check.verified = true;
|
|
2402
|
+
check.detail = 'Target referenced in source';
|
|
2403
|
+
} else {
|
|
2404
|
+
check.detail = 'Target not referenced in source';
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
results.push(check);
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
const verified = results.filter(r => r.verified).length;
|
|
2412
|
+
output({
|
|
2413
|
+
all_verified: verified === results.length,
|
|
2414
|
+
verified,
|
|
2415
|
+
total: results.length,
|
|
2416
|
+
links: results,
|
|
2417
|
+
}, raw, verified === results.length ? 'valid' : 'invalid');
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// ─── Roadmap Analysis ─────────────────────────────────────────────────────────
|
|
2421
|
+
|
|
2422
|
+
function cmdRoadmapAnalyze(cwd, raw) {
|
|
2423
|
+
const roadmapPath = path.join(cwd, '.blueprint', 'ROADMAP.md');
|
|
2424
|
+
|
|
2425
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
2426
|
+
output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw);
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
2431
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
2432
|
+
|
|
2433
|
+
// Extract all phase headings: ### Phase N: Name
|
|
2434
|
+
const phasePattern = /###\s*Phase\s+(\d+(?:\.\d+)?)\s*:\s*([^\n]+)/gi;
|
|
2435
|
+
const phases = [];
|
|
2436
|
+
let match;
|
|
2437
|
+
|
|
2438
|
+
while ((match = phasePattern.exec(content)) !== null) {
|
|
2439
|
+
const phaseNum = match[1];
|
|
2440
|
+
const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
|
|
2441
|
+
|
|
2442
|
+
// Extract goal from the section
|
|
2443
|
+
const sectionStart = match.index;
|
|
2444
|
+
const restOfContent = content.slice(sectionStart);
|
|
2445
|
+
const nextHeader = restOfContent.match(/\n###\s+Phase\s+\d/i);
|
|
2446
|
+
const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
|
|
2447
|
+
const section = content.slice(sectionStart, sectionEnd);
|
|
2448
|
+
|
|
2449
|
+
const goalMatch = section.match(/\*\*Goal:\*\*\s*([^\n]+)/i);
|
|
2450
|
+
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
2451
|
+
|
|
2452
|
+
const dependsMatch = section.match(/\*\*Depends on:\*\*\s*([^\n]+)/i);
|
|
2453
|
+
const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
|
|
2454
|
+
|
|
2455
|
+
// Check completion on disk
|
|
2456
|
+
const normalized = normalizePhaseName(phaseNum);
|
|
2457
|
+
let diskStatus = 'no_directory';
|
|
2458
|
+
let planCount = 0;
|
|
2459
|
+
let summaryCount = 0;
|
|
2460
|
+
let hasContext = false;
|
|
2461
|
+
let hasResearch = false;
|
|
2462
|
+
|
|
2463
|
+
try {
|
|
2464
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
2465
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
2466
|
+
const dirMatch = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
|
|
2467
|
+
|
|
2468
|
+
if (dirMatch) {
|
|
2469
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
|
|
2470
|
+
planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
2471
|
+
summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
2472
|
+
hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
2473
|
+
hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
2474
|
+
|
|
2475
|
+
if (summaryCount >= planCount && planCount > 0) diskStatus = 'complete';
|
|
2476
|
+
else if (summaryCount > 0) diskStatus = 'partial';
|
|
2477
|
+
else if (planCount > 0) diskStatus = 'planned';
|
|
2478
|
+
else if (hasResearch) diskStatus = 'researched';
|
|
2479
|
+
else if (hasContext) diskStatus = 'discussed';
|
|
2480
|
+
else diskStatus = 'empty';
|
|
2481
|
+
}
|
|
2482
|
+
} catch {}
|
|
2483
|
+
|
|
2484
|
+
// Check ROADMAP checkbox status
|
|
2485
|
+
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}`, 'i');
|
|
2486
|
+
const checkboxMatch = content.match(checkboxPattern);
|
|
2487
|
+
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
|
2488
|
+
|
|
2489
|
+
phases.push({
|
|
2490
|
+
number: phaseNum,
|
|
2491
|
+
name: phaseName,
|
|
2492
|
+
goal,
|
|
2493
|
+
depends_on,
|
|
2494
|
+
plan_count: planCount,
|
|
2495
|
+
summary_count: summaryCount,
|
|
2496
|
+
has_context: hasContext,
|
|
2497
|
+
has_research: hasResearch,
|
|
2498
|
+
disk_status: diskStatus,
|
|
2499
|
+
roadmap_complete: roadmapComplete,
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// Extract milestone info
|
|
2504
|
+
const milestones = [];
|
|
2505
|
+
const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi;
|
|
2506
|
+
let mMatch;
|
|
2507
|
+
while ((mMatch = milestonePattern.exec(content)) !== null) {
|
|
2508
|
+
milestones.push({
|
|
2509
|
+
heading: mMatch[1].trim(),
|
|
2510
|
+
version: 'v' + mMatch[2],
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// Find current and next phase
|
|
2515
|
+
const currentPhase = phases.find(p => p.disk_status === 'planned' || p.disk_status === 'partial') || null;
|
|
2516
|
+
const nextPhase = phases.find(p => p.disk_status === 'empty' || p.disk_status === 'no_directory' || p.disk_status === 'discussed' || p.disk_status === 'researched') || null;
|
|
2517
|
+
|
|
2518
|
+
// Aggregated stats
|
|
2519
|
+
const totalPlans = phases.reduce((sum, p) => sum + p.plan_count, 0);
|
|
2520
|
+
const totalSummaries = phases.reduce((sum, p) => sum + p.summary_count, 0);
|
|
2521
|
+
const completedPhases = phases.filter(p => p.disk_status === 'complete').length;
|
|
2522
|
+
|
|
2523
|
+
const result = {
|
|
2524
|
+
milestones,
|
|
2525
|
+
phases,
|
|
2526
|
+
phase_count: phases.length,
|
|
2527
|
+
completed_phases: completedPhases,
|
|
2528
|
+
total_plans: totalPlans,
|
|
2529
|
+
total_summaries: totalSummaries,
|
|
2530
|
+
progress_percent: totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0,
|
|
2531
|
+
current_phase: currentPhase ? currentPhase.number : null,
|
|
2532
|
+
next_phase: nextPhase ? nextPhase.number : null,
|
|
2533
|
+
};
|
|
2534
|
+
|
|
2535
|
+
output(result, raw);
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// ─── Phase Add ────────────────────────────────────────────────────────────────
|
|
2539
|
+
|
|
2540
|
+
function cmdPhaseAdd(cwd, description, raw) {
|
|
2541
|
+
if (!description) {
|
|
2542
|
+
error('description required for phase add');
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
const roadmapPath = path.join(cwd, '.blueprint', 'ROADMAP.md');
|
|
2546
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
2547
|
+
error('ROADMAP.md not found');
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
2551
|
+
const slug = generateSlugInternal(description);
|
|
2552
|
+
|
|
2553
|
+
// Find highest integer phase number
|
|
2554
|
+
const phasePattern = /###\s*Phase\s+(\d+)(?:\.\d+)?:/gi;
|
|
2555
|
+
let maxPhase = 0;
|
|
2556
|
+
let m;
|
|
2557
|
+
while ((m = phasePattern.exec(content)) !== null) {
|
|
2558
|
+
const num = parseInt(m[1], 10);
|
|
2559
|
+
if (num > maxPhase) maxPhase = num;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
const newPhaseNum = maxPhase + 1;
|
|
2563
|
+
const paddedNum = String(newPhaseNum).padStart(2, '0');
|
|
2564
|
+
const dirName = `${paddedNum}-${slug}`;
|
|
2565
|
+
const dirPath = path.join(cwd, '.blueprint', 'phases', dirName);
|
|
2566
|
+
|
|
2567
|
+
// Create directory
|
|
2568
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
2569
|
+
|
|
2570
|
+
// Build phase entry
|
|
2571
|
+
const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /bp:plan-phase ${newPhaseNum} to break down)\n`;
|
|
2572
|
+
|
|
2573
|
+
// Find insertion point: before last "---" or at end
|
|
2574
|
+
let updatedContent;
|
|
2575
|
+
const lastSeparator = content.lastIndexOf('\n---');
|
|
2576
|
+
if (lastSeparator > 0) {
|
|
2577
|
+
updatedContent = content.slice(0, lastSeparator) + phaseEntry + content.slice(lastSeparator);
|
|
2578
|
+
} else {
|
|
2579
|
+
updatedContent = content + phaseEntry;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
|
2583
|
+
|
|
2584
|
+
const result = {
|
|
2585
|
+
phase_number: newPhaseNum,
|
|
2586
|
+
padded: paddedNum,
|
|
2587
|
+
name: description,
|
|
2588
|
+
slug,
|
|
2589
|
+
directory: `.blueprint/phases/${dirName}`,
|
|
2590
|
+
};
|
|
2591
|
+
|
|
2592
|
+
output(result, raw, paddedNum);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// ─── Phase Insert (Decimal) ──────────────────────────────────────────────────
|
|
2596
|
+
|
|
2597
|
+
function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
2598
|
+
if (!afterPhase || !description) {
|
|
2599
|
+
error('after-phase and description required for phase insert');
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
const roadmapPath = path.join(cwd, '.blueprint', 'ROADMAP.md');
|
|
2603
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
2604
|
+
error('ROADMAP.md not found');
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
2608
|
+
const slug = generateSlugInternal(description);
|
|
2609
|
+
|
|
2610
|
+
// Verify target phase exists
|
|
2611
|
+
const afterPhaseEscaped = afterPhase.replace(/\./g, '\\.');
|
|
2612
|
+
const targetPattern = new RegExp(`###\\s*Phase\\s+${afterPhaseEscaped}:`, 'i');
|
|
2613
|
+
if (!targetPattern.test(content)) {
|
|
2614
|
+
error(`Phase ${afterPhase} not found in ROADMAP.md`);
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// Calculate next decimal using existing logic
|
|
2618
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
2619
|
+
const normalizedBase = normalizePhaseName(afterPhase);
|
|
2620
|
+
let existingDecimals = [];
|
|
2621
|
+
|
|
2622
|
+
try {
|
|
2623
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
2624
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
2625
|
+
const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
|
|
2626
|
+
for (const dir of dirs) {
|
|
2627
|
+
const dm = dir.match(decimalPattern);
|
|
2628
|
+
if (dm) existingDecimals.push(parseInt(dm[1], 10));
|
|
2629
|
+
}
|
|
2630
|
+
} catch {}
|
|
2631
|
+
|
|
2632
|
+
const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
|
|
2633
|
+
const decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
|
2634
|
+
const dirName = `${decimalPhase}-${slug}`;
|
|
2635
|
+
const dirPath = path.join(cwd, '.blueprint', 'phases', dirName);
|
|
2636
|
+
|
|
2637
|
+
// Create directory
|
|
2638
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
2639
|
+
|
|
2640
|
+
// Build phase entry
|
|
2641
|
+
const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /bp:plan-phase ${decimalPhase} to break down)\n`;
|
|
2642
|
+
|
|
2643
|
+
// Insert after the target phase section
|
|
2644
|
+
const headerPattern = new RegExp(`(###\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
|
2645
|
+
const headerMatch = content.match(headerPattern);
|
|
2646
|
+
if (!headerMatch) {
|
|
2647
|
+
error(`Could not find Phase ${afterPhase} header`);
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
const headerIdx = content.indexOf(headerMatch[0]);
|
|
2651
|
+
const afterHeader = content.slice(headerIdx + headerMatch[0].length);
|
|
2652
|
+
const nextPhaseMatch = afterHeader.match(/\n###\s+Phase\s+\d/i);
|
|
2653
|
+
|
|
2654
|
+
let insertIdx;
|
|
2655
|
+
if (nextPhaseMatch) {
|
|
2656
|
+
insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
|
|
2657
|
+
} else {
|
|
2658
|
+
insertIdx = content.length;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
const updatedContent = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx);
|
|
2662
|
+
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
|
2663
|
+
|
|
2664
|
+
const result = {
|
|
2665
|
+
phase_number: decimalPhase,
|
|
2666
|
+
after_phase: afterPhase,
|
|
2667
|
+
name: description,
|
|
2668
|
+
slug,
|
|
2669
|
+
directory: `.blueprint/phases/${dirName}`,
|
|
2670
|
+
};
|
|
2671
|
+
|
|
2672
|
+
output(result, raw, decimalPhase);
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// ─── Phase Remove ─────────────────────────────────────────────────────────────
|
|
2676
|
+
|
|
2677
|
+
function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
2678
|
+
if (!targetPhase) {
|
|
2679
|
+
error('phase number required for phase remove');
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
const roadmapPath = path.join(cwd, '.blueprint', 'ROADMAP.md');
|
|
2683
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
2684
|
+
const force = options.force || false;
|
|
2685
|
+
|
|
2686
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
2687
|
+
error('ROADMAP.md not found');
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// Normalize the target
|
|
2691
|
+
const normalized = normalizePhaseName(targetPhase);
|
|
2692
|
+
const isDecimal = targetPhase.includes('.');
|
|
2693
|
+
|
|
2694
|
+
// Find and validate target directory
|
|
2695
|
+
let targetDir = null;
|
|
2696
|
+
try {
|
|
2697
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
2698
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
2699
|
+
targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
|
|
2700
|
+
} catch {}
|
|
2701
|
+
|
|
2702
|
+
// Check for executed work (SUMMARY.md files)
|
|
2703
|
+
if (targetDir && !force) {
|
|
2704
|
+
const targetPath = path.join(phasesDir, targetDir);
|
|
2705
|
+
const files = fs.readdirSync(targetPath);
|
|
2706
|
+
const summaries = files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
2707
|
+
if (summaries.length > 0) {
|
|
2708
|
+
error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// Delete target directory
|
|
2713
|
+
if (targetDir) {
|
|
2714
|
+
fs.rmSync(path.join(phasesDir, targetDir), { recursive: true, force: true });
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
// Renumber subsequent phases
|
|
2718
|
+
const renamedDirs = [];
|
|
2719
|
+
const renamedFiles = [];
|
|
2720
|
+
|
|
2721
|
+
if (isDecimal) {
|
|
2722
|
+
// Decimal removal: renumber sibling decimals (e.g., removing 06.2 → 06.3 becomes 06.2)
|
|
2723
|
+
const baseParts = normalized.split('.');
|
|
2724
|
+
const baseInt = baseParts[0];
|
|
2725
|
+
const removedDecimal = parseInt(baseParts[1], 10);
|
|
2726
|
+
|
|
2727
|
+
try {
|
|
2728
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
2729
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
2730
|
+
|
|
2731
|
+
// Find sibling decimals with higher numbers
|
|
2732
|
+
const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
|
|
2733
|
+
const toRename = [];
|
|
2734
|
+
for (const dir of dirs) {
|
|
2735
|
+
const dm = dir.match(decPattern);
|
|
2736
|
+
if (dm && parseInt(dm[1], 10) > removedDecimal) {
|
|
2737
|
+
toRename.push({ dir, oldDecimal: parseInt(dm[1], 10), slug: dm[2] });
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
// Sort descending to avoid conflicts
|
|
2742
|
+
toRename.sort((a, b) => b.oldDecimal - a.oldDecimal);
|
|
2743
|
+
|
|
2744
|
+
for (const item of toRename) {
|
|
2745
|
+
const newDecimal = item.oldDecimal - 1;
|
|
2746
|
+
const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
|
|
2747
|
+
const newPhaseId = `${baseInt}.${newDecimal}`;
|
|
2748
|
+
const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
|
|
2749
|
+
|
|
2750
|
+
// Rename directory
|
|
2751
|
+
fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
|
|
2752
|
+
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
2753
|
+
|
|
2754
|
+
// Rename files inside
|
|
2755
|
+
const dirFiles = fs.readdirSync(path.join(phasesDir, newDirName));
|
|
2756
|
+
for (const f of dirFiles) {
|
|
2757
|
+
// Files may have phase prefix like "06.2-01-PLAN.md"
|
|
2758
|
+
if (f.includes(oldPhaseId)) {
|
|
2759
|
+
const newFileName = f.replace(oldPhaseId, newPhaseId);
|
|
2760
|
+
fs.renameSync(
|
|
2761
|
+
path.join(phasesDir, newDirName, f),
|
|
2762
|
+
path.join(phasesDir, newDirName, newFileName)
|
|
2763
|
+
);
|
|
2764
|
+
renamedFiles.push({ from: f, to: newFileName });
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
} catch {}
|
|
2769
|
+
|
|
2770
|
+
} else {
|
|
2771
|
+
// Integer removal: renumber all subsequent integer phases
|
|
2772
|
+
const removedInt = parseInt(normalized, 10);
|
|
2773
|
+
|
|
2774
|
+
try {
|
|
2775
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
2776
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
2777
|
+
|
|
2778
|
+
// Collect directories that need renumbering (integer phases > removed, and their decimals)
|
|
2779
|
+
const toRename = [];
|
|
2780
|
+
for (const dir of dirs) {
|
|
2781
|
+
const dm = dir.match(/^(\d+)(?:\.(\d+))?-(.+)$/);
|
|
2782
|
+
if (!dm) continue;
|
|
2783
|
+
const dirInt = parseInt(dm[1], 10);
|
|
2784
|
+
if (dirInt > removedInt) {
|
|
2785
|
+
toRename.push({
|
|
2786
|
+
dir,
|
|
2787
|
+
oldInt: dirInt,
|
|
2788
|
+
decimal: dm[2] ? parseInt(dm[2], 10) : null,
|
|
2789
|
+
slug: dm[3],
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// Sort descending to avoid conflicts
|
|
2795
|
+
toRename.sort((a, b) => {
|
|
2796
|
+
if (a.oldInt !== b.oldInt) return b.oldInt - a.oldInt;
|
|
2797
|
+
return (b.decimal || 0) - (a.decimal || 0);
|
|
2798
|
+
});
|
|
2799
|
+
|
|
2800
|
+
for (const item of toRename) {
|
|
2801
|
+
const newInt = item.oldInt - 1;
|
|
2802
|
+
const newPadded = String(newInt).padStart(2, '0');
|
|
2803
|
+
const oldPadded = String(item.oldInt).padStart(2, '0');
|
|
2804
|
+
const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
|
|
2805
|
+
const oldPrefix = `${oldPadded}${decimalSuffix}`;
|
|
2806
|
+
const newPrefix = `${newPadded}${decimalSuffix}`;
|
|
2807
|
+
const newDirName = `${newPrefix}-${item.slug}`;
|
|
2808
|
+
|
|
2809
|
+
// Rename directory
|
|
2810
|
+
fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
|
|
2811
|
+
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
2812
|
+
|
|
2813
|
+
// Rename files inside
|
|
2814
|
+
const dirFiles = fs.readdirSync(path.join(phasesDir, newDirName));
|
|
2815
|
+
for (const f of dirFiles) {
|
|
2816
|
+
if (f.startsWith(oldPrefix)) {
|
|
2817
|
+
const newFileName = newPrefix + f.slice(oldPrefix.length);
|
|
2818
|
+
fs.renameSync(
|
|
2819
|
+
path.join(phasesDir, newDirName, f),
|
|
2820
|
+
path.join(phasesDir, newDirName, newFileName)
|
|
2821
|
+
);
|
|
2822
|
+
renamedFiles.push({ from: f, to: newFileName });
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
} catch {}
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// Update ROADMAP.md
|
|
2830
|
+
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
2831
|
+
|
|
2832
|
+
// Remove the target phase section
|
|
2833
|
+
const targetEscaped = targetPhase.replace(/\./g, '\\.');
|
|
2834
|
+
const sectionPattern = new RegExp(
|
|
2835
|
+
`\\n?###\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n###\\s+Phase\\s+\\d|$)`,
|
|
2836
|
+
'i'
|
|
2837
|
+
);
|
|
2838
|
+
roadmapContent = roadmapContent.replace(sectionPattern, '');
|
|
2839
|
+
|
|
2840
|
+
// Remove from phase list (checkbox)
|
|
2841
|
+
const checkboxPattern = new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${targetEscaped}[:\\s][^\\n]*`, 'gi');
|
|
2842
|
+
roadmapContent = roadmapContent.replace(checkboxPattern, '');
|
|
2843
|
+
|
|
2844
|
+
// Remove from progress table
|
|
2845
|
+
const tableRowPattern = new RegExp(`\\n?\\|\\s*${targetEscaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi');
|
|
2846
|
+
roadmapContent = roadmapContent.replace(tableRowPattern, '');
|
|
2847
|
+
|
|
2848
|
+
// Renumber references in ROADMAP for subsequent phases
|
|
2849
|
+
if (!isDecimal) {
|
|
2850
|
+
const removedInt = parseInt(normalized, 10);
|
|
2851
|
+
|
|
2852
|
+
// Collect all integer phases > removedInt
|
|
2853
|
+
const maxPhase = 99; // reasonable upper bound
|
|
2854
|
+
for (let oldNum = maxPhase; oldNum > removedInt; oldNum--) {
|
|
2855
|
+
const newNum = oldNum - 1;
|
|
2856
|
+
const oldStr = String(oldNum);
|
|
2857
|
+
const newStr = String(newNum);
|
|
2858
|
+
const oldPad = oldStr.padStart(2, '0');
|
|
2859
|
+
const newPad = newStr.padStart(2, '0');
|
|
2860
|
+
|
|
2861
|
+
// Phase headings: ### Phase 18: → ### Phase 17:
|
|
2862
|
+
roadmapContent = roadmapContent.replace(
|
|
2863
|
+
new RegExp(`(###\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'),
|
|
2864
|
+
`$1${newStr}$2`
|
|
2865
|
+
);
|
|
2866
|
+
|
|
2867
|
+
// Checkbox items: - [ ] **Phase 18:** → - [ ] **Phase 17:**
|
|
2868
|
+
roadmapContent = roadmapContent.replace(
|
|
2869
|
+
new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'),
|
|
2870
|
+
`$1${newStr}$2`
|
|
2871
|
+
);
|
|
2872
|
+
|
|
2873
|
+
// Plan references: 18-01 → 17-01
|
|
2874
|
+
roadmapContent = roadmapContent.replace(
|
|
2875
|
+
new RegExp(`${oldPad}-(\\d{2})`, 'g'),
|
|
2876
|
+
`${newPad}-$1`
|
|
2877
|
+
);
|
|
2878
|
+
|
|
2879
|
+
// Table rows: | 18. → | 17.
|
|
2880
|
+
roadmapContent = roadmapContent.replace(
|
|
2881
|
+
new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'),
|
|
2882
|
+
`$1${newStr}. `
|
|
2883
|
+
);
|
|
2884
|
+
|
|
2885
|
+
// Depends on references
|
|
2886
|
+
roadmapContent = roadmapContent.replace(
|
|
2887
|
+
new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'),
|
|
2888
|
+
`$1${newStr}`
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
|
2894
|
+
|
|
2895
|
+
// Update STATE.md phase count
|
|
2896
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
2897
|
+
if (fs.existsSync(statePath)) {
|
|
2898
|
+
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
2899
|
+
// Update "Total Phases" field
|
|
2900
|
+
const totalPattern = /(\*\*Total Phases:\*\*\s*)(\d+)/;
|
|
2901
|
+
const totalMatch = stateContent.match(totalPattern);
|
|
2902
|
+
if (totalMatch) {
|
|
2903
|
+
const oldTotal = parseInt(totalMatch[2], 10);
|
|
2904
|
+
stateContent = stateContent.replace(totalPattern, `$1${oldTotal - 1}`);
|
|
2905
|
+
}
|
|
2906
|
+
// Update "Phase: X of Y" pattern
|
|
2907
|
+
const ofPattern = /(\bof\s+)(\d+)(\s*(?:\(|phases?))/i;
|
|
2908
|
+
const ofMatch = stateContent.match(ofPattern);
|
|
2909
|
+
if (ofMatch) {
|
|
2910
|
+
const oldTotal = parseInt(ofMatch[2], 10);
|
|
2911
|
+
stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`);
|
|
2912
|
+
}
|
|
2913
|
+
fs.writeFileSync(statePath, stateContent, 'utf-8');
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
const result = {
|
|
2917
|
+
removed: targetPhase,
|
|
2918
|
+
directory_deleted: targetDir || null,
|
|
2919
|
+
renamed_directories: renamedDirs,
|
|
2920
|
+
renamed_files: renamedFiles,
|
|
2921
|
+
roadmap_updated: true,
|
|
2922
|
+
state_updated: fs.existsSync(statePath),
|
|
2923
|
+
};
|
|
2924
|
+
|
|
2925
|
+
output(result, raw);
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
// ─── Phase Complete (Transition) ──────────────────────────────────────────────
|
|
2929
|
+
|
|
2930
|
+
function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
2931
|
+
if (!phaseNum) {
|
|
2932
|
+
error('phase number required for phase complete');
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
const roadmapPath = path.join(cwd, '.blueprint', 'ROADMAP.md');
|
|
2936
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
2937
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
2938
|
+
const normalized = normalizePhaseName(phaseNum);
|
|
2939
|
+
const today = new Date().toISOString().split('T')[0];
|
|
2940
|
+
|
|
2941
|
+
// Verify phase info
|
|
2942
|
+
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
2943
|
+
if (!phaseInfo) {
|
|
2944
|
+
error(`Phase ${phaseNum} not found`);
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
const planCount = phaseInfo.plans.length;
|
|
2948
|
+
const summaryCount = phaseInfo.summaries.length;
|
|
2949
|
+
|
|
2950
|
+
// Update ROADMAP.md: mark phase complete
|
|
2951
|
+
if (fs.existsSync(roadmapPath)) {
|
|
2952
|
+
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
2953
|
+
|
|
2954
|
+
// Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
|
|
2955
|
+
const checkboxPattern = new RegExp(
|
|
2956
|
+
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}[:\\s][^\\n]*)`,
|
|
2957
|
+
'i'
|
|
2958
|
+
);
|
|
2959
|
+
roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
|
|
2960
|
+
|
|
2961
|
+
// Progress table: update Status to Complete, add date
|
|
2962
|
+
const phaseEscaped = phaseNum.replace('.', '\\.');
|
|
2963
|
+
const tablePattern = new RegExp(
|
|
2964
|
+
`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
|
|
2965
|
+
'i'
|
|
2966
|
+
);
|
|
2967
|
+
roadmapContent = roadmapContent.replace(
|
|
2968
|
+
tablePattern,
|
|
2969
|
+
`$1 Complete $2 ${today} $3`
|
|
2970
|
+
);
|
|
2971
|
+
|
|
2972
|
+
// Update plan count in phase section
|
|
2973
|
+
const planCountPattern = new RegExp(
|
|
2974
|
+
`(###\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
|
2975
|
+
'i'
|
|
2976
|
+
);
|
|
2977
|
+
roadmapContent = roadmapContent.replace(
|
|
2978
|
+
planCountPattern,
|
|
2979
|
+
`$1${summaryCount}/${planCount} plans complete`
|
|
2980
|
+
);
|
|
2981
|
+
|
|
2982
|
+
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// Find next phase
|
|
2986
|
+
let nextPhaseNum = null;
|
|
2987
|
+
let nextPhaseName = null;
|
|
2988
|
+
let isLastPhase = true;
|
|
2989
|
+
|
|
2990
|
+
try {
|
|
2991
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
2992
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
2993
|
+
const currentFloat = parseFloat(phaseNum);
|
|
2994
|
+
|
|
2995
|
+
// Find the next phase directory after current
|
|
2996
|
+
for (const dir of dirs) {
|
|
2997
|
+
const dm = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
|
|
2998
|
+
if (dm) {
|
|
2999
|
+
const dirFloat = parseFloat(dm[1]);
|
|
3000
|
+
if (dirFloat > currentFloat) {
|
|
3001
|
+
nextPhaseNum = dm[1];
|
|
3002
|
+
nextPhaseName = dm[2] || null;
|
|
3003
|
+
isLastPhase = false;
|
|
3004
|
+
break;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
} catch {}
|
|
3009
|
+
|
|
3010
|
+
// Update STATE.md
|
|
3011
|
+
if (fs.existsSync(statePath)) {
|
|
3012
|
+
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
3013
|
+
|
|
3014
|
+
// Update Current Phase
|
|
3015
|
+
stateContent = stateContent.replace(
|
|
3016
|
+
/(\*\*Current Phase:\*\*\s*).*/,
|
|
3017
|
+
`$1${nextPhaseNum || phaseNum}`
|
|
3018
|
+
);
|
|
3019
|
+
|
|
3020
|
+
// Update Current Phase Name
|
|
3021
|
+
if (nextPhaseName) {
|
|
3022
|
+
stateContent = stateContent.replace(
|
|
3023
|
+
/(\*\*Current Phase Name:\*\*\s*).*/,
|
|
3024
|
+
`$1${nextPhaseName.replace(/-/g, ' ')}`
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// Update Status
|
|
3029
|
+
stateContent = stateContent.replace(
|
|
3030
|
+
/(\*\*Status:\*\*\s*).*/,
|
|
3031
|
+
`$1${isLastPhase ? 'Milestone complete' : 'Ready to plan'}`
|
|
3032
|
+
);
|
|
3033
|
+
|
|
3034
|
+
// Update Current Plan
|
|
3035
|
+
stateContent = stateContent.replace(
|
|
3036
|
+
/(\*\*Current Plan:\*\*\s*).*/,
|
|
3037
|
+
`$1Not started`
|
|
3038
|
+
);
|
|
3039
|
+
|
|
3040
|
+
// Update Last Activity
|
|
3041
|
+
stateContent = stateContent.replace(
|
|
3042
|
+
/(\*\*Last Activity:\*\*\s*).*/,
|
|
3043
|
+
`$1${today}`
|
|
3044
|
+
);
|
|
3045
|
+
|
|
3046
|
+
// Update Last Activity Description
|
|
3047
|
+
stateContent = stateContent.replace(
|
|
3048
|
+
/(\*\*Last Activity Description:\*\*\s*).*/,
|
|
3049
|
+
`$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`
|
|
3050
|
+
);
|
|
3051
|
+
|
|
3052
|
+
fs.writeFileSync(statePath, stateContent, 'utf-8');
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
const result = {
|
|
3056
|
+
completed_phase: phaseNum,
|
|
3057
|
+
phase_name: phaseInfo.phase_name,
|
|
3058
|
+
plans_executed: `${summaryCount}/${planCount}`,
|
|
3059
|
+
next_phase: nextPhaseNum,
|
|
3060
|
+
next_phase_name: nextPhaseName,
|
|
3061
|
+
is_last_phase: isLastPhase,
|
|
3062
|
+
date: today,
|
|
3063
|
+
roadmap_updated: fs.existsSync(roadmapPath),
|
|
3064
|
+
state_updated: fs.existsSync(statePath),
|
|
3065
|
+
};
|
|
3066
|
+
|
|
3067
|
+
output(result, raw);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
// ─── Milestone Complete ───────────────────────────────────────────────────────
|
|
3071
|
+
|
|
3072
|
+
function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
3073
|
+
if (!version) {
|
|
3074
|
+
error('version required for milestone complete (e.g., v1.0)');
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
const roadmapPath = path.join(cwd, '.blueprint', 'ROADMAP.md');
|
|
3078
|
+
const reqPath = path.join(cwd, '.blueprint', 'REQUIREMENTS.md');
|
|
3079
|
+
const statePath = path.join(cwd, '.blueprint', 'STATE.md');
|
|
3080
|
+
const milestonesPath = path.join(cwd, '.blueprint', 'MILESTONES.md');
|
|
3081
|
+
const archiveDir = path.join(cwd, '.blueprint', 'milestones');
|
|
3082
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
3083
|
+
const today = new Date().toISOString().split('T')[0];
|
|
3084
|
+
const milestoneName = options.name || version;
|
|
3085
|
+
|
|
3086
|
+
// Ensure archive directory exists
|
|
3087
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
3088
|
+
|
|
3089
|
+
// Gather stats from phases
|
|
3090
|
+
let phaseCount = 0;
|
|
3091
|
+
let totalPlans = 0;
|
|
3092
|
+
let totalTasks = 0;
|
|
3093
|
+
const accomplishments = [];
|
|
3094
|
+
|
|
3095
|
+
try {
|
|
3096
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
3097
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
3098
|
+
|
|
3099
|
+
for (const dir of dirs) {
|
|
3100
|
+
phaseCount++;
|
|
3101
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
3102
|
+
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
3103
|
+
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
3104
|
+
totalPlans += plans.length;
|
|
3105
|
+
|
|
3106
|
+
// Extract one-liners from summaries
|
|
3107
|
+
for (const s of summaries) {
|
|
3108
|
+
try {
|
|
3109
|
+
const content = fs.readFileSync(path.join(phasesDir, dir, s), 'utf-8');
|
|
3110
|
+
const fm = extractFrontmatter(content);
|
|
3111
|
+
if (fm['one-liner']) {
|
|
3112
|
+
accomplishments.push(fm['one-liner']);
|
|
3113
|
+
}
|
|
3114
|
+
// Count tasks
|
|
3115
|
+
const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
|
|
3116
|
+
totalTasks += taskMatches.length;
|
|
3117
|
+
} catch {}
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
} catch {}
|
|
3121
|
+
|
|
3122
|
+
// Archive ROADMAP.md
|
|
3123
|
+
if (fs.existsSync(roadmapPath)) {
|
|
3124
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
3125
|
+
fs.writeFileSync(path.join(archiveDir, `${version}-ROADMAP.md`), roadmapContent, 'utf-8');
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// Archive REQUIREMENTS.md
|
|
3129
|
+
if (fs.existsSync(reqPath)) {
|
|
3130
|
+
const reqContent = fs.readFileSync(reqPath, 'utf-8');
|
|
3131
|
+
const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`.blueprint/REQUIREMENTS.md\`.\n\n---\n\n`;
|
|
3132
|
+
fs.writeFileSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent, 'utf-8');
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
// Archive audit file if exists
|
|
3136
|
+
const auditFile = path.join(cwd, '.blueprint', `${version}-MILESTONE-AUDIT.md`);
|
|
3137
|
+
if (fs.existsSync(auditFile)) {
|
|
3138
|
+
fs.renameSync(auditFile, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// Create/append MILESTONES.md entry
|
|
3142
|
+
const accomplishmentsList = accomplishments.map(a => `- ${a}`).join('\n');
|
|
3143
|
+
const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
|
|
3144
|
+
|
|
3145
|
+
if (fs.existsSync(milestonesPath)) {
|
|
3146
|
+
const existing = fs.readFileSync(milestonesPath, 'utf-8');
|
|
3147
|
+
fs.writeFileSync(milestonesPath, existing + '\n' + milestoneEntry, 'utf-8');
|
|
3148
|
+
} else {
|
|
3149
|
+
fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// Update STATE.md
|
|
3153
|
+
if (fs.existsSync(statePath)) {
|
|
3154
|
+
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
3155
|
+
stateContent = stateContent.replace(
|
|
3156
|
+
/(\*\*Status:\*\*\s*).*/,
|
|
3157
|
+
`$1${version} milestone complete`
|
|
3158
|
+
);
|
|
3159
|
+
stateContent = stateContent.replace(
|
|
3160
|
+
/(\*\*Last Activity:\*\*\s*).*/,
|
|
3161
|
+
`$1${today}`
|
|
3162
|
+
);
|
|
3163
|
+
stateContent = stateContent.replace(
|
|
3164
|
+
/(\*\*Last Activity Description:\*\*\s*).*/,
|
|
3165
|
+
`$1${version} milestone completed and archived`
|
|
3166
|
+
);
|
|
3167
|
+
fs.writeFileSync(statePath, stateContent, 'utf-8');
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
const result = {
|
|
3171
|
+
version,
|
|
3172
|
+
name: milestoneName,
|
|
3173
|
+
date: today,
|
|
3174
|
+
phases: phaseCount,
|
|
3175
|
+
plans: totalPlans,
|
|
3176
|
+
tasks: totalTasks,
|
|
3177
|
+
accomplishments,
|
|
3178
|
+
archived: {
|
|
3179
|
+
roadmap: fs.existsSync(path.join(archiveDir, `${version}-ROADMAP.md`)),
|
|
3180
|
+
requirements: fs.existsSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`)),
|
|
3181
|
+
audit: fs.existsSync(path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)),
|
|
3182
|
+
},
|
|
3183
|
+
milestones_updated: true,
|
|
3184
|
+
state_updated: fs.existsSync(statePath),
|
|
3185
|
+
};
|
|
3186
|
+
|
|
3187
|
+
output(result, raw);
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
// ─── Validate Consistency ─────────────────────────────────────────────────────
|
|
3191
|
+
|
|
3192
|
+
function cmdValidateConsistency(cwd, raw) {
|
|
3193
|
+
const roadmapPath = path.join(cwd, '.blueprint', 'ROADMAP.md');
|
|
3194
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
3195
|
+
const errors = [];
|
|
3196
|
+
const warnings = [];
|
|
3197
|
+
|
|
3198
|
+
// Check for ROADMAP
|
|
3199
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
3200
|
+
errors.push('ROADMAP.md not found');
|
|
3201
|
+
output({ passed: false, errors, warnings }, raw, 'failed');
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
3206
|
+
|
|
3207
|
+
// Extract phases from ROADMAP
|
|
3208
|
+
const roadmapPhases = new Set();
|
|
3209
|
+
const phasePattern = /###\s*Phase\s+(\d+(?:\.\d+)?)\s*:/gi;
|
|
3210
|
+
let m;
|
|
3211
|
+
while ((m = phasePattern.exec(roadmapContent)) !== null) {
|
|
3212
|
+
roadmapPhases.add(m[1]);
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// Get phases on disk
|
|
3216
|
+
const diskPhases = new Set();
|
|
3217
|
+
try {
|
|
3218
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
3219
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
3220
|
+
for (const dir of dirs) {
|
|
3221
|
+
const dm = dir.match(/^(\d+(?:\.\d+)?)/);
|
|
3222
|
+
if (dm) diskPhases.add(dm[1]);
|
|
3223
|
+
}
|
|
3224
|
+
} catch {}
|
|
3225
|
+
|
|
3226
|
+
// Check: phases in ROADMAP but not on disk
|
|
3227
|
+
for (const p of roadmapPhases) {
|
|
3228
|
+
if (!diskPhases.has(p) && !diskPhases.has(normalizePhaseName(p))) {
|
|
3229
|
+
warnings.push(`Phase ${p} in ROADMAP.md but no directory on disk`);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
// Check: phases on disk but not in ROADMAP
|
|
3234
|
+
for (const p of diskPhases) {
|
|
3235
|
+
const unpadded = String(parseInt(p, 10));
|
|
3236
|
+
if (!roadmapPhases.has(p) && !roadmapPhases.has(unpadded)) {
|
|
3237
|
+
warnings.push(`Phase ${p} exists on disk but not in ROADMAP.md`);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
// Check: sequential phase numbers (integers only)
|
|
3242
|
+
const integerPhases = [...diskPhases]
|
|
3243
|
+
.filter(p => !p.includes('.'))
|
|
3244
|
+
.map(p => parseInt(p, 10))
|
|
3245
|
+
.sort((a, b) => a - b);
|
|
3246
|
+
|
|
3247
|
+
for (let i = 1; i < integerPhases.length; i++) {
|
|
3248
|
+
if (integerPhases[i] !== integerPhases[i - 1] + 1) {
|
|
3249
|
+
warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// Check: plan numbering within phases
|
|
3254
|
+
try {
|
|
3255
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
3256
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
3257
|
+
|
|
3258
|
+
for (const dir of dirs) {
|
|
3259
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
3260
|
+
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md')).sort();
|
|
3261
|
+
|
|
3262
|
+
// Extract plan numbers
|
|
3263
|
+
const planNums = plans.map(p => {
|
|
3264
|
+
const pm = p.match(/-(\d{2})-PLAN\.md$/);
|
|
3265
|
+
return pm ? parseInt(pm[1], 10) : null;
|
|
3266
|
+
}).filter(n => n !== null);
|
|
3267
|
+
|
|
3268
|
+
for (let i = 1; i < planNums.length; i++) {
|
|
3269
|
+
if (planNums[i] !== planNums[i - 1] + 1) {
|
|
3270
|
+
warnings.push(`Gap in plan numbering in ${dir}: plan ${planNums[i - 1]} → ${planNums[i]}`);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
// Check: plans without summaries (completed plans)
|
|
3275
|
+
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md'));
|
|
3276
|
+
const planIds = new Set(plans.map(p => p.replace('-PLAN.md', '')));
|
|
3277
|
+
const summaryIds = new Set(summaries.map(s => s.replace('-SUMMARY.md', '')));
|
|
3278
|
+
|
|
3279
|
+
// Summary without matching plan is suspicious
|
|
3280
|
+
for (const sid of summaryIds) {
|
|
3281
|
+
if (!planIds.has(sid)) {
|
|
3282
|
+
warnings.push(`Summary ${sid}-SUMMARY.md in ${dir} has no matching PLAN.md`);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
} catch {}
|
|
3287
|
+
|
|
3288
|
+
// Check: frontmatter in plans has required fields
|
|
3289
|
+
try {
|
|
3290
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
3291
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
3292
|
+
|
|
3293
|
+
for (const dir of dirs) {
|
|
3294
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
3295
|
+
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md'));
|
|
3296
|
+
|
|
3297
|
+
for (const plan of plans) {
|
|
3298
|
+
const content = fs.readFileSync(path.join(phasesDir, dir, plan), 'utf-8');
|
|
3299
|
+
const fm = extractFrontmatter(content);
|
|
3300
|
+
|
|
3301
|
+
if (!fm.wave) {
|
|
3302
|
+
warnings.push(`${dir}/${plan}: missing 'wave' in frontmatter`);
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
} catch {}
|
|
3307
|
+
|
|
3308
|
+
const passed = errors.length === 0;
|
|
3309
|
+
output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
// ─── Progress Render ──────────────────────────────────────────────────────────
|
|
3313
|
+
|
|
3314
|
+
function cmdProgressRender(cwd, format, raw) {
|
|
3315
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
3316
|
+
const roadmapPath = path.join(cwd, '.blueprint', 'ROADMAP.md');
|
|
3317
|
+
const milestone = getMilestoneInfo(cwd);
|
|
3318
|
+
|
|
3319
|
+
const phases = [];
|
|
3320
|
+
let totalPlans = 0;
|
|
3321
|
+
let totalSummaries = 0;
|
|
3322
|
+
|
|
3323
|
+
try {
|
|
3324
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
3325
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => {
|
|
3326
|
+
const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
3327
|
+
const bNum = parseFloat(b.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
3328
|
+
return aNum - bNum;
|
|
3329
|
+
});
|
|
3330
|
+
|
|
3331
|
+
for (const dir of dirs) {
|
|
3332
|
+
const dm = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
|
|
3333
|
+
const phaseNum = dm ? dm[1] : dir;
|
|
3334
|
+
const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
3335
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
3336
|
+
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
3337
|
+
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
3338
|
+
|
|
3339
|
+
totalPlans += plans;
|
|
3340
|
+
totalSummaries += summaries;
|
|
3341
|
+
|
|
3342
|
+
let status;
|
|
3343
|
+
if (plans === 0) status = 'Pending';
|
|
3344
|
+
else if (summaries >= plans) status = 'Complete';
|
|
3345
|
+
else if (summaries > 0) status = 'In Progress';
|
|
3346
|
+
else status = 'Planned';
|
|
3347
|
+
|
|
3348
|
+
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
|
|
3349
|
+
}
|
|
3350
|
+
} catch {}
|
|
3351
|
+
|
|
3352
|
+
const percent = totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0;
|
|
3353
|
+
|
|
3354
|
+
if (format === 'table') {
|
|
3355
|
+
// Render markdown table
|
|
3356
|
+
const barWidth = 10;
|
|
3357
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
3358
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
3359
|
+
let out = `# ${milestone.version} ${milestone.name}\n\n`;
|
|
3360
|
+
out += `**Progress:** [${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)\n\n`;
|
|
3361
|
+
out += `| Phase | Name | Plans | Status |\n`;
|
|
3362
|
+
out += `|-------|------|-------|--------|\n`;
|
|
3363
|
+
for (const p of phases) {
|
|
3364
|
+
out += `| ${p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`;
|
|
3365
|
+
}
|
|
3366
|
+
output({ rendered: out }, raw, out);
|
|
3367
|
+
} else if (format === 'bar') {
|
|
3368
|
+
const barWidth = 20;
|
|
3369
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
3370
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
3371
|
+
const text = `[${bar}] ${totalSummaries}/${totalPlans} plans (${percent}%)`;
|
|
3372
|
+
output({ bar: text, percent, completed: totalSummaries, total: totalPlans }, raw, text);
|
|
3373
|
+
} else {
|
|
3374
|
+
// JSON format
|
|
3375
|
+
output({
|
|
3376
|
+
milestone_version: milestone.version,
|
|
3377
|
+
milestone_name: milestone.name,
|
|
3378
|
+
phases,
|
|
3379
|
+
total_plans: totalPlans,
|
|
3380
|
+
total_summaries: totalSummaries,
|
|
3381
|
+
percent,
|
|
3382
|
+
}, raw);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
// ─── Todo Complete ────────────────────────────────────────────────────────────
|
|
3387
|
+
|
|
3388
|
+
function cmdTodoComplete(cwd, filename, raw) {
|
|
3389
|
+
if (!filename) {
|
|
3390
|
+
error('filename required for todo complete');
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
const pendingDir = path.join(cwd, '.blueprint', 'todos', 'pending');
|
|
3394
|
+
const completedDir = path.join(cwd, '.blueprint', 'todos', 'completed');
|
|
3395
|
+
const sourcePath = path.join(pendingDir, filename);
|
|
3396
|
+
|
|
3397
|
+
if (!fs.existsSync(sourcePath)) {
|
|
3398
|
+
error(`Todo not found: ${filename}`);
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// Ensure completed directory exists
|
|
3402
|
+
fs.mkdirSync(completedDir, { recursive: true });
|
|
3403
|
+
|
|
3404
|
+
// Read, add completion timestamp, move
|
|
3405
|
+
let content = fs.readFileSync(sourcePath, 'utf-8');
|
|
3406
|
+
const today = new Date().toISOString().split('T')[0];
|
|
3407
|
+
content = `completed: ${today}\n` + content;
|
|
3408
|
+
|
|
3409
|
+
fs.writeFileSync(path.join(completedDir, filename), content, 'utf-8');
|
|
3410
|
+
fs.unlinkSync(sourcePath);
|
|
3411
|
+
|
|
3412
|
+
output({ completed: true, file: filename, date: today }, raw, 'completed');
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
// ─── Scaffold ─────────────────────────────────────────────────────────────────
|
|
3416
|
+
|
|
3417
|
+
function cmdScaffold(cwd, type, options, raw) {
|
|
3418
|
+
const { phase, name } = options;
|
|
3419
|
+
const padded = phase ? normalizePhaseName(phase) : '00';
|
|
3420
|
+
const today = new Date().toISOString().split('T')[0];
|
|
3421
|
+
|
|
3422
|
+
// Find phase directory
|
|
3423
|
+
const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
|
|
3424
|
+
const phaseDir = phaseInfo ? path.join(cwd, phaseInfo.directory) : null;
|
|
3425
|
+
|
|
3426
|
+
if (phase && !phaseDir && type !== 'phase-dir') {
|
|
3427
|
+
error(`Phase ${phase} directory not found`);
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
let filePath, content;
|
|
3431
|
+
|
|
3432
|
+
switch (type) {
|
|
3433
|
+
case 'context': {
|
|
3434
|
+
filePath = path.join(phaseDir, `${padded}-CONTEXT.md`);
|
|
3435
|
+
content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during /bp:discuss-phase ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
|
|
3436
|
+
break;
|
|
3437
|
+
}
|
|
3438
|
+
case 'uat': {
|
|
3439
|
+
filePath = path.join(phaseDir, `${padded}-UAT.md`);
|
|
3440
|
+
content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — User Acceptance Testing\n\n## Test Results\n\n| # | Test | Status | Notes |\n|---|------|--------|-------|\n\n## Summary\n\n_Pending UAT_\n`;
|
|
3441
|
+
break;
|
|
3442
|
+
}
|
|
3443
|
+
case 'verification': {
|
|
3444
|
+
filePath = path.join(phaseDir, `${padded}-VERIFICATION.md`);
|
|
3445
|
+
content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Verification\n\n## Goal-Backward Verification\n\n**Phase Goal:** [From ROADMAP.md]\n\n## Checks\n\n| # | Requirement | Status | Evidence |\n|---|------------|--------|----------|\n\n## Result\n\n_Pending verification_\n`;
|
|
3446
|
+
break;
|
|
3447
|
+
}
|
|
3448
|
+
case 'phase-dir': {
|
|
3449
|
+
if (!phase || !name) {
|
|
3450
|
+
error('phase and name required for phase-dir scaffold');
|
|
3451
|
+
}
|
|
3452
|
+
const slug = generateSlugInternal(name);
|
|
3453
|
+
const dirName = `${padded}-${slug}`;
|
|
3454
|
+
const phasesParent = path.join(cwd, '.blueprint', 'phases');
|
|
3455
|
+
fs.mkdirSync(phasesParent, { recursive: true });
|
|
3456
|
+
const dirPath = path.join(phasesParent, dirName);
|
|
3457
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
3458
|
+
output({ created: true, directory: `.blueprint/phases/${dirName}`, path: dirPath }, raw, dirPath);
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
default:
|
|
3462
|
+
error(`Unknown scaffold type: ${type}. Available: context, uat, verification, phase-dir`);
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
if (fs.existsSync(filePath)) {
|
|
3466
|
+
output({ created: false, reason: 'already_exists', path: filePath }, raw, 'exists');
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
3471
|
+
const relPath = path.relative(cwd, filePath);
|
|
3472
|
+
output({ created: true, path: relPath }, raw, relPath);
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
// ─── Compound Commands ────────────────────────────────────────────────────────
|
|
3476
|
+
|
|
3477
|
+
function resolveModelInternal(cwd, agentType) {
|
|
3478
|
+
const config = loadConfig(cwd);
|
|
3479
|
+
const profile = config.model_profile || 'balanced';
|
|
3480
|
+
const agentModels = MODEL_PROFILES[agentType];
|
|
3481
|
+
if (!agentModels) return 'sonnet';
|
|
3482
|
+
return agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
function findPhaseInternal(cwd, phase) {
|
|
3486
|
+
if (!phase) return null;
|
|
3487
|
+
|
|
3488
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
3489
|
+
const normalized = normalizePhaseName(phase);
|
|
3490
|
+
|
|
3491
|
+
try {
|
|
3492
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
3493
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
3494
|
+
const match = dirs.find(d => d.startsWith(normalized));
|
|
3495
|
+
if (!match) return null;
|
|
3496
|
+
|
|
3497
|
+
const dirMatch = match.match(/^(\d+(?:\.\d+)?)-?(.*)/);
|
|
3498
|
+
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
3499
|
+
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
3500
|
+
const phaseDir = path.join(phasesDir, match);
|
|
3501
|
+
const phaseFiles = fs.readdirSync(phaseDir);
|
|
3502
|
+
|
|
3503
|
+
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
|
|
3504
|
+
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').sort();
|
|
3505
|
+
const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
3506
|
+
const hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
3507
|
+
const hasVerification = phaseFiles.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
|
|
3508
|
+
|
|
3509
|
+
// Determine incomplete plans (plans without matching summaries)
|
|
3510
|
+
const completedPlanIds = new Set(
|
|
3511
|
+
summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
|
3512
|
+
);
|
|
3513
|
+
const incompletePlans = plans.filter(p => {
|
|
3514
|
+
const planId = p.replace('-PLAN.md', '').replace('PLAN.md', '');
|
|
3515
|
+
return !completedPlanIds.has(planId);
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
return {
|
|
3519
|
+
found: true,
|
|
3520
|
+
directory: path.join('.blueprint', 'phases', match),
|
|
3521
|
+
phase_number: phaseNumber,
|
|
3522
|
+
phase_name: phaseName,
|
|
3523
|
+
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
3524
|
+
plans,
|
|
3525
|
+
summaries,
|
|
3526
|
+
incomplete_plans: incompletePlans,
|
|
3527
|
+
has_research: hasResearch,
|
|
3528
|
+
has_context: hasContext,
|
|
3529
|
+
has_verification: hasVerification,
|
|
3530
|
+
};
|
|
3531
|
+
} catch {
|
|
3532
|
+
return null;
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
function pathExistsInternal(cwd, targetPath) {
|
|
3537
|
+
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
|
|
3538
|
+
try {
|
|
3539
|
+
fs.statSync(fullPath);
|
|
3540
|
+
return true;
|
|
3541
|
+
} catch {
|
|
3542
|
+
return false;
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
function generateSlugInternal(text) {
|
|
3547
|
+
if (!text) return null;
|
|
3548
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
function getMilestoneInfo(cwd) {
|
|
3552
|
+
try {
|
|
3553
|
+
const roadmap = fs.readFileSync(path.join(cwd, '.blueprint', 'ROADMAP.md'), 'utf-8');
|
|
3554
|
+
const versionMatch = roadmap.match(/v(\d+\.\d+)/);
|
|
3555
|
+
const nameMatch = roadmap.match(/## .*v\d+\.\d+[:\s]+([^\n(]+)/);
|
|
3556
|
+
return {
|
|
3557
|
+
version: versionMatch ? versionMatch[0] : 'v1.0',
|
|
3558
|
+
name: nameMatch ? nameMatch[1].trim() : 'milestone',
|
|
3559
|
+
};
|
|
3560
|
+
} catch {
|
|
3561
|
+
return { version: 'v1.0', name: 'milestone' };
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
function cmdInitExecutePhase(cwd, phase, includes, raw) {
|
|
3566
|
+
if (!phase) {
|
|
3567
|
+
error('phase required for init execute-phase');
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
const config = loadConfig(cwd);
|
|
3571
|
+
const phaseInfo = findPhaseInternal(cwd, phase);
|
|
3572
|
+
const milestone = getMilestoneInfo(cwd);
|
|
3573
|
+
|
|
3574
|
+
const result = {
|
|
3575
|
+
// Models
|
|
3576
|
+
executor_model: resolveModelInternal(cwd, 'bp-executor'),
|
|
3577
|
+
verifier_model: resolveModelInternal(cwd, 'bp-verifier'),
|
|
3578
|
+
|
|
3579
|
+
// Config flags
|
|
3580
|
+
commit_docs: config.commit_docs,
|
|
3581
|
+
parallelization: config.parallelization,
|
|
3582
|
+
branching_strategy: config.branching_strategy,
|
|
3583
|
+
phase_branch_template: config.phase_branch_template,
|
|
3584
|
+
milestone_branch_template: config.milestone_branch_template,
|
|
3585
|
+
verifier_enabled: config.verifier,
|
|
3586
|
+
|
|
3587
|
+
// Phase info
|
|
3588
|
+
phase_found: !!phaseInfo,
|
|
3589
|
+
phase_dir: phaseInfo?.directory || null,
|
|
3590
|
+
phase_number: phaseInfo?.phase_number || null,
|
|
3591
|
+
phase_name: phaseInfo?.phase_name || null,
|
|
3592
|
+
phase_slug: phaseInfo?.phase_slug || null,
|
|
3593
|
+
|
|
3594
|
+
// Plan inventory
|
|
3595
|
+
plans: phaseInfo?.plans || [],
|
|
3596
|
+
summaries: phaseInfo?.summaries || [],
|
|
3597
|
+
incomplete_plans: phaseInfo?.incomplete_plans || [],
|
|
3598
|
+
plan_count: phaseInfo?.plans?.length || 0,
|
|
3599
|
+
incomplete_count: phaseInfo?.incomplete_plans?.length || 0,
|
|
3600
|
+
|
|
3601
|
+
// Branch name (pre-computed)
|
|
3602
|
+
branch_name: config.branching_strategy === 'phase' && phaseInfo
|
|
3603
|
+
? config.phase_branch_template
|
|
3604
|
+
.replace('{phase}', phaseInfo.phase_number)
|
|
3605
|
+
.replace('{slug}', phaseInfo.phase_slug || 'phase')
|
|
3606
|
+
: config.branching_strategy === 'milestone'
|
|
3607
|
+
? config.milestone_branch_template
|
|
3608
|
+
.replace('{milestone}', milestone.version)
|
|
3609
|
+
.replace('{slug}', generateSlugInternal(milestone.name) || 'milestone')
|
|
3610
|
+
: null,
|
|
3611
|
+
|
|
3612
|
+
// Milestone info
|
|
3613
|
+
milestone_version: milestone.version,
|
|
3614
|
+
milestone_name: milestone.name,
|
|
3615
|
+
milestone_slug: generateSlugInternal(milestone.name),
|
|
3616
|
+
|
|
3617
|
+
// File existence
|
|
3618
|
+
state_exists: pathExistsInternal(cwd, '.blueprint/STATE.md'),
|
|
3619
|
+
roadmap_exists: pathExistsInternal(cwd, '.blueprint/ROADMAP.md'),
|
|
3620
|
+
config_exists: pathExistsInternal(cwd, '.blueprint/config.json'),
|
|
3621
|
+
};
|
|
3622
|
+
|
|
3623
|
+
// Include file contents if requested via --include
|
|
3624
|
+
if (includes.has('state')) {
|
|
3625
|
+
result.state_content = safeReadFile(path.join(cwd, '.blueprint', 'STATE.md'));
|
|
3626
|
+
}
|
|
3627
|
+
if (includes.has('config')) {
|
|
3628
|
+
result.config_content = safeReadFile(path.join(cwd, '.blueprint', 'config.json'));
|
|
3629
|
+
}
|
|
3630
|
+
if (includes.has('roadmap')) {
|
|
3631
|
+
result.roadmap_content = safeReadFile(path.join(cwd, '.blueprint', 'ROADMAP.md'));
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
output(result, raw);
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
function cmdInitPlanPhase(cwd, phase, includes, raw) {
|
|
3638
|
+
if (!phase) {
|
|
3639
|
+
error('phase required for init plan-phase');
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
const config = loadConfig(cwd);
|
|
3643
|
+
const phaseInfo = findPhaseInternal(cwd, phase);
|
|
3644
|
+
|
|
3645
|
+
const result = {
|
|
3646
|
+
// Models
|
|
3647
|
+
researcher_model: resolveModelInternal(cwd, 'bp-phase-researcher'),
|
|
3648
|
+
planner_model: resolveModelInternal(cwd, 'bp-planner'),
|
|
3649
|
+
checker_model: resolveModelInternal(cwd, 'bp-plan-checker'),
|
|
3650
|
+
|
|
3651
|
+
// Workflow flags
|
|
3652
|
+
research_enabled: config.research,
|
|
3653
|
+
plan_checker_enabled: config.plan_checker,
|
|
3654
|
+
commit_docs: config.commit_docs,
|
|
3655
|
+
|
|
3656
|
+
// Phase info
|
|
3657
|
+
phase_found: !!phaseInfo,
|
|
3658
|
+
phase_dir: phaseInfo?.directory || null,
|
|
3659
|
+
phase_number: phaseInfo?.phase_number || null,
|
|
3660
|
+
phase_name: phaseInfo?.phase_name || null,
|
|
3661
|
+
phase_slug: phaseInfo?.phase_slug || null,
|
|
3662
|
+
padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
|
|
3663
|
+
|
|
3664
|
+
// Existing artifacts
|
|
3665
|
+
has_research: phaseInfo?.has_research || false,
|
|
3666
|
+
has_context: phaseInfo?.has_context || false,
|
|
3667
|
+
has_plans: (phaseInfo?.plans?.length || 0) > 0,
|
|
3668
|
+
plan_count: phaseInfo?.plans?.length || 0,
|
|
3669
|
+
|
|
3670
|
+
// Environment
|
|
3671
|
+
planning_exists: pathExistsInternal(cwd, '.blueprint'),
|
|
3672
|
+
roadmap_exists: pathExistsInternal(cwd, '.blueprint/ROADMAP.md'),
|
|
3673
|
+
};
|
|
3674
|
+
|
|
3675
|
+
// Include file contents if requested via --include
|
|
3676
|
+
if (includes.has('state')) {
|
|
3677
|
+
result.state_content = safeReadFile(path.join(cwd, '.blueprint', 'STATE.md'));
|
|
3678
|
+
}
|
|
3679
|
+
if (includes.has('roadmap')) {
|
|
3680
|
+
result.roadmap_content = safeReadFile(path.join(cwd, '.blueprint', 'ROADMAP.md'));
|
|
3681
|
+
}
|
|
3682
|
+
if (includes.has('requirements')) {
|
|
3683
|
+
result.requirements_content = safeReadFile(path.join(cwd, '.blueprint', 'REQUIREMENTS.md'));
|
|
3684
|
+
}
|
|
3685
|
+
if (includes.has('context') && phaseInfo?.directory) {
|
|
3686
|
+
// Find *-CONTEXT.md in phase directory
|
|
3687
|
+
const phaseDirFull = path.join(cwd, phaseInfo.directory);
|
|
3688
|
+
try {
|
|
3689
|
+
const files = fs.readdirSync(phaseDirFull);
|
|
3690
|
+
const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
3691
|
+
if (contextFile) {
|
|
3692
|
+
result.context_content = safeReadFile(path.join(phaseDirFull, contextFile));
|
|
3693
|
+
}
|
|
3694
|
+
} catch {}
|
|
3695
|
+
}
|
|
3696
|
+
if (includes.has('research') && phaseInfo?.directory) {
|
|
3697
|
+
// Find *-RESEARCH.md in phase directory
|
|
3698
|
+
const phaseDirFull = path.join(cwd, phaseInfo.directory);
|
|
3699
|
+
try {
|
|
3700
|
+
const files = fs.readdirSync(phaseDirFull);
|
|
3701
|
+
const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
3702
|
+
if (researchFile) {
|
|
3703
|
+
result.research_content = safeReadFile(path.join(phaseDirFull, researchFile));
|
|
3704
|
+
}
|
|
3705
|
+
} catch {}
|
|
3706
|
+
}
|
|
3707
|
+
if (includes.has('verification') && phaseInfo?.directory) {
|
|
3708
|
+
// Find *-VERIFICATION.md in phase directory
|
|
3709
|
+
const phaseDirFull = path.join(cwd, phaseInfo.directory);
|
|
3710
|
+
try {
|
|
3711
|
+
const files = fs.readdirSync(phaseDirFull);
|
|
3712
|
+
const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
|
|
3713
|
+
if (verificationFile) {
|
|
3714
|
+
result.verification_content = safeReadFile(path.join(phaseDirFull, verificationFile));
|
|
3715
|
+
}
|
|
3716
|
+
} catch {}
|
|
3717
|
+
}
|
|
3718
|
+
if (includes.has('uat') && phaseInfo?.directory) {
|
|
3719
|
+
// Find *-UAT.md in phase directory
|
|
3720
|
+
const phaseDirFull = path.join(cwd, phaseInfo.directory);
|
|
3721
|
+
try {
|
|
3722
|
+
const files = fs.readdirSync(phaseDirFull);
|
|
3723
|
+
const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
|
|
3724
|
+
if (uatFile) {
|
|
3725
|
+
result.uat_content = safeReadFile(path.join(phaseDirFull, uatFile));
|
|
3726
|
+
}
|
|
3727
|
+
} catch {}
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
output(result, raw);
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
function cmdInitNewProject(cwd, raw) {
|
|
3734
|
+
const config = loadConfig(cwd);
|
|
3735
|
+
|
|
3736
|
+
// Detect Brave Search API key availability
|
|
3737
|
+
const homedir = require('os').homedir();
|
|
3738
|
+
const braveKeyFile = path.join(homedir, '.blueprint', 'brave_api_key');
|
|
3739
|
+
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
|
|
3740
|
+
|
|
3741
|
+
// Detect existing code
|
|
3742
|
+
let hasCode = false;
|
|
3743
|
+
let hasPackageFile = false;
|
|
3744
|
+
try {
|
|
3745
|
+
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', {
|
|
3746
|
+
cwd,
|
|
3747
|
+
encoding: 'utf-8',
|
|
3748
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3749
|
+
});
|
|
3750
|
+
hasCode = files.trim().length > 0;
|
|
3751
|
+
} catch {}
|
|
3752
|
+
|
|
3753
|
+
hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
|
|
3754
|
+
pathExistsInternal(cwd, 'requirements.txt') ||
|
|
3755
|
+
pathExistsInternal(cwd, 'Cargo.toml') ||
|
|
3756
|
+
pathExistsInternal(cwd, 'go.mod') ||
|
|
3757
|
+
pathExistsInternal(cwd, 'Package.swift');
|
|
3758
|
+
|
|
3759
|
+
const result = {
|
|
3760
|
+
// Models
|
|
3761
|
+
researcher_model: resolveModelInternal(cwd, 'bp-project-researcher'),
|
|
3762
|
+
synthesizer_model: resolveModelInternal(cwd, 'bp-research-synthesizer'),
|
|
3763
|
+
roadmapper_model: resolveModelInternal(cwd, 'bp-roadmapper'),
|
|
3764
|
+
|
|
3765
|
+
// Config
|
|
3766
|
+
commit_docs: config.commit_docs,
|
|
3767
|
+
|
|
3768
|
+
// Existing state
|
|
3769
|
+
project_exists: pathExistsInternal(cwd, '.blueprint/PROJECT.md'),
|
|
3770
|
+
has_codebase_map: pathExistsInternal(cwd, '.blueprint/codebase'),
|
|
3771
|
+
planning_exists: pathExistsInternal(cwd, '.blueprint'),
|
|
3772
|
+
|
|
3773
|
+
// Brownfield detection
|
|
3774
|
+
has_existing_code: hasCode,
|
|
3775
|
+
has_package_file: hasPackageFile,
|
|
3776
|
+
is_brownfield: hasCode || hasPackageFile,
|
|
3777
|
+
needs_codebase_map: (hasCode || hasPackageFile) && !pathExistsInternal(cwd, '.blueprint/codebase'),
|
|
3778
|
+
|
|
3779
|
+
// Git state
|
|
3780
|
+
has_git: pathExistsInternal(cwd, '.git'),
|
|
3781
|
+
|
|
3782
|
+
// Enhanced search
|
|
3783
|
+
brave_search_available: hasBraveSearch,
|
|
3784
|
+
};
|
|
3785
|
+
|
|
3786
|
+
output(result, raw);
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
function cmdInitNewMilestone(cwd, raw) {
|
|
3790
|
+
const config = loadConfig(cwd);
|
|
3791
|
+
const milestone = getMilestoneInfo(cwd);
|
|
3792
|
+
|
|
3793
|
+
const result = {
|
|
3794
|
+
// Models
|
|
3795
|
+
researcher_model: resolveModelInternal(cwd, 'bp-project-researcher'),
|
|
3796
|
+
synthesizer_model: resolveModelInternal(cwd, 'bp-research-synthesizer'),
|
|
3797
|
+
roadmapper_model: resolveModelInternal(cwd, 'bp-roadmapper'),
|
|
3798
|
+
|
|
3799
|
+
// Config
|
|
3800
|
+
commit_docs: config.commit_docs,
|
|
3801
|
+
research_enabled: config.research,
|
|
3802
|
+
|
|
3803
|
+
// Current milestone
|
|
3804
|
+
current_milestone: milestone.version,
|
|
3805
|
+
current_milestone_name: milestone.name,
|
|
3806
|
+
|
|
3807
|
+
// File existence
|
|
3808
|
+
project_exists: pathExistsInternal(cwd, '.blueprint/PROJECT.md'),
|
|
3809
|
+
roadmap_exists: pathExistsInternal(cwd, '.blueprint/ROADMAP.md'),
|
|
3810
|
+
state_exists: pathExistsInternal(cwd, '.blueprint/STATE.md'),
|
|
3811
|
+
};
|
|
3812
|
+
|
|
3813
|
+
output(result, raw);
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
function cmdInitQuick(cwd, description, raw) {
|
|
3817
|
+
const config = loadConfig(cwd);
|
|
3818
|
+
const now = new Date();
|
|
3819
|
+
const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
|
|
3820
|
+
|
|
3821
|
+
// Find next quick task number
|
|
3822
|
+
const quickDir = path.join(cwd, '.blueprint', 'quick');
|
|
3823
|
+
let nextNum = 1;
|
|
3824
|
+
try {
|
|
3825
|
+
const existing = fs.readdirSync(quickDir)
|
|
3826
|
+
.filter(f => /^\d+-/.test(f))
|
|
3827
|
+
.map(f => parseInt(f.split('-')[0], 10))
|
|
3828
|
+
.filter(n => !isNaN(n));
|
|
3829
|
+
if (existing.length > 0) {
|
|
3830
|
+
nextNum = Math.max(...existing) + 1;
|
|
3831
|
+
}
|
|
3832
|
+
} catch {}
|
|
3833
|
+
|
|
3834
|
+
const result = {
|
|
3835
|
+
// Models
|
|
3836
|
+
planner_model: resolveModelInternal(cwd, 'bp-planner'),
|
|
3837
|
+
executor_model: resolveModelInternal(cwd, 'bp-executor'),
|
|
3838
|
+
|
|
3839
|
+
// Config
|
|
3840
|
+
commit_docs: config.commit_docs,
|
|
3841
|
+
|
|
3842
|
+
// Quick task info
|
|
3843
|
+
next_num: nextNum,
|
|
3844
|
+
slug: slug,
|
|
3845
|
+
description: description || null,
|
|
3846
|
+
|
|
3847
|
+
// Timestamps
|
|
3848
|
+
date: now.toISOString().split('T')[0],
|
|
3849
|
+
timestamp: now.toISOString(),
|
|
3850
|
+
|
|
3851
|
+
// Paths
|
|
3852
|
+
quick_dir: '.blueprint/quick',
|
|
3853
|
+
task_dir: slug ? `.blueprint/quick/${nextNum}-${slug}` : null,
|
|
3854
|
+
|
|
3855
|
+
// File existence
|
|
3856
|
+
roadmap_exists: pathExistsInternal(cwd, '.blueprint/ROADMAP.md'),
|
|
3857
|
+
planning_exists: pathExistsInternal(cwd, '.blueprint'),
|
|
3858
|
+
};
|
|
3859
|
+
|
|
3860
|
+
output(result, raw);
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
function cmdInitResume(cwd, raw) {
|
|
3864
|
+
const config = loadConfig(cwd);
|
|
3865
|
+
|
|
3866
|
+
// Check for interrupted agent
|
|
3867
|
+
let interruptedAgentId = null;
|
|
3868
|
+
try {
|
|
3869
|
+
interruptedAgentId = fs.readFileSync(path.join(cwd, '.blueprint', 'current-agent-id.txt'), 'utf-8').trim();
|
|
3870
|
+
} catch {}
|
|
3871
|
+
|
|
3872
|
+
const result = {
|
|
3873
|
+
// File existence
|
|
3874
|
+
state_exists: pathExistsInternal(cwd, '.blueprint/STATE.md'),
|
|
3875
|
+
roadmap_exists: pathExistsInternal(cwd, '.blueprint/ROADMAP.md'),
|
|
3876
|
+
project_exists: pathExistsInternal(cwd, '.blueprint/PROJECT.md'),
|
|
3877
|
+
planning_exists: pathExistsInternal(cwd, '.blueprint'),
|
|
3878
|
+
|
|
3879
|
+
// Agent state
|
|
3880
|
+
has_interrupted_agent: !!interruptedAgentId,
|
|
3881
|
+
interrupted_agent_id: interruptedAgentId,
|
|
3882
|
+
|
|
3883
|
+
// Config
|
|
3884
|
+
commit_docs: config.commit_docs,
|
|
3885
|
+
};
|
|
3886
|
+
|
|
3887
|
+
output(result, raw);
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
function cmdInitVerifyWork(cwd, phase, raw) {
|
|
3891
|
+
if (!phase) {
|
|
3892
|
+
error('phase required for init verify-work');
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
const config = loadConfig(cwd);
|
|
3896
|
+
const phaseInfo = findPhaseInternal(cwd, phase);
|
|
3897
|
+
|
|
3898
|
+
const result = {
|
|
3899
|
+
// Models
|
|
3900
|
+
planner_model: resolveModelInternal(cwd, 'bp-planner'),
|
|
3901
|
+
checker_model: resolveModelInternal(cwd, 'bp-plan-checker'),
|
|
3902
|
+
|
|
3903
|
+
// Config
|
|
3904
|
+
commit_docs: config.commit_docs,
|
|
3905
|
+
|
|
3906
|
+
// Phase info
|
|
3907
|
+
phase_found: !!phaseInfo,
|
|
3908
|
+
phase_dir: phaseInfo?.directory || null,
|
|
3909
|
+
phase_number: phaseInfo?.phase_number || null,
|
|
3910
|
+
phase_name: phaseInfo?.phase_name || null,
|
|
3911
|
+
|
|
3912
|
+
// Existing artifacts
|
|
3913
|
+
has_verification: phaseInfo?.has_verification || false,
|
|
3914
|
+
};
|
|
3915
|
+
|
|
3916
|
+
output(result, raw);
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
function cmdInitPhaseOp(cwd, phase, raw) {
|
|
3920
|
+
const config = loadConfig(cwd);
|
|
3921
|
+
const phaseInfo = findPhaseInternal(cwd, phase);
|
|
3922
|
+
|
|
3923
|
+
const result = {
|
|
3924
|
+
// Config
|
|
3925
|
+
commit_docs: config.commit_docs,
|
|
3926
|
+
brave_search: config.brave_search,
|
|
3927
|
+
|
|
3928
|
+
// Phase info
|
|
3929
|
+
phase_found: !!phaseInfo,
|
|
3930
|
+
phase_dir: phaseInfo?.directory || null,
|
|
3931
|
+
phase_number: phaseInfo?.phase_number || null,
|
|
3932
|
+
phase_name: phaseInfo?.phase_name || null,
|
|
3933
|
+
phase_slug: phaseInfo?.phase_slug || null,
|
|
3934
|
+
padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
|
|
3935
|
+
|
|
3936
|
+
// Existing artifacts
|
|
3937
|
+
has_research: phaseInfo?.has_research || false,
|
|
3938
|
+
has_context: phaseInfo?.has_context || false,
|
|
3939
|
+
has_plans: (phaseInfo?.plans?.length || 0) > 0,
|
|
3940
|
+
has_verification: phaseInfo?.has_verification || false,
|
|
3941
|
+
plan_count: phaseInfo?.plans?.length || 0,
|
|
3942
|
+
|
|
3943
|
+
// File existence
|
|
3944
|
+
roadmap_exists: pathExistsInternal(cwd, '.blueprint/ROADMAP.md'),
|
|
3945
|
+
planning_exists: pathExistsInternal(cwd, '.blueprint'),
|
|
3946
|
+
};
|
|
3947
|
+
|
|
3948
|
+
output(result, raw);
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
function cmdInitTodos(cwd, area, raw) {
|
|
3952
|
+
const config = loadConfig(cwd);
|
|
3953
|
+
const now = new Date();
|
|
3954
|
+
|
|
3955
|
+
// List todos (reuse existing logic)
|
|
3956
|
+
const pendingDir = path.join(cwd, '.blueprint', 'todos', 'pending');
|
|
3957
|
+
let count = 0;
|
|
3958
|
+
const todos = [];
|
|
3959
|
+
|
|
3960
|
+
try {
|
|
3961
|
+
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
|
|
3962
|
+
for (const file of files) {
|
|
3963
|
+
try {
|
|
3964
|
+
const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
|
|
3965
|
+
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
|
3966
|
+
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
3967
|
+
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
|
3968
|
+
const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
|
|
3969
|
+
|
|
3970
|
+
if (area && todoArea !== area) continue;
|
|
3971
|
+
|
|
3972
|
+
count++;
|
|
3973
|
+
todos.push({
|
|
3974
|
+
file,
|
|
3975
|
+
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
3976
|
+
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
3977
|
+
area: todoArea,
|
|
3978
|
+
path: path.join('.blueprint', 'todos', 'pending', file),
|
|
3979
|
+
});
|
|
3980
|
+
} catch {}
|
|
3981
|
+
}
|
|
3982
|
+
} catch {}
|
|
3983
|
+
|
|
3984
|
+
const result = {
|
|
3985
|
+
// Config
|
|
3986
|
+
commit_docs: config.commit_docs,
|
|
3987
|
+
|
|
3988
|
+
// Timestamps
|
|
3989
|
+
date: now.toISOString().split('T')[0],
|
|
3990
|
+
timestamp: now.toISOString(),
|
|
3991
|
+
|
|
3992
|
+
// Todo inventory
|
|
3993
|
+
todo_count: count,
|
|
3994
|
+
todos,
|
|
3995
|
+
area_filter: area || null,
|
|
3996
|
+
|
|
3997
|
+
// Paths
|
|
3998
|
+
pending_dir: '.blueprint/todos/pending',
|
|
3999
|
+
completed_dir: '.blueprint/todos/completed',
|
|
4000
|
+
|
|
4001
|
+
// File existence
|
|
4002
|
+
planning_exists: pathExistsInternal(cwd, '.blueprint'),
|
|
4003
|
+
todos_dir_exists: pathExistsInternal(cwd, '.blueprint/todos'),
|
|
4004
|
+
pending_dir_exists: pathExistsInternal(cwd, '.blueprint/todos/pending'),
|
|
4005
|
+
};
|
|
4006
|
+
|
|
4007
|
+
output(result, raw);
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
function cmdInitMilestoneOp(cwd, raw) {
|
|
4011
|
+
const config = loadConfig(cwd);
|
|
4012
|
+
const milestone = getMilestoneInfo(cwd);
|
|
4013
|
+
|
|
4014
|
+
// Count phases
|
|
4015
|
+
let phaseCount = 0;
|
|
4016
|
+
let completedPhases = 0;
|
|
4017
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
4018
|
+
try {
|
|
4019
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
4020
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
4021
|
+
phaseCount = dirs.length;
|
|
4022
|
+
|
|
4023
|
+
// Count phases with summaries (completed)
|
|
4024
|
+
for (const dir of dirs) {
|
|
4025
|
+
try {
|
|
4026
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
4027
|
+
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
4028
|
+
if (hasSummary) completedPhases++;
|
|
4029
|
+
} catch {}
|
|
4030
|
+
}
|
|
4031
|
+
} catch {}
|
|
4032
|
+
|
|
4033
|
+
// Check archive
|
|
4034
|
+
const archiveDir = path.join(cwd, '.blueprint', 'archive');
|
|
4035
|
+
let archivedMilestones = [];
|
|
4036
|
+
try {
|
|
4037
|
+
archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
|
|
4038
|
+
.filter(e => e.isDirectory())
|
|
4039
|
+
.map(e => e.name);
|
|
4040
|
+
} catch {}
|
|
4041
|
+
|
|
4042
|
+
const result = {
|
|
4043
|
+
// Config
|
|
4044
|
+
commit_docs: config.commit_docs,
|
|
4045
|
+
|
|
4046
|
+
// Current milestone
|
|
4047
|
+
milestone_version: milestone.version,
|
|
4048
|
+
milestone_name: milestone.name,
|
|
4049
|
+
milestone_slug: generateSlugInternal(milestone.name),
|
|
4050
|
+
|
|
4051
|
+
// Phase counts
|
|
4052
|
+
phase_count: phaseCount,
|
|
4053
|
+
completed_phases: completedPhases,
|
|
4054
|
+
all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
|
|
4055
|
+
|
|
4056
|
+
// Archive
|
|
4057
|
+
archived_milestones: archivedMilestones,
|
|
4058
|
+
archive_count: archivedMilestones.length,
|
|
4059
|
+
|
|
4060
|
+
// File existence
|
|
4061
|
+
project_exists: pathExistsInternal(cwd, '.blueprint/PROJECT.md'),
|
|
4062
|
+
roadmap_exists: pathExistsInternal(cwd, '.blueprint/ROADMAP.md'),
|
|
4063
|
+
state_exists: pathExistsInternal(cwd, '.blueprint/STATE.md'),
|
|
4064
|
+
archive_exists: pathExistsInternal(cwd, '.blueprint/archive'),
|
|
4065
|
+
phases_dir_exists: pathExistsInternal(cwd, '.blueprint/phases'),
|
|
4066
|
+
};
|
|
4067
|
+
|
|
4068
|
+
output(result, raw);
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
function cmdInitMapCodebase(cwd, raw) {
|
|
4072
|
+
const config = loadConfig(cwd);
|
|
4073
|
+
|
|
4074
|
+
// Check for existing codebase maps
|
|
4075
|
+
const codebaseDir = path.join(cwd, '.blueprint', 'codebase');
|
|
4076
|
+
let existingMaps = [];
|
|
4077
|
+
try {
|
|
4078
|
+
existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
|
|
4079
|
+
} catch {}
|
|
4080
|
+
|
|
4081
|
+
const result = {
|
|
4082
|
+
// Models
|
|
4083
|
+
mapper_model: resolveModelInternal(cwd, 'bp-codebase-mapper'),
|
|
4084
|
+
|
|
4085
|
+
// Config
|
|
4086
|
+
commit_docs: config.commit_docs,
|
|
4087
|
+
search_gitignored: config.search_gitignored,
|
|
4088
|
+
parallelization: config.parallelization,
|
|
4089
|
+
|
|
4090
|
+
// Paths
|
|
4091
|
+
codebase_dir: '.blueprint/codebase',
|
|
4092
|
+
|
|
4093
|
+
// Existing maps
|
|
4094
|
+
existing_maps: existingMaps,
|
|
4095
|
+
has_maps: existingMaps.length > 0,
|
|
4096
|
+
|
|
4097
|
+
// File existence
|
|
4098
|
+
planning_exists: pathExistsInternal(cwd, '.blueprint'),
|
|
4099
|
+
codebase_dir_exists: pathExistsInternal(cwd, '.blueprint/codebase'),
|
|
4100
|
+
};
|
|
4101
|
+
|
|
4102
|
+
output(result, raw);
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
function cmdInitProgress(cwd, includes, raw) {
|
|
4106
|
+
const config = loadConfig(cwd);
|
|
4107
|
+
const milestone = getMilestoneInfo(cwd);
|
|
4108
|
+
|
|
4109
|
+
// Analyze phases
|
|
4110
|
+
const phasesDir = path.join(cwd, '.blueprint', 'phases');
|
|
4111
|
+
const phases = [];
|
|
4112
|
+
let currentPhase = null;
|
|
4113
|
+
let nextPhase = null;
|
|
4114
|
+
|
|
4115
|
+
try {
|
|
4116
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
4117
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
4118
|
+
|
|
4119
|
+
for (const dir of dirs) {
|
|
4120
|
+
const match = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
|
|
4121
|
+
const phaseNumber = match ? match[1] : dir;
|
|
4122
|
+
const phaseName = match && match[2] ? match[2] : null;
|
|
4123
|
+
|
|
4124
|
+
const phasePath = path.join(phasesDir, dir);
|
|
4125
|
+
const phaseFiles = fs.readdirSync(phasePath);
|
|
4126
|
+
|
|
4127
|
+
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
4128
|
+
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
4129
|
+
const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
4130
|
+
|
|
4131
|
+
const status = summaries.length >= plans.length && plans.length > 0 ? 'complete' :
|
|
4132
|
+
plans.length > 0 ? 'in_progress' :
|
|
4133
|
+
hasResearch ? 'researched' : 'pending';
|
|
4134
|
+
|
|
4135
|
+
const phaseInfo = {
|
|
4136
|
+
number: phaseNumber,
|
|
4137
|
+
name: phaseName,
|
|
4138
|
+
directory: path.join('.blueprint', 'phases', dir),
|
|
4139
|
+
status,
|
|
4140
|
+
plan_count: plans.length,
|
|
4141
|
+
summary_count: summaries.length,
|
|
4142
|
+
has_research: hasResearch,
|
|
4143
|
+
};
|
|
4144
|
+
|
|
4145
|
+
phases.push(phaseInfo);
|
|
4146
|
+
|
|
4147
|
+
// Find current (first incomplete with plans) and next (first pending)
|
|
4148
|
+
if (!currentPhase && (status === 'in_progress' || status === 'researched')) {
|
|
4149
|
+
currentPhase = phaseInfo;
|
|
4150
|
+
}
|
|
4151
|
+
if (!nextPhase && status === 'pending') {
|
|
4152
|
+
nextPhase = phaseInfo;
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
} catch {}
|
|
4156
|
+
|
|
4157
|
+
// Check for paused work
|
|
4158
|
+
let pausedAt = null;
|
|
4159
|
+
try {
|
|
4160
|
+
const state = fs.readFileSync(path.join(cwd, '.blueprint', 'STATE.md'), 'utf-8');
|
|
4161
|
+
const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
|
|
4162
|
+
if (pauseMatch) pausedAt = pauseMatch[1].trim();
|
|
4163
|
+
} catch {}
|
|
4164
|
+
|
|
4165
|
+
const result = {
|
|
4166
|
+
// Models
|
|
4167
|
+
executor_model: resolveModelInternal(cwd, 'bp-executor'),
|
|
4168
|
+
planner_model: resolveModelInternal(cwd, 'bp-planner'),
|
|
4169
|
+
|
|
4170
|
+
// Config
|
|
4171
|
+
commit_docs: config.commit_docs,
|
|
4172
|
+
|
|
4173
|
+
// Milestone
|
|
4174
|
+
milestone_version: milestone.version,
|
|
4175
|
+
milestone_name: milestone.name,
|
|
4176
|
+
|
|
4177
|
+
// Phase overview
|
|
4178
|
+
phases,
|
|
4179
|
+
phase_count: phases.length,
|
|
4180
|
+
completed_count: phases.filter(p => p.status === 'complete').length,
|
|
4181
|
+
in_progress_count: phases.filter(p => p.status === 'in_progress').length,
|
|
4182
|
+
|
|
4183
|
+
// Current state
|
|
4184
|
+
current_phase: currentPhase,
|
|
4185
|
+
next_phase: nextPhase,
|
|
4186
|
+
paused_at: pausedAt,
|
|
4187
|
+
has_work_in_progress: !!currentPhase,
|
|
4188
|
+
|
|
4189
|
+
// File existence
|
|
4190
|
+
project_exists: pathExistsInternal(cwd, '.blueprint/PROJECT.md'),
|
|
4191
|
+
roadmap_exists: pathExistsInternal(cwd, '.blueprint/ROADMAP.md'),
|
|
4192
|
+
state_exists: pathExistsInternal(cwd, '.blueprint/STATE.md'),
|
|
4193
|
+
};
|
|
4194
|
+
|
|
4195
|
+
// Include file contents if requested via --include
|
|
4196
|
+
if (includes.has('state')) {
|
|
4197
|
+
result.state_content = safeReadFile(path.join(cwd, '.blueprint', 'STATE.md'));
|
|
4198
|
+
}
|
|
4199
|
+
if (includes.has('roadmap')) {
|
|
4200
|
+
result.roadmap_content = safeReadFile(path.join(cwd, '.blueprint', 'ROADMAP.md'));
|
|
4201
|
+
}
|
|
4202
|
+
if (includes.has('project')) {
|
|
4203
|
+
result.project_content = safeReadFile(path.join(cwd, '.blueprint', 'PROJECT.md'));
|
|
4204
|
+
}
|
|
4205
|
+
if (includes.has('config')) {
|
|
4206
|
+
result.config_content = safeReadFile(path.join(cwd, '.blueprint', 'config.json'));
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
output(result, raw);
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
// ─── CLI Router ───────────────────────────────────────────────────────────────
|
|
4213
|
+
|
|
4214
|
+
async function main() {
|
|
4215
|
+
const args = process.argv.slice(2);
|
|
4216
|
+
const rawIndex = args.indexOf('--raw');
|
|
4217
|
+
const raw = rawIndex !== -1;
|
|
4218
|
+
if (rawIndex !== -1) args.splice(rawIndex, 1);
|
|
4219
|
+
|
|
4220
|
+
const command = args[0];
|
|
4221
|
+
const cwd = process.cwd();
|
|
4222
|
+
|
|
4223
|
+
if (!command) {
|
|
4224
|
+
error('Usage: blueprint-tools <command> [args] [--raw]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init');
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
switch (command) {
|
|
4228
|
+
case 'state': {
|
|
4229
|
+
const subcommand = args[1];
|
|
4230
|
+
if (subcommand === 'update') {
|
|
4231
|
+
cmdStateUpdate(cwd, args[2], args[3]);
|
|
4232
|
+
} else if (subcommand === 'get') {
|
|
4233
|
+
cmdStateGet(cwd, args[2], raw);
|
|
4234
|
+
} else if (subcommand === 'patch') {
|
|
4235
|
+
const patches = {};
|
|
4236
|
+
for (let i = 2; i < args.length; i += 2) {
|
|
4237
|
+
const key = args[i].replace(/^--/, '');
|
|
4238
|
+
const value = args[i + 1];
|
|
4239
|
+
if (key && value !== undefined) {
|
|
4240
|
+
patches[key] = value;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
cmdStatePatch(cwd, patches, raw);
|
|
4244
|
+
} else if (subcommand === 'advance-plan') {
|
|
4245
|
+
cmdStateAdvancePlan(cwd, raw);
|
|
4246
|
+
} else if (subcommand === 'record-metric') {
|
|
4247
|
+
const phaseIdx = args.indexOf('--phase');
|
|
4248
|
+
const planIdx = args.indexOf('--plan');
|
|
4249
|
+
const durationIdx = args.indexOf('--duration');
|
|
4250
|
+
const tasksIdx = args.indexOf('--tasks');
|
|
4251
|
+
const filesIdx = args.indexOf('--files');
|
|
4252
|
+
cmdStateRecordMetric(cwd, {
|
|
4253
|
+
phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null,
|
|
4254
|
+
plan: planIdx !== -1 ? args[planIdx + 1] : null,
|
|
4255
|
+
duration: durationIdx !== -1 ? args[durationIdx + 1] : null,
|
|
4256
|
+
tasks: tasksIdx !== -1 ? args[tasksIdx + 1] : null,
|
|
4257
|
+
files: filesIdx !== -1 ? args[filesIdx + 1] : null,
|
|
4258
|
+
}, raw);
|
|
4259
|
+
} else if (subcommand === 'update-progress') {
|
|
4260
|
+
cmdStateUpdateProgress(cwd, raw);
|
|
4261
|
+
} else if (subcommand === 'add-decision') {
|
|
4262
|
+
const phaseIdx = args.indexOf('--phase');
|
|
4263
|
+
const summaryIdx = args.indexOf('--summary');
|
|
4264
|
+
const rationaleIdx = args.indexOf('--rationale');
|
|
4265
|
+
cmdStateAddDecision(cwd, {
|
|
4266
|
+
phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null,
|
|
4267
|
+
summary: summaryIdx !== -1 ? args[summaryIdx + 1] : null,
|
|
4268
|
+
rationale: rationaleIdx !== -1 ? args[rationaleIdx + 1] : '',
|
|
4269
|
+
}, raw);
|
|
4270
|
+
} else if (subcommand === 'add-blocker') {
|
|
4271
|
+
const textIdx = args.indexOf('--text');
|
|
4272
|
+
cmdStateAddBlocker(cwd, textIdx !== -1 ? args[textIdx + 1] : null, raw);
|
|
4273
|
+
} else if (subcommand === 'resolve-blocker') {
|
|
4274
|
+
const textIdx = args.indexOf('--text');
|
|
4275
|
+
cmdStateResolveBlocker(cwd, textIdx !== -1 ? args[textIdx + 1] : null, raw);
|
|
4276
|
+
} else if (subcommand === 'record-session') {
|
|
4277
|
+
const stoppedIdx = args.indexOf('--stopped-at');
|
|
4278
|
+
const resumeIdx = args.indexOf('--resume-file');
|
|
4279
|
+
cmdStateRecordSession(cwd, {
|
|
4280
|
+
stopped_at: stoppedIdx !== -1 ? args[stoppedIdx + 1] : null,
|
|
4281
|
+
resume_file: resumeIdx !== -1 ? args[resumeIdx + 1] : 'None',
|
|
4282
|
+
}, raw);
|
|
4283
|
+
} else {
|
|
4284
|
+
cmdStateLoad(cwd, raw);
|
|
4285
|
+
}
|
|
4286
|
+
break;
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
case 'resolve-model': {
|
|
4290
|
+
cmdResolveModel(cwd, args[1], raw);
|
|
4291
|
+
break;
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
case 'find-phase': {
|
|
4295
|
+
cmdFindPhase(cwd, args[1], raw);
|
|
4296
|
+
break;
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
case 'commit': {
|
|
4300
|
+
const amend = args.includes('--amend');
|
|
4301
|
+
const message = args[1];
|
|
4302
|
+
// Parse --files flag (collect args after --files, stopping at other flags)
|
|
4303
|
+
const filesIndex = args.indexOf('--files');
|
|
4304
|
+
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
|
|
4305
|
+
cmdCommit(cwd, message, files, raw, amend);
|
|
4306
|
+
break;
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
case 'verify-summary': {
|
|
4310
|
+
const summaryPath = args[1];
|
|
4311
|
+
const countIndex = args.indexOf('--check-count');
|
|
4312
|
+
const checkCount = countIndex !== -1 ? parseInt(args[countIndex + 1], 10) : 2;
|
|
4313
|
+
cmdVerifySummary(cwd, summaryPath, checkCount, raw);
|
|
4314
|
+
break;
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
case 'template': {
|
|
4318
|
+
const subcommand = args[1];
|
|
4319
|
+
if (subcommand === 'select') {
|
|
4320
|
+
cmdTemplateSelect(cwd, args[2], raw);
|
|
4321
|
+
} else if (subcommand === 'fill') {
|
|
4322
|
+
const templateType = args[2];
|
|
4323
|
+
const phaseIdx = args.indexOf('--phase');
|
|
4324
|
+
const planIdx = args.indexOf('--plan');
|
|
4325
|
+
const nameIdx = args.indexOf('--name');
|
|
4326
|
+
const typeIdx = args.indexOf('--type');
|
|
4327
|
+
const waveIdx = args.indexOf('--wave');
|
|
4328
|
+
const fieldsIdx = args.indexOf('--fields');
|
|
4329
|
+
cmdTemplateFill(cwd, templateType, {
|
|
4330
|
+
phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null,
|
|
4331
|
+
plan: planIdx !== -1 ? args[planIdx + 1] : null,
|
|
4332
|
+
name: nameIdx !== -1 ? args[nameIdx + 1] : null,
|
|
4333
|
+
type: typeIdx !== -1 ? args[typeIdx + 1] : 'execute',
|
|
4334
|
+
wave: waveIdx !== -1 ? args[waveIdx + 1] : '1',
|
|
4335
|
+
fields: fieldsIdx !== -1 ? JSON.parse(args[fieldsIdx + 1]) : {},
|
|
4336
|
+
}, raw);
|
|
4337
|
+
} else {
|
|
4338
|
+
error('Unknown template subcommand. Available: select, fill');
|
|
4339
|
+
}
|
|
4340
|
+
break;
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
case 'frontmatter': {
|
|
4344
|
+
const subcommand = args[1];
|
|
4345
|
+
const file = args[2];
|
|
4346
|
+
if (subcommand === 'get') {
|
|
4347
|
+
const fieldIdx = args.indexOf('--field');
|
|
4348
|
+
cmdFrontmatterGet(cwd, file, fieldIdx !== -1 ? args[fieldIdx + 1] : null, raw);
|
|
4349
|
+
} else if (subcommand === 'set') {
|
|
4350
|
+
const fieldIdx = args.indexOf('--field');
|
|
4351
|
+
const valueIdx = args.indexOf('--value');
|
|
4352
|
+
cmdFrontmatterSet(cwd, file, fieldIdx !== -1 ? args[fieldIdx + 1] : null, valueIdx !== -1 ? args[valueIdx + 1] : undefined, raw);
|
|
4353
|
+
} else if (subcommand === 'merge') {
|
|
4354
|
+
const dataIdx = args.indexOf('--data');
|
|
4355
|
+
cmdFrontmatterMerge(cwd, file, dataIdx !== -1 ? args[dataIdx + 1] : null, raw);
|
|
4356
|
+
} else if (subcommand === 'validate') {
|
|
4357
|
+
const schemaIdx = args.indexOf('--schema');
|
|
4358
|
+
cmdFrontmatterValidate(cwd, file, schemaIdx !== -1 ? args[schemaIdx + 1] : null, raw);
|
|
4359
|
+
} else {
|
|
4360
|
+
error('Unknown frontmatter subcommand. Available: get, set, merge, validate');
|
|
4361
|
+
}
|
|
4362
|
+
break;
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
case 'verify': {
|
|
4366
|
+
const subcommand = args[1];
|
|
4367
|
+
if (subcommand === 'plan-structure') {
|
|
4368
|
+
cmdVerifyPlanStructure(cwd, args[2], raw);
|
|
4369
|
+
} else if (subcommand === 'phase-completeness') {
|
|
4370
|
+
cmdVerifyPhaseCompleteness(cwd, args[2], raw);
|
|
4371
|
+
} else if (subcommand === 'references') {
|
|
4372
|
+
cmdVerifyReferences(cwd, args[2], raw);
|
|
4373
|
+
} else if (subcommand === 'commits') {
|
|
4374
|
+
cmdVerifyCommits(cwd, args.slice(2), raw);
|
|
4375
|
+
} else if (subcommand === 'artifacts') {
|
|
4376
|
+
cmdVerifyArtifacts(cwd, args[2], raw);
|
|
4377
|
+
} else if (subcommand === 'key-links') {
|
|
4378
|
+
cmdVerifyKeyLinks(cwd, args[2], raw);
|
|
4379
|
+
} else {
|
|
4380
|
+
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links');
|
|
4381
|
+
}
|
|
4382
|
+
break;
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
case 'generate-slug': {
|
|
4386
|
+
cmdGenerateSlug(args[1], raw);
|
|
4387
|
+
break;
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
case 'current-timestamp': {
|
|
4391
|
+
cmdCurrentTimestamp(args[1] || 'full', raw);
|
|
4392
|
+
break;
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
case 'list-todos': {
|
|
4396
|
+
cmdListTodos(cwd, args[1], raw);
|
|
4397
|
+
break;
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
case 'verify-path-exists': {
|
|
4401
|
+
cmdVerifyPathExists(cwd, args[1], raw);
|
|
4402
|
+
break;
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
case 'config-ensure-section': {
|
|
4406
|
+
cmdConfigEnsureSection(cwd, raw);
|
|
4407
|
+
break;
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
case 'config-set': {
|
|
4411
|
+
cmdConfigSet(cwd, args[1], args[2], raw);
|
|
4412
|
+
break;
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
case 'history-digest': {
|
|
4416
|
+
cmdHistoryDigest(cwd, raw);
|
|
4417
|
+
break;
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
case 'phases': {
|
|
4421
|
+
const subcommand = args[1];
|
|
4422
|
+
if (subcommand === 'list') {
|
|
4423
|
+
const typeIndex = args.indexOf('--type');
|
|
4424
|
+
const phaseIndex = args.indexOf('--phase');
|
|
4425
|
+
const options = {
|
|
4426
|
+
type: typeIndex !== -1 ? args[typeIndex + 1] : null,
|
|
4427
|
+
phase: phaseIndex !== -1 ? args[phaseIndex + 1] : null,
|
|
4428
|
+
};
|
|
4429
|
+
cmdPhasesList(cwd, options, raw);
|
|
4430
|
+
} else {
|
|
4431
|
+
error('Unknown phases subcommand. Available: list');
|
|
4432
|
+
}
|
|
4433
|
+
break;
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
case 'roadmap': {
|
|
4437
|
+
const subcommand = args[1];
|
|
4438
|
+
if (subcommand === 'get-phase') {
|
|
4439
|
+
cmdRoadmapGetPhase(cwd, args[2], raw);
|
|
4440
|
+
} else if (subcommand === 'analyze') {
|
|
4441
|
+
cmdRoadmapAnalyze(cwd, raw);
|
|
4442
|
+
} else {
|
|
4443
|
+
error('Unknown roadmap subcommand. Available: get-phase, analyze');
|
|
4444
|
+
}
|
|
4445
|
+
break;
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
case 'phase': {
|
|
4449
|
+
const subcommand = args[1];
|
|
4450
|
+
if (subcommand === 'next-decimal') {
|
|
4451
|
+
cmdPhaseNextDecimal(cwd, args[2], raw);
|
|
4452
|
+
} else if (subcommand === 'add') {
|
|
4453
|
+
cmdPhaseAdd(cwd, args.slice(2).join(' '), raw);
|
|
4454
|
+
} else if (subcommand === 'insert') {
|
|
4455
|
+
cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw);
|
|
4456
|
+
} else if (subcommand === 'remove') {
|
|
4457
|
+
const forceFlag = args.includes('--force');
|
|
4458
|
+
cmdPhaseRemove(cwd, args[2], { force: forceFlag }, raw);
|
|
4459
|
+
} else if (subcommand === 'complete') {
|
|
4460
|
+
cmdPhaseComplete(cwd, args[2], raw);
|
|
4461
|
+
} else {
|
|
4462
|
+
error('Unknown phase subcommand. Available: next-decimal, add, insert, remove, complete');
|
|
4463
|
+
}
|
|
4464
|
+
break;
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
case 'milestone': {
|
|
4468
|
+
const subcommand = args[1];
|
|
4469
|
+
if (subcommand === 'complete') {
|
|
4470
|
+
const nameIndex = args.indexOf('--name');
|
|
4471
|
+
const milestoneName = nameIndex !== -1 ? args.slice(nameIndex + 1).join(' ') : null;
|
|
4472
|
+
cmdMilestoneComplete(cwd, args[2], { name: milestoneName }, raw);
|
|
4473
|
+
} else {
|
|
4474
|
+
error('Unknown milestone subcommand. Available: complete');
|
|
4475
|
+
}
|
|
4476
|
+
break;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
case 'validate': {
|
|
4480
|
+
const subcommand = args[1];
|
|
4481
|
+
if (subcommand === 'consistency') {
|
|
4482
|
+
cmdValidateConsistency(cwd, raw);
|
|
4483
|
+
} else {
|
|
4484
|
+
error('Unknown validate subcommand. Available: consistency');
|
|
4485
|
+
}
|
|
4486
|
+
break;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
case 'progress': {
|
|
4490
|
+
const subcommand = args[1] || 'json';
|
|
4491
|
+
cmdProgressRender(cwd, subcommand, raw);
|
|
4492
|
+
break;
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
case 'todo': {
|
|
4496
|
+
const subcommand = args[1];
|
|
4497
|
+
if (subcommand === 'complete') {
|
|
4498
|
+
cmdTodoComplete(cwd, args[2], raw);
|
|
4499
|
+
} else {
|
|
4500
|
+
error('Unknown todo subcommand. Available: complete');
|
|
4501
|
+
}
|
|
4502
|
+
break;
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
case 'scaffold': {
|
|
4506
|
+
const scaffoldType = args[1];
|
|
4507
|
+
const phaseIndex = args.indexOf('--phase');
|
|
4508
|
+
const nameIndex = args.indexOf('--name');
|
|
4509
|
+
const scaffoldOptions = {
|
|
4510
|
+
phase: phaseIndex !== -1 ? args[phaseIndex + 1] : null,
|
|
4511
|
+
name: nameIndex !== -1 ? args.slice(nameIndex + 1).join(' ') : null,
|
|
4512
|
+
};
|
|
4513
|
+
cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
|
|
4514
|
+
break;
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
case 'init': {
|
|
4518
|
+
const workflow = args[1];
|
|
4519
|
+
const includes = parseIncludeFlag(args);
|
|
4520
|
+
switch (workflow) {
|
|
4521
|
+
case 'execute-phase':
|
|
4522
|
+
cmdInitExecutePhase(cwd, args[2], includes, raw);
|
|
4523
|
+
break;
|
|
4524
|
+
case 'plan-phase':
|
|
4525
|
+
cmdInitPlanPhase(cwd, args[2], includes, raw);
|
|
4526
|
+
break;
|
|
4527
|
+
case 'new-project':
|
|
4528
|
+
cmdInitNewProject(cwd, raw);
|
|
4529
|
+
break;
|
|
4530
|
+
case 'new-milestone':
|
|
4531
|
+
cmdInitNewMilestone(cwd, raw);
|
|
4532
|
+
break;
|
|
4533
|
+
case 'quick':
|
|
4534
|
+
cmdInitQuick(cwd, args.slice(2).join(' '), raw);
|
|
4535
|
+
break;
|
|
4536
|
+
case 'resume':
|
|
4537
|
+
cmdInitResume(cwd, raw);
|
|
4538
|
+
break;
|
|
4539
|
+
case 'verify-work':
|
|
4540
|
+
cmdInitVerifyWork(cwd, args[2], raw);
|
|
4541
|
+
break;
|
|
4542
|
+
case 'phase-op':
|
|
4543
|
+
cmdInitPhaseOp(cwd, args[2], raw);
|
|
4544
|
+
break;
|
|
4545
|
+
case 'todos':
|
|
4546
|
+
cmdInitTodos(cwd, args[2], raw);
|
|
4547
|
+
break;
|
|
4548
|
+
case 'milestone-op':
|
|
4549
|
+
cmdInitMilestoneOp(cwd, raw);
|
|
4550
|
+
break;
|
|
4551
|
+
case 'map-codebase':
|
|
4552
|
+
cmdInitMapCodebase(cwd, raw);
|
|
4553
|
+
break;
|
|
4554
|
+
case 'progress':
|
|
4555
|
+
cmdInitProgress(cwd, includes, raw);
|
|
4556
|
+
break;
|
|
4557
|
+
default:
|
|
4558
|
+
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`);
|
|
4559
|
+
}
|
|
4560
|
+
break;
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
case 'phase-plan-index': {
|
|
4564
|
+
cmdPhasePlanIndex(cwd, args[1], raw);
|
|
4565
|
+
break;
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
case 'state-snapshot': {
|
|
4569
|
+
cmdStateSnapshot(cwd, raw);
|
|
4570
|
+
break;
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4573
|
+
case 'summary-extract': {
|
|
4574
|
+
const summaryPath = args[1];
|
|
4575
|
+
const fieldsIndex = args.indexOf('--fields');
|
|
4576
|
+
const fields = fieldsIndex !== -1 ? args[fieldsIndex + 1].split(',') : null;
|
|
4577
|
+
cmdSummaryExtract(cwd, summaryPath, fields, raw);
|
|
4578
|
+
break;
|
|
4579
|
+
}
|
|
4580
|
+
|
|
4581
|
+
case 'websearch': {
|
|
4582
|
+
const query = args[1];
|
|
4583
|
+
const limitIdx = args.indexOf('--limit');
|
|
4584
|
+
const freshnessIdx = args.indexOf('--freshness');
|
|
4585
|
+
await cmdWebsearch(query, {
|
|
4586
|
+
limit: limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 10,
|
|
4587
|
+
freshness: freshnessIdx !== -1 ? args[freshnessIdx + 1] : null,
|
|
4588
|
+
}, raw);
|
|
4589
|
+
break;
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
default:
|
|
4593
|
+
error(`Unknown command: ${command}`);
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
main();
|