@ydtb/specsmd 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +322 -0
- package/bin/cli.js +21 -0
- package/flows/aidlc/README.md +372 -0
- package/flows/aidlc/agents/construction-agent.md +80 -0
- package/flows/aidlc/agents/inception-agent.md +97 -0
- package/flows/aidlc/agents/master-agent.md +61 -0
- package/flows/aidlc/agents/operations-agent.md +89 -0
- package/flows/aidlc/commands/construction-agent.md +67 -0
- package/flows/aidlc/commands/inception-agent.md +59 -0
- package/flows/aidlc/commands/master-agent.md +51 -0
- package/flows/aidlc/commands/operations-agent.md +81 -0
- package/flows/aidlc/context-config.yaml +67 -0
- package/flows/aidlc/memory-bank.yaml +105 -0
- package/flows/aidlc/quick-start.md +322 -0
- package/flows/aidlc/scripts/artifact-validator.cjs +594 -0
- package/flows/aidlc/scripts/bolt-complete.cjs +606 -0
- package/flows/aidlc/scripts/status-integrity.cjs +598 -0
- package/flows/aidlc/skills/construction/bolt-list.md +163 -0
- package/flows/aidlc/skills/construction/bolt-replan.md +345 -0
- package/flows/aidlc/skills/construction/bolt-start.md +442 -0
- package/flows/aidlc/skills/construction/bolt-status.md +185 -0
- package/flows/aidlc/skills/construction/navigator.md +196 -0
- package/flows/aidlc/skills/construction/prototype-apply.md +311 -0
- package/flows/aidlc/skills/inception/bolt-plan.md +372 -0
- package/flows/aidlc/skills/inception/context.md +171 -0
- package/flows/aidlc/skills/inception/intent-create.md +211 -0
- package/flows/aidlc/skills/inception/intent-list.md +124 -0
- package/flows/aidlc/skills/inception/navigator.md +207 -0
- package/flows/aidlc/skills/inception/requirements.md +227 -0
- package/flows/aidlc/skills/inception/review.md +248 -0
- package/flows/aidlc/skills/inception/story-create.md +304 -0
- package/flows/aidlc/skills/inception/units.md +278 -0
- package/flows/aidlc/skills/inception/vibe-to-spec.md +410 -0
- package/flows/aidlc/skills/master/analyze-context.md +239 -0
- package/flows/aidlc/skills/master/answer-question.md +141 -0
- package/flows/aidlc/skills/master/explain-flow.md +158 -0
- package/flows/aidlc/skills/master/project-init.md +281 -0
- package/flows/aidlc/skills/master/route-request.md +126 -0
- package/flows/aidlc/skills/operations/build.md +237 -0
- package/flows/aidlc/skills/operations/deploy.md +259 -0
- package/flows/aidlc/skills/operations/monitor.md +265 -0
- package/flows/aidlc/skills/operations/navigator.md +209 -0
- package/flows/aidlc/skills/operations/verify.md +224 -0
- package/flows/aidlc/templates/construction/bolt-template.md +226 -0
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt/adr-template.md +49 -0
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt/ddd-01-domain-model-template.md +55 -0
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt/ddd-02-technical-design-template.md +67 -0
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt/ddd-03-test-report-template.md +62 -0
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt.md +590 -0
- package/flows/aidlc/templates/construction/bolt-types/simple-construction-bolt.md +347 -0
- package/flows/aidlc/templates/construction/bolt-types/spike-bolt.md +240 -0
- package/flows/aidlc/templates/construction/construction-log-template.md +129 -0
- package/flows/aidlc/templates/construction/standards/coding-standards.md +29 -0
- package/flows/aidlc/templates/construction/standards/system-architecture.md +22 -0
- package/flows/aidlc/templates/construction/standards/tech-stack.md +19 -0
- package/flows/aidlc/templates/inception/inception-log-template.md +134 -0
- package/flows/aidlc/templates/inception/project/README.md +55 -0
- package/flows/aidlc/templates/inception/requirements-template.md +144 -0
- package/flows/aidlc/templates/inception/stories-template.md +38 -0
- package/flows/aidlc/templates/inception/story-template.md +147 -0
- package/flows/aidlc/templates/inception/system-context-template.md +29 -0
- package/flows/aidlc/templates/inception/unit-brief-template.md +177 -0
- package/flows/aidlc/templates/inception/units-template.md +52 -0
- package/flows/aidlc/templates/standards/catalog.yaml +345 -0
- package/flows/aidlc/templates/standards/coding-standards.guide.md +553 -0
- package/flows/aidlc/templates/standards/data-stack.guide.md +162 -0
- package/flows/aidlc/templates/standards/decision-index-template.md +32 -0
- package/flows/aidlc/templates/standards/tech-stack.guide.md +280 -0
- package/flows/fire/README.md +19 -0
- package/flows/fire/agents/builder/agent.md +254 -0
- package/flows/fire/agents/builder/skills/code-review/SKILL.md +257 -0
- package/flows/fire/agents/builder/skills/code-review/references/auto-fix-rules.md +218 -0
- package/flows/fire/agents/builder/skills/code-review/references/review-categories.md +154 -0
- package/flows/fire/agents/builder/skills/code-review/templates/review-report.md.hbs +120 -0
- package/flows/fire/agents/builder/skills/commit-changes/SKILL.md +232 -0
- package/flows/fire/agents/builder/skills/commit-changes/scripts/commit-changes.cjs +447 -0
- package/flows/fire/agents/builder/skills/run-execute/SKILL.md +700 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/complete-run.cjs +748 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/init-run.cjs +457 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/update-phase.cjs +239 -0
- package/flows/fire/agents/builder/skills/run-execute/templates/plan.md.hbs +61 -0
- package/flows/fire/agents/builder/skills/run-execute/templates/test-report.md.hbs +81 -0
- package/flows/fire/agents/builder/skills/run-plan/SKILL.md +366 -0
- package/flows/fire/agents/builder/skills/run-status/SKILL.md +96 -0
- package/flows/fire/agents/builder/skills/walkthrough-generate/SKILL.md +181 -0
- package/flows/fire/agents/builder/skills/walkthrough-generate/templates/walkthrough.md.hbs +108 -0
- package/flows/fire/agents/orchestrator/agent.md +144 -0
- package/flows/fire/agents/orchestrator/skills/project-init/SKILL.md +226 -0
- package/flows/fire/agents/orchestrator/skills/project-init/templates/coding-standards.md.hbs +149 -0
- package/flows/fire/agents/orchestrator/skills/project-init/templates/constitution.md.hbs +43 -0
- package/flows/fire/agents/orchestrator/skills/project-init/templates/system-architecture.md.hbs +101 -0
- package/flows/fire/agents/orchestrator/skills/project-init/templates/tech-stack.md.hbs +136 -0
- package/flows/fire/agents/orchestrator/skills/project-init/templates/testing-standards.md.hbs +94 -0
- package/flows/fire/agents/orchestrator/skills/route/SKILL.md +146 -0
- package/flows/fire/agents/orchestrator/skills/status/SKILL.md +696 -0
- package/flows/fire/agents/planner/agent.md +143 -0
- package/flows/fire/agents/planner/skills/design-doc-generate/SKILL.md +156 -0
- package/flows/fire/agents/planner/skills/design-doc-generate/templates/design.md.hbs +124 -0
- package/flows/fire/agents/planner/skills/intent-capture/SKILL.md +125 -0
- package/flows/fire/agents/planner/skills/intent-capture/templates/brief.md.hbs +40 -0
- package/flows/fire/agents/planner/skills/work-item-decompose/SKILL.md +166 -0
- package/flows/fire/agents/planner/skills/work-item-decompose/templates/work-item.md.hbs +40 -0
- package/flows/fire/commands/fire-builder.md +56 -0
- package/flows/fire/commands/fire-planner.md +48 -0
- package/flows/fire/commands/fire.md +46 -0
- package/flows/fire/memory-bank.yaml +240 -0
- package/flows/fire/quick-start.md +146 -0
- package/flows/simple/README.md +190 -0
- package/flows/simple/agents/agent.md +404 -0
- package/flows/simple/commands/agent.md +60 -0
- package/flows/simple/context-config.yaml +34 -0
- package/flows/simple/memory-bank.yaml +66 -0
- package/flows/simple/quick-start.md +231 -0
- package/flows/simple/skills/design.md +96 -0
- package/flows/simple/skills/execute.md +190 -0
- package/flows/simple/skills/requirements.md +94 -0
- package/flows/simple/skills/tasks.md +136 -0
- package/flows/simple/templates/design-template.md +138 -0
- package/flows/simple/templates/requirements-template.md +85 -0
- package/flows/simple/templates/tasks-template.md +104 -0
- package/lib/InstallerFactory.js +36 -0
- package/lib/analytics/env-detector.js +92 -0
- package/lib/analytics/index.js +22 -0
- package/lib/analytics/machine-id.js +33 -0
- package/lib/analytics/tracker.js +232 -0
- package/lib/cli-utils.js +342 -0
- package/lib/constants.js +44 -0
- package/lib/installer.js +406 -0
- package/lib/installers/AntigravityInstaller.js +22 -0
- package/lib/installers/ClaudeInstaller.js +85 -0
- package/lib/installers/ClineInstaller.js +21 -0
- package/lib/installers/CodexInstaller.js +21 -0
- package/lib/installers/CopilotInstaller.js +113 -0
- package/lib/installers/CursorInstaller.js +63 -0
- package/lib/installers/GeminiInstaller.js +75 -0
- package/lib/installers/KiroInstaller.js +77 -0
- package/lib/installers/OpenCodeInstaller.js +30 -0
- package/lib/installers/RooInstaller.js +22 -0
- package/lib/installers/ToolInstaller.js +76 -0
- package/lib/installers/WindsurfInstaller.js +22 -0
- package/lib/markdown-validator.ts +175 -0
- package/lib/yaml-validator.ts +99 -0
- package/package.json +69 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FIRE Run Completion Script
|
|
5
|
+
*
|
|
6
|
+
* Supports both single and batch/wide runs.
|
|
7
|
+
*
|
|
8
|
+
* For single runs: Completes the run and removes from runs.active[].
|
|
9
|
+
* For batch/wide runs:
|
|
10
|
+
* - --complete-item: Marks current work item done, moves to next
|
|
11
|
+
* - --complete-run: Marks all items done and finalizes entire run
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* Complete current item: node complete-run.cjs <rootPath> <runId> --complete-item [options]
|
|
15
|
+
* Complete entire run: node complete-run.cjs <rootPath> <runId> --complete-run [options]
|
|
16
|
+
* Complete (single/auto): node complete-run.cjs <rootPath> <runId> [options]
|
|
17
|
+
*
|
|
18
|
+
* Options:
|
|
19
|
+
* --files-created=JSON - JSON array of {path, purpose}
|
|
20
|
+
* --files-modified=JSON - JSON array of {path, changes}
|
|
21
|
+
* --decisions=JSON - JSON array of {decision, choice, rationale}
|
|
22
|
+
* --tests=N - Number of tests added
|
|
23
|
+
* --coverage=N - Coverage percentage
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const yaml = require('yaml');
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Error Helper
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
function fireError(message, code, suggestion) {
|
|
35
|
+
const err = new Error(`FIRE Error [${code}]: ${message} ${suggestion}`);
|
|
36
|
+
err.code = code;
|
|
37
|
+
err.suggestion = suggestion;
|
|
38
|
+
return err;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Validation
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
function validateInputs(rootPath, runId) {
|
|
46
|
+
if (!rootPath || typeof rootPath !== 'string' || rootPath.trim() === '') {
|
|
47
|
+
throw fireError('rootPath is required.', 'COMPLETE_001', 'Provide a valid project root path.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!runId || typeof runId !== 'string' || runId.trim() === '') {
|
|
51
|
+
throw fireError('runId is required.', 'COMPLETE_002', 'Provide the run ID to complete.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(rootPath)) {
|
|
55
|
+
throw fireError(
|
|
56
|
+
`Project root not found: "${rootPath}".`,
|
|
57
|
+
'COMPLETE_003',
|
|
58
|
+
'Ensure the path exists and is accessible.'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateFireProject(rootPath, runId) {
|
|
64
|
+
const fireDir = path.join(rootPath, '.specs-fire');
|
|
65
|
+
const statePath = path.join(fireDir, 'state.yaml');
|
|
66
|
+
const runsPath = path.join(fireDir, 'runs');
|
|
67
|
+
const runPath = path.join(runsPath, runId);
|
|
68
|
+
const runLogPath = path.join(runPath, 'run.md');
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(fireDir)) {
|
|
71
|
+
throw fireError(
|
|
72
|
+
`FIRE project not initialized at: "${rootPath}".`,
|
|
73
|
+
'COMPLETE_010',
|
|
74
|
+
'Run fire-init first to initialize the project.'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(statePath)) {
|
|
79
|
+
throw fireError(
|
|
80
|
+
`State file not found at: "${statePath}".`,
|
|
81
|
+
'COMPLETE_011',
|
|
82
|
+
'The project may be corrupted. Try re-initializing.'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(runPath)) {
|
|
87
|
+
throw fireError(
|
|
88
|
+
`Run folder not found: "${runPath}".`,
|
|
89
|
+
'COMPLETE_012',
|
|
90
|
+
`Ensure run "${runId}" was properly initialized.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!fs.existsSync(runLogPath)) {
|
|
95
|
+
throw fireError(
|
|
96
|
+
`Run log not found: "${runLogPath}".`,
|
|
97
|
+
'COMPLETE_013',
|
|
98
|
+
`The run may have been partially initialized.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { statePath, runPath, runLogPath };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// =============================================================================
|
|
106
|
+
// Frontmatter Helpers
|
|
107
|
+
// =============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse YAML frontmatter from markdown content.
|
|
111
|
+
* Returns { frontmatter: object, body: string } or null if no frontmatter.
|
|
112
|
+
*/
|
|
113
|
+
function parseFrontmatter(content) {
|
|
114
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
115
|
+
if (!match) return null;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const frontmatter = yaml.parse(match[1]);
|
|
119
|
+
const body = content.slice(match[0].length);
|
|
120
|
+
return { frontmatter, body };
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Reconstruct markdown content from frontmatter and body.
|
|
128
|
+
*/
|
|
129
|
+
function buildMarkdownWithFrontmatter(frontmatter, body) {
|
|
130
|
+
return `---\n${yaml.stringify(frontmatter)}---${body}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// =============================================================================
|
|
134
|
+
// Markdown Frontmatter Sync
|
|
135
|
+
// =============================================================================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Update work item markdown file frontmatter with new status.
|
|
139
|
+
*/
|
|
140
|
+
function updateWorkItemMarkdown(rootPath, intentId, workItemId, status, runId, completedAt) {
|
|
141
|
+
const filePath = path.join(rootPath, '.specs-fire', 'intents', intentId, 'work-items', `${workItemId}.md`);
|
|
142
|
+
|
|
143
|
+
if (!fs.existsSync(filePath)) {
|
|
144
|
+
// File doesn't exist - not an error, just skip
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
150
|
+
const parsed = parseFrontmatter(content);
|
|
151
|
+
|
|
152
|
+
if (!parsed) {
|
|
153
|
+
// No valid frontmatter - skip
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update frontmatter fields
|
|
158
|
+
parsed.frontmatter.status = status;
|
|
159
|
+
if (runId) parsed.frontmatter.run_id = runId;
|
|
160
|
+
if (completedAt && status === 'completed') {
|
|
161
|
+
parsed.frontmatter.completed_at = completedAt;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const newContent = buildMarkdownWithFrontmatter(parsed.frontmatter, parsed.body);
|
|
165
|
+
fs.writeFileSync(filePath, newContent);
|
|
166
|
+
return true;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
// Log but don't fail - markdown sync is best-effort
|
|
169
|
+
console.error(`Warning: Could not update work item markdown ${filePath}: ${err.message}`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Update intent brief.md frontmatter based on work item statuses.
|
|
176
|
+
*/
|
|
177
|
+
function updateIntentMarkdown(rootPath, intentId, state) {
|
|
178
|
+
const filePath = path.join(rootPath, '.specs-fire', 'intents', intentId, 'brief.md');
|
|
179
|
+
|
|
180
|
+
if (!fs.existsSync(filePath)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// Determine intent status from its work items
|
|
186
|
+
const intent = state.intents?.find(i => i.id === intentId);
|
|
187
|
+
if (!intent || !Array.isArray(intent.work_items)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const allCompleted = intent.work_items.every(wi => wi.status === 'completed');
|
|
192
|
+
const anyInProgress = intent.work_items.some(wi => wi.status === 'in_progress');
|
|
193
|
+
|
|
194
|
+
let newStatus = 'pending';
|
|
195
|
+
if (allCompleted) {
|
|
196
|
+
newStatus = 'completed';
|
|
197
|
+
} else if (anyInProgress || intent.work_items.some(wi => wi.status === 'completed')) {
|
|
198
|
+
newStatus = 'in_progress';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
202
|
+
const parsed = parseFrontmatter(content);
|
|
203
|
+
|
|
204
|
+
if (!parsed) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Only update if status actually changed
|
|
209
|
+
if (parsed.frontmatter.status === newStatus) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
parsed.frontmatter.status = newStatus;
|
|
214
|
+
if (allCompleted) {
|
|
215
|
+
parsed.frontmatter.completed_at = new Date().toISOString();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const newContent = buildMarkdownWithFrontmatter(parsed.frontmatter, parsed.body);
|
|
219
|
+
fs.writeFileSync(filePath, newContent);
|
|
220
|
+
return true;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error(`Warning: Could not update intent markdown ${filePath}: ${err.message}`);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// =============================================================================
|
|
228
|
+
// State Operations
|
|
229
|
+
// =============================================================================
|
|
230
|
+
|
|
231
|
+
function readState(statePath) {
|
|
232
|
+
try {
|
|
233
|
+
const content = fs.readFileSync(statePath, 'utf8');
|
|
234
|
+
const state = yaml.parse(content);
|
|
235
|
+
if (!state || typeof state !== 'object') {
|
|
236
|
+
throw fireError('State file is empty or invalid.', 'COMPLETE_020', 'Check state.yaml format.');
|
|
237
|
+
}
|
|
238
|
+
return state;
|
|
239
|
+
} catch (err) {
|
|
240
|
+
if (err.code && err.code.startsWith('COMPLETE_')) throw err;
|
|
241
|
+
throw fireError(
|
|
242
|
+
`Failed to read state file: ${err.message}`,
|
|
243
|
+
'COMPLETE_021',
|
|
244
|
+
'Check file permissions and YAML syntax.'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function writeState(statePath, state) {
|
|
250
|
+
try {
|
|
251
|
+
fs.writeFileSync(statePath, yaml.stringify(state));
|
|
252
|
+
} catch (err) {
|
|
253
|
+
throw fireError(
|
|
254
|
+
`Failed to write state file: ${err.message}`,
|
|
255
|
+
'COMPLETE_022',
|
|
256
|
+
'Check file permissions and disk space.'
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// =============================================================================
|
|
262
|
+
// Run Log Operations
|
|
263
|
+
// =============================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Update run.md using proper YAML parsing instead of fragile regex.
|
|
267
|
+
* This ensures frontmatter updates work regardless of field order or formatting.
|
|
268
|
+
*/
|
|
269
|
+
function updateRunLog(runLogPath, activeRun, params, completedTime, isFullCompletion) {
|
|
270
|
+
let content;
|
|
271
|
+
try {
|
|
272
|
+
content = fs.readFileSync(runLogPath, 'utf8');
|
|
273
|
+
} catch (err) {
|
|
274
|
+
throw fireError(
|
|
275
|
+
`Failed to read run log: ${err.message}`,
|
|
276
|
+
'COMPLETE_030',
|
|
277
|
+
'Check file permissions.'
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Parse frontmatter using YAML (robust approach)
|
|
282
|
+
const parsed = parseFrontmatter(content);
|
|
283
|
+
if (!parsed) {
|
|
284
|
+
throw fireError(
|
|
285
|
+
'Invalid run.md format - no valid YAML frontmatter found.',
|
|
286
|
+
'COMPLETE_032',
|
|
287
|
+
'Ensure run.md has valid ---\\n...\\n--- frontmatter.'
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let { frontmatter, body } = parsed;
|
|
292
|
+
|
|
293
|
+
// Update frontmatter fields
|
|
294
|
+
if (isFullCompletion) {
|
|
295
|
+
frontmatter.status = 'completed';
|
|
296
|
+
frontmatter.completed = completedTime;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
frontmatter.current_item = activeRun.current_item || null;
|
|
300
|
+
|
|
301
|
+
// Update work_items array in frontmatter
|
|
302
|
+
if (activeRun.work_items && Array.isArray(activeRun.work_items)) {
|
|
303
|
+
frontmatter.work_items = activeRun.work_items.map(item => ({
|
|
304
|
+
id: item.id,
|
|
305
|
+
intent: item.intent,
|
|
306
|
+
mode: item.mode,
|
|
307
|
+
status: item.status,
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Update markdown body sections
|
|
312
|
+
// Update Work Items section
|
|
313
|
+
if (activeRun.work_items && Array.isArray(activeRun.work_items)) {
|
|
314
|
+
const workItemsLines = activeRun.work_items.map((item, i) =>
|
|
315
|
+
`${i + 1}. **${item.id}** (${item.mode}) — ${item.status}`
|
|
316
|
+
).join('\n');
|
|
317
|
+
|
|
318
|
+
body = body.replace(
|
|
319
|
+
/## Work Items\n[\s\S]*?(?=\n## )/,
|
|
320
|
+
`## Work Items\n${workItemsLines}\n\n`
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Update Current Item section
|
|
324
|
+
if (activeRun.current_item) {
|
|
325
|
+
const currentItem = activeRun.work_items.find(i => i.id === activeRun.current_item);
|
|
326
|
+
if (currentItem) {
|
|
327
|
+
body = body.replace(
|
|
328
|
+
/## Current Item\n[^\n]+/,
|
|
329
|
+
`## Current Item\n${currentItem.id} (${currentItem.mode})`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
body = body.replace(
|
|
334
|
+
/## Current Item\n[^\n]+/,
|
|
335
|
+
`## Current Item\n(all completed)`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Format file lists (only on full completion)
|
|
341
|
+
if (isFullCompletion) {
|
|
342
|
+
const filesCreatedText = params.filesCreated.length > 0
|
|
343
|
+
? params.filesCreated.map(f => `- \`${f.path}\`: ${f.purpose || '(no purpose)'}`).join('\n')
|
|
344
|
+
: '(none)';
|
|
345
|
+
|
|
346
|
+
const filesModifiedText = params.filesModified.length > 0
|
|
347
|
+
? params.filesModified.map(f => `- \`${f.path}\`: ${f.changes || '(no changes)'}`).join('\n')
|
|
348
|
+
: '(none)';
|
|
349
|
+
|
|
350
|
+
const decisionsText = params.decisions.length > 0
|
|
351
|
+
? params.decisions.map(d => `- **${d.decision}**: ${d.choice} (${d.rationale || 'no rationale'})`).join('\n')
|
|
352
|
+
: '(none)';
|
|
353
|
+
|
|
354
|
+
// Replace placeholder sections
|
|
355
|
+
body = body.replace('## Files Created\n(none yet)', `## Files Created\n${filesCreatedText}`);
|
|
356
|
+
body = body.replace('## Files Modified\n(none yet)', `## Files Modified\n${filesModifiedText}`);
|
|
357
|
+
body = body.replace('## Decisions\n(none yet)', `## Decisions\n${decisionsText}`);
|
|
358
|
+
|
|
359
|
+
// Add summary if not present
|
|
360
|
+
if (!body.includes('## Summary')) {
|
|
361
|
+
const itemCount = activeRun.work_items ? activeRun.work_items.length : 1;
|
|
362
|
+
body += `
|
|
363
|
+
|
|
364
|
+
## Summary
|
|
365
|
+
|
|
366
|
+
- Work items completed: ${itemCount}
|
|
367
|
+
- Files created: ${params.filesCreated.length}
|
|
368
|
+
- Files modified: ${params.filesModified.length}
|
|
369
|
+
- Tests added: ${params.testsAdded}
|
|
370
|
+
- Coverage: ${params.coverage}%
|
|
371
|
+
- Completed: ${completedTime}
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Reconstruct content with updated frontmatter and body
|
|
377
|
+
const newContent = buildMarkdownWithFrontmatter(frontmatter, body);
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
fs.writeFileSync(runLogPath, newContent);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
throw fireError(
|
|
383
|
+
`Failed to write run log: ${err.message}`,
|
|
384
|
+
'COMPLETE_031',
|
|
385
|
+
'Check file permissions.'
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// =============================================================================
|
|
391
|
+
// Complete Current Item (for batch runs)
|
|
392
|
+
// =============================================================================
|
|
393
|
+
|
|
394
|
+
function completeCurrentItem(rootPath, runId, params = {}) {
|
|
395
|
+
const completionParams = {
|
|
396
|
+
filesCreated: params.filesCreated || [],
|
|
397
|
+
filesModified: params.filesModified || [],
|
|
398
|
+
decisions: params.decisions || [],
|
|
399
|
+
testsAdded: params.testsAdded || 0,
|
|
400
|
+
coverage: params.coverage || 0,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
validateInputs(rootPath, runId);
|
|
404
|
+
const { statePath, runLogPath } = validateFireProject(rootPath, runId);
|
|
405
|
+
const state = readState(statePath);
|
|
406
|
+
|
|
407
|
+
// Find run in active runs list
|
|
408
|
+
const activeRuns = state.runs?.active || [];
|
|
409
|
+
const runIndex = activeRuns.findIndex(r => r.id === runId);
|
|
410
|
+
|
|
411
|
+
if (runIndex === -1) {
|
|
412
|
+
throw fireError(
|
|
413
|
+
`Run "${runId}" not found in active runs.`,
|
|
414
|
+
'COMPLETE_040',
|
|
415
|
+
'The run may have already been completed or was never started.'
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const activeRun = activeRuns[runIndex];
|
|
420
|
+
const completedTime = new Date().toISOString();
|
|
421
|
+
const workItems = activeRun.work_items || [];
|
|
422
|
+
const currentItemId = activeRun.current_item;
|
|
423
|
+
|
|
424
|
+
// Find and mark current item as completed
|
|
425
|
+
let currentItemIndex = -1;
|
|
426
|
+
for (let i = 0; i < workItems.length; i++) {
|
|
427
|
+
if (workItems[i].id === currentItemId) {
|
|
428
|
+
workItems[i].status = 'completed';
|
|
429
|
+
workItems[i].completed_at = completedTime;
|
|
430
|
+
currentItemIndex = i;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (currentItemIndex === -1) {
|
|
436
|
+
throw fireError(
|
|
437
|
+
`Current item "${currentItemId}" not found in work items.`,
|
|
438
|
+
'COMPLETE_050',
|
|
439
|
+
'The run state may be corrupted.'
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Find next pending item
|
|
444
|
+
let nextItem = null;
|
|
445
|
+
for (let i = currentItemIndex + 1; i < workItems.length; i++) {
|
|
446
|
+
if (workItems[i].status === 'pending') {
|
|
447
|
+
workItems[i].status = 'in_progress';
|
|
448
|
+
workItems[i].current_phase = 'plan';
|
|
449
|
+
nextItem = workItems[i];
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Update active run in list
|
|
455
|
+
activeRun.work_items = workItems;
|
|
456
|
+
activeRun.current_item = nextItem ? nextItem.id : null;
|
|
457
|
+
state.runs.active[runIndex] = activeRun;
|
|
458
|
+
|
|
459
|
+
// Update run log
|
|
460
|
+
updateRunLog(runLogPath, activeRun, completionParams, completedTime, false);
|
|
461
|
+
|
|
462
|
+
// Sync markdown frontmatter for completed work item
|
|
463
|
+
const completedWorkItem = workItems.find(wi => wi.id === currentItemId);
|
|
464
|
+
if (completedWorkItem) {
|
|
465
|
+
updateWorkItemMarkdown(
|
|
466
|
+
rootPath,
|
|
467
|
+
completedWorkItem.intent,
|
|
468
|
+
currentItemId,
|
|
469
|
+
'completed',
|
|
470
|
+
runId,
|
|
471
|
+
completedTime
|
|
472
|
+
);
|
|
473
|
+
// Update intent status based on its work items
|
|
474
|
+
updateIntentMarkdown(rootPath, completedWorkItem.intent, state);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Also update next item's markdown to in_progress
|
|
478
|
+
if (nextItem) {
|
|
479
|
+
updateWorkItemMarkdown(rootPath, nextItem.intent, nextItem.id, 'in_progress', null, null);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Save state
|
|
483
|
+
writeState(statePath, state);
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
success: true,
|
|
487
|
+
runId: runId,
|
|
488
|
+
completedItem: currentItemId,
|
|
489
|
+
nextItem: nextItem ? nextItem.id : null,
|
|
490
|
+
remainingItems: workItems.filter(i => i.status === 'pending').length,
|
|
491
|
+
allItemsCompleted: nextItem === null,
|
|
492
|
+
completedAt: completedTime,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// =============================================================================
|
|
497
|
+
// Complete Entire Run
|
|
498
|
+
// =============================================================================
|
|
499
|
+
|
|
500
|
+
function completeRun(rootPath, runId, params = {}) {
|
|
501
|
+
const completionParams = {
|
|
502
|
+
filesCreated: params.filesCreated || [],
|
|
503
|
+
filesModified: params.filesModified || [],
|
|
504
|
+
decisions: params.decisions || [],
|
|
505
|
+
testsAdded: params.testsAdded || 0,
|
|
506
|
+
coverage: params.coverage || 0,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
validateInputs(rootPath, runId);
|
|
510
|
+
const { statePath, runLogPath } = validateFireProject(rootPath, runId);
|
|
511
|
+
const state = readState(statePath);
|
|
512
|
+
|
|
513
|
+
// Initialize runs structure if needed
|
|
514
|
+
if (!state.runs) {
|
|
515
|
+
state.runs = { active: [], completed: [] };
|
|
516
|
+
}
|
|
517
|
+
if (!Array.isArray(state.runs.active)) {
|
|
518
|
+
state.runs.active = [];
|
|
519
|
+
}
|
|
520
|
+
if (!Array.isArray(state.runs.completed)) {
|
|
521
|
+
state.runs.completed = [];
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Find run in active runs list
|
|
525
|
+
const runIndex = state.runs.active.findIndex(r => r.id === runId);
|
|
526
|
+
|
|
527
|
+
if (runIndex === -1) {
|
|
528
|
+
throw fireError(
|
|
529
|
+
`Run "${runId}" not found in active runs.`,
|
|
530
|
+
'COMPLETE_040',
|
|
531
|
+
'The run may have already been completed or was never started.'
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const activeRun = state.runs.active[runIndex];
|
|
536
|
+
const completedTime = new Date().toISOString();
|
|
537
|
+
const workItems = activeRun.work_items || [];
|
|
538
|
+
const scope = activeRun.scope || 'single';
|
|
539
|
+
|
|
540
|
+
// Mark all items as completed
|
|
541
|
+
for (const item of workItems) {
|
|
542
|
+
if (item.status !== 'completed') {
|
|
543
|
+
item.status = 'completed';
|
|
544
|
+
item.completed_at = completedTime;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
activeRun.work_items = workItems;
|
|
549
|
+
activeRun.current_item = null;
|
|
550
|
+
|
|
551
|
+
// Update run log
|
|
552
|
+
updateRunLog(runLogPath, activeRun, completionParams, completedTime, true);
|
|
553
|
+
|
|
554
|
+
// Build completed run record
|
|
555
|
+
const completedRun = {
|
|
556
|
+
id: runId,
|
|
557
|
+
scope: scope,
|
|
558
|
+
work_items: workItems.map(i => ({
|
|
559
|
+
id: i.id,
|
|
560
|
+
intent: i.intent,
|
|
561
|
+
mode: i.mode,
|
|
562
|
+
})),
|
|
563
|
+
started: activeRun.started,
|
|
564
|
+
completed: completedTime,
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// Check for duplicate (idempotency)
|
|
568
|
+
const alreadyRecorded = state.runs.completed.some(r => r.id === runId);
|
|
569
|
+
|
|
570
|
+
// Update work item status in intents (state.yaml)
|
|
571
|
+
const affectedIntents = new Set();
|
|
572
|
+
if (Array.isArray(state.intents)) {
|
|
573
|
+
for (const workItem of workItems) {
|
|
574
|
+
for (const intent of state.intents) {
|
|
575
|
+
if (intent.id === workItem.intent && Array.isArray(intent.work_items)) {
|
|
576
|
+
for (const wi of intent.work_items) {
|
|
577
|
+
if (wi.id === workItem.id) {
|
|
578
|
+
wi.status = 'completed';
|
|
579
|
+
wi.run_id = runId;
|
|
580
|
+
wi.completed_at = completedTime;
|
|
581
|
+
affectedIntents.add(intent.id);
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Remove from active runs and add to completed
|
|
591
|
+
state.runs.active.splice(runIndex, 1);
|
|
592
|
+
if (!alreadyRecorded) {
|
|
593
|
+
state.runs.completed.push(completedRun);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Save state first (so markdown sync has correct state)
|
|
597
|
+
writeState(statePath, state);
|
|
598
|
+
|
|
599
|
+
// Sync markdown frontmatter for all completed work items
|
|
600
|
+
for (const workItem of workItems) {
|
|
601
|
+
updateWorkItemMarkdown(
|
|
602
|
+
rootPath,
|
|
603
|
+
workItem.intent,
|
|
604
|
+
workItem.id,
|
|
605
|
+
'completed',
|
|
606
|
+
runId,
|
|
607
|
+
workItem.completed_at || completedTime
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Update intent markdown for all affected intents
|
|
612
|
+
for (const intentId of affectedIntents) {
|
|
613
|
+
updateIntentMarkdown(rootPath, intentId, state);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
success: true,
|
|
618
|
+
runId: runId,
|
|
619
|
+
scope: scope,
|
|
620
|
+
workItemsCompleted: workItems.length,
|
|
621
|
+
completedAt: completedTime,
|
|
622
|
+
filesCreated: completionParams.filesCreated.length,
|
|
623
|
+
filesModified: completionParams.filesModified.length,
|
|
624
|
+
testsAdded: completionParams.testsAdded,
|
|
625
|
+
coverage: completionParams.coverage,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// =============================================================================
|
|
630
|
+
// CLI Argument Parsing
|
|
631
|
+
// =============================================================================
|
|
632
|
+
|
|
633
|
+
function parseArgs(args) {
|
|
634
|
+
const result = {
|
|
635
|
+
rootPath: args[0],
|
|
636
|
+
runId: args[1],
|
|
637
|
+
completeItem: false,
|
|
638
|
+
completeRunFlag: false,
|
|
639
|
+
filesCreated: [],
|
|
640
|
+
filesModified: [],
|
|
641
|
+
decisions: [],
|
|
642
|
+
testsAdded: 0,
|
|
643
|
+
coverage: 0,
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
for (let i = 2; i < args.length; i++) {
|
|
647
|
+
const arg = args[i];
|
|
648
|
+
if (arg === '--complete-item') {
|
|
649
|
+
result.completeItem = true;
|
|
650
|
+
} else if (arg === '--complete-run') {
|
|
651
|
+
result.completeRunFlag = true;
|
|
652
|
+
} else if (arg.startsWith('--files-created=')) {
|
|
653
|
+
try {
|
|
654
|
+
result.filesCreated = JSON.parse(arg.substring('--files-created='.length));
|
|
655
|
+
} catch (e) {
|
|
656
|
+
console.error('Warning: Could not parse --files-created JSON');
|
|
657
|
+
}
|
|
658
|
+
} else if (arg.startsWith('--files-modified=')) {
|
|
659
|
+
try {
|
|
660
|
+
result.filesModified = JSON.parse(arg.substring('--files-modified='.length));
|
|
661
|
+
} catch (e) {
|
|
662
|
+
console.error('Warning: Could not parse --files-modified JSON');
|
|
663
|
+
}
|
|
664
|
+
} else if (arg.startsWith('--decisions=')) {
|
|
665
|
+
try {
|
|
666
|
+
result.decisions = JSON.parse(arg.substring('--decisions='.length));
|
|
667
|
+
} catch (e) {
|
|
668
|
+
console.error('Warning: Could not parse --decisions JSON');
|
|
669
|
+
}
|
|
670
|
+
} else if (arg.startsWith('--tests=')) {
|
|
671
|
+
result.testsAdded = parseInt(arg.substring('--tests='.length), 10) || 0;
|
|
672
|
+
} else if (arg.startsWith('--coverage=')) {
|
|
673
|
+
result.coverage = parseFloat(arg.substring('--coverage='.length)) || 0;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function printUsage() {
|
|
681
|
+
console.error('Usage:');
|
|
682
|
+
console.error(' Complete current item: node complete-run.cjs <rootPath> <runId> --complete-item [options]');
|
|
683
|
+
console.error(' Complete entire run: node complete-run.cjs <rootPath> <runId> --complete-run [options]');
|
|
684
|
+
console.error(' Auto (single runs): node complete-run.cjs <rootPath> <runId> [options]');
|
|
685
|
+
console.error('');
|
|
686
|
+
console.error('Arguments:');
|
|
687
|
+
console.error(' rootPath - Project root directory');
|
|
688
|
+
console.error(' runId - Run ID to complete (e.g., run-003)');
|
|
689
|
+
console.error('');
|
|
690
|
+
console.error('Flags:');
|
|
691
|
+
console.error(' --complete-item - Complete only the current work item (batch/wide runs)');
|
|
692
|
+
console.error(' --complete-run - Complete the entire run');
|
|
693
|
+
console.error('');
|
|
694
|
+
console.error('Options:');
|
|
695
|
+
console.error(' --files-created=JSON - JSON array of {path, purpose}');
|
|
696
|
+
console.error(' --files-modified=JSON - JSON array of {path, changes}');
|
|
697
|
+
console.error(' --decisions=JSON - JSON array of {decision, choice, rationale}');
|
|
698
|
+
console.error(' --tests=N - Number of tests added');
|
|
699
|
+
console.error(' --coverage=N - Coverage percentage');
|
|
700
|
+
console.error('');
|
|
701
|
+
console.error('Examples:');
|
|
702
|
+
console.error(' node complete-run.cjs /project run-003 --complete-item');
|
|
703
|
+
console.error(' node complete-run.cjs /project run-003 --complete-run --tests=5 --coverage=85');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// =============================================================================
|
|
707
|
+
// CLI Interface
|
|
708
|
+
// =============================================================================
|
|
709
|
+
|
|
710
|
+
if (require.main === module) {
|
|
711
|
+
const args = process.argv.slice(2);
|
|
712
|
+
|
|
713
|
+
if (args.length < 2) {
|
|
714
|
+
printUsage();
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const params = parseArgs(args);
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
let result;
|
|
722
|
+
if (params.completeItem) {
|
|
723
|
+
result = completeCurrentItem(params.rootPath, params.runId, {
|
|
724
|
+
filesCreated: params.filesCreated,
|
|
725
|
+
filesModified: params.filesModified,
|
|
726
|
+
decisions: params.decisions,
|
|
727
|
+
testsAdded: params.testsAdded,
|
|
728
|
+
coverage: params.coverage,
|
|
729
|
+
});
|
|
730
|
+
} else {
|
|
731
|
+
// Default: complete entire run
|
|
732
|
+
result = completeRun(params.rootPath, params.runId, {
|
|
733
|
+
filesCreated: params.filesCreated,
|
|
734
|
+
filesModified: params.filesModified,
|
|
735
|
+
decisions: params.decisions,
|
|
736
|
+
testsAdded: params.testsAdded,
|
|
737
|
+
coverage: params.coverage,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
console.log(JSON.stringify(result, null, 2));
|
|
741
|
+
process.exit(0);
|
|
742
|
+
} catch (err) {
|
|
743
|
+
console.error(err.message);
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
module.exports = { completeRun, completeCurrentItem };
|