specsmd 0.0.0-dev.86 → 0.0.0-dev.87
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 +15 -0
- package/bin/cli.js +15 -1
- package/flows/fire/agents/builder/agent.md +2 -2
- package/flows/fire/agents/builder/skills/code-review/SKILL.md +1 -1
- package/flows/fire/agents/builder/skills/run-execute/SKILL.md +16 -7
- package/flows/fire/agents/builder/skills/run-execute/scripts/complete-run.cjs +22 -3
- package/flows/fire/agents/builder/skills/run-execute/scripts/init-run.cjs +63 -20
- package/flows/fire/agents/builder/skills/run-execute/scripts/update-checkpoint.cjs +254 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/update-phase.cjs +17 -6
- package/flows/fire/agents/builder/skills/run-status/SKILL.md +1 -1
- package/flows/fire/agents/orchestrator/agent.md +1 -1
- package/flows/fire/agents/orchestrator/skills/status/SKILL.md +2 -2
- package/flows/fire/memory-bank.yaml +4 -4
- package/lib/dashboard/aidlc/parser.js +581 -0
- package/lib/dashboard/fire/model.js +382 -0
- package/lib/dashboard/fire/parser.js +470 -0
- package/lib/dashboard/flow-detect.js +86 -0
- package/lib/dashboard/git/changes.js +362 -0
- package/lib/dashboard/git/worktrees.js +248 -0
- package/lib/dashboard/index.js +709 -0
- package/lib/dashboard/runtime/watch-runtime.js +122 -0
- package/lib/dashboard/simple/parser.js +293 -0
- package/lib/dashboard/tui/app.js +1675 -0
- package/lib/dashboard/tui/components/error-banner.js +35 -0
- package/lib/dashboard/tui/components/header.js +60 -0
- package/lib/dashboard/tui/components/help-footer.js +15 -0
- package/lib/dashboard/tui/components/stats-strip.js +35 -0
- package/lib/dashboard/tui/file-entries.js +383 -0
- package/lib/dashboard/tui/flow-builders.js +991 -0
- package/lib/dashboard/tui/git-builders.js +218 -0
- package/lib/dashboard/tui/helpers.js +236 -0
- package/lib/dashboard/tui/overlays.js +242 -0
- package/lib/dashboard/tui/preview.js +220 -0
- package/lib/dashboard/tui/renderer.js +76 -0
- package/lib/dashboard/tui/row-builders.js +797 -0
- package/lib/dashboard/tui/sections.js +45 -0
- package/lib/dashboard/tui/store.js +44 -0
- package/lib/dashboard/tui/views/overview-view.js +61 -0
- package/lib/dashboard/tui/views/runs-view.js +93 -0
- package/lib/dashboard/tui/worktree-builders.js +229 -0
- package/lib/installers/CodexInstaller.js +72 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -89,6 +89,21 @@ During installation, select your flow:
|
|
|
89
89
|
|
|
90
90
|
The installer detects your AI coding tools and sets up agent definitions, slash commands, and project structure for your selected flow.
|
|
91
91
|
|
|
92
|
+
### Live Dashboard (FIRE)
|
|
93
|
+
|
|
94
|
+
Track FIRE state continuously from terminal:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx specsmd@latest dashboard
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Useful options:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx specsmd@latest dashboard --flow fire --path . --refresh-ms 1000
|
|
104
|
+
npx specsmd@latest dashboard --no-watch
|
|
105
|
+
```
|
|
106
|
+
|
|
92
107
|
### Install VS Code Extension (Optional)
|
|
93
108
|
|
|
94
109
|
Track your progress visually with our sidebar extension:
|
package/bin/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
4
|
const installer = require('../lib/installer');
|
|
5
|
+
const dashboard = require('../lib/dashboard');
|
|
5
6
|
const packageJson = require('../package.json');
|
|
6
7
|
|
|
7
8
|
program
|
|
@@ -18,4 +19,17 @@ program
|
|
|
18
19
|
.description('Uninstall specsmd from the current project')
|
|
19
20
|
.action(installer.uninstall);
|
|
20
21
|
|
|
21
|
-
program
|
|
22
|
+
program
|
|
23
|
+
.command('dashboard')
|
|
24
|
+
.description('Live terminal dashboard for flow state (FIRE first)')
|
|
25
|
+
.option('--flow <flow>', 'Flow to inspect (fire|aidlc|simple), default auto-detect')
|
|
26
|
+
.option('--path <dir>', 'Workspace path', process.cwd())
|
|
27
|
+
.option('--worktree <nameOrPath>', 'Initial git worktree (branch name, worktree name, id, or absolute path)')
|
|
28
|
+
.option('--refresh-ms <n>', 'Fallback refresh interval in milliseconds (default: 1000)', '1000')
|
|
29
|
+
.option('--no-watch', 'Render once and exit')
|
|
30
|
+
.action((options) => dashboard.run(options));
|
|
31
|
+
|
|
32
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
33
|
+
console.error(error.message);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
@@ -130,7 +130,7 @@ You are the **Builder Agent** for FIRE (Fast Intent-Run Engineering).
|
|
|
130
130
|
|
|
131
131
|
```yaml
|
|
132
132
|
run:
|
|
133
|
-
id: run-001
|
|
133
|
+
id: run-fabriqa-2026-001
|
|
134
134
|
scope: batch # single | batch | wide
|
|
135
135
|
work_items:
|
|
136
136
|
- id: login-endpoint
|
|
@@ -165,7 +165,7 @@ You are the **Builder Agent** for FIRE (Fast Intent-Run Engineering).
|
|
|
165
165
|
| Create run.md | (handled by init-run.cjs) | ❌ NO direct write |
|
|
166
166
|
| Update state.yaml | (handled by scripts) | ❌ NO direct edit |
|
|
167
167
|
|
|
168
|
-
<check if="about to mkdir .specs-fire/runs/run
|
|
168
|
+
<check if="about to mkdir .specs-fire/runs/run-<worktree>-XXX">
|
|
169
169
|
<action>STOP — use init-run.cjs instead</action>
|
|
170
170
|
</check>
|
|
171
171
|
<check if="about to edit state.yaml directly">
|
|
@@ -248,6 +248,8 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
248
248
|
<action>Save plan IMMEDIATELY using template: templates/plan.md.hbs</action>
|
|
249
249
|
<action>Write to: .specs-fire/runs/{run-id}/plan.md</action>
|
|
250
250
|
<output>Plan saved to: .specs-fire/runs/{run-id}/plan.md</output>
|
|
251
|
+
<action>Mark checkpoint as waiting:</action>
|
|
252
|
+
<code>node scripts/update-checkpoint.cjs {rootPath} {runId} awaiting_approval --checkpoint=plan</code>
|
|
251
253
|
|
|
252
254
|
<checkpoint>
|
|
253
255
|
<template_output section="plan">
|
|
@@ -276,6 +278,8 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
276
278
|
<action>Update plan.md with changes</action>
|
|
277
279
|
<goto step="3b"/>
|
|
278
280
|
</check>
|
|
281
|
+
<action>Mark checkpoint approved:</action>
|
|
282
|
+
<code>node scripts/update-checkpoint.cjs {rootPath} {runId} approved --checkpoint=plan</code>
|
|
279
283
|
<goto step="5"/>
|
|
280
284
|
</step>
|
|
281
285
|
|
|
@@ -286,6 +290,8 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
286
290
|
<action>Write to: .specs-fire/runs/{run-id}/plan.md</action>
|
|
287
291
|
<action>Include reference to design doc in plan</action>
|
|
288
292
|
<output>Plan saved to: .specs-fire/runs/{run-id}/plan.md</output>
|
|
293
|
+
<action>Mark checkpoint as waiting:</action>
|
|
294
|
+
<code>node scripts/update-checkpoint.cjs {rootPath} {runId} awaiting_approval --checkpoint=plan</code>
|
|
289
295
|
|
|
290
296
|
<checkpoint>
|
|
291
297
|
<template_output section="plan">
|
|
@@ -314,6 +320,8 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
314
320
|
<action>Update plan.md with changes</action>
|
|
315
321
|
<goto step="3c"/>
|
|
316
322
|
</check>
|
|
323
|
+
<action>Mark checkpoint approved:</action>
|
|
324
|
+
<code>node scripts/update-checkpoint.cjs {rootPath} {runId} approved --checkpoint=plan</code>
|
|
317
325
|
<goto step="5"/>
|
|
318
326
|
</step>
|
|
319
327
|
|
|
@@ -574,6 +582,7 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
574
582
|
| Script | Purpose | Usage |
|
|
575
583
|
|--------|---------|-------|
|
|
576
584
|
| `scripts/init-run.cjs` | Initialize run record and folder | Creates run.md with all work items |
|
|
585
|
+
| `scripts/update-checkpoint.cjs` | Mark approval gate state for active item | `awaiting_approval` / `approved` |
|
|
577
586
|
| `scripts/update-phase.cjs` | Update current work item's phase | `node scripts/update-phase.cjs {rootPath} {runId} {phase}` |
|
|
578
587
|
| `scripts/complete-run.cjs` | Finalize run and update state | `--complete-item` or `--complete-run` |
|
|
579
588
|
|
|
@@ -593,8 +602,8 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
593
602
|
```json
|
|
594
603
|
{
|
|
595
604
|
"success": true,
|
|
596
|
-
"runId": "run-001",
|
|
597
|
-
"runPath": "/project/.specs-fire/runs/run-001",
|
|
605
|
+
"runId": "run-fabriqa-2026-001",
|
|
606
|
+
"runPath": "/project/.specs-fire/runs/run-fabriqa-2026-001",
|
|
598
607
|
"scope": "batch",
|
|
599
608
|
"workItems": [...],
|
|
600
609
|
"currentItem": "wi-1"
|
|
@@ -606,10 +615,10 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
606
615
|
<script name="complete-run.cjs">
|
|
607
616
|
```bash
|
|
608
617
|
# Complete current item (batch runs - moves to next item)
|
|
609
|
-
node scripts/complete-run.cjs /project run-001 --complete-item
|
|
618
|
+
node scripts/complete-run.cjs /project run-fabriqa-2026-001 --complete-item
|
|
610
619
|
|
|
611
620
|
# Complete entire run (single runs or final item in batch)
|
|
612
|
-
node scripts/complete-run.cjs /project run-001 --complete-run \
|
|
621
|
+
node scripts/complete-run.cjs /project run-fabriqa-2026-001 --complete-run \
|
|
613
622
|
--files-created='[{"path":"src/new.ts","purpose":"New feature"}]' \
|
|
614
623
|
--files-modified='[{"path":"src/old.ts","changes":"Added import"}]' \
|
|
615
624
|
--tests=5 --coverage=85
|
|
@@ -619,7 +628,7 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
619
628
|
```json
|
|
620
629
|
{
|
|
621
630
|
"success": true,
|
|
622
|
-
"runId": "run-001",
|
|
631
|
+
"runId": "run-fabriqa-2026-001",
|
|
623
632
|
"completedItem": "wi-1",
|
|
624
633
|
"nextItem": "wi-2",
|
|
625
634
|
"remainingItems": 1,
|
|
@@ -632,7 +641,7 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
632
641
|
```json
|
|
633
642
|
{
|
|
634
643
|
"success": true,
|
|
635
|
-
"runId": "run-001",
|
|
644
|
+
"runId": "run-fabriqa-2026-001",
|
|
636
645
|
"scope": "batch",
|
|
637
646
|
"workItemsCompleted": 2,
|
|
638
647
|
"completedAt": "2026-01-20T..."
|
|
@@ -664,7 +673,7 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
664
673
|
After init-run.cjs creates a run:
|
|
665
674
|
|
|
666
675
|
```
|
|
667
|
-
.specs-fire/runs/run-001/
|
|
676
|
+
.specs-fire/runs/run-fabriqa-2026-001/
|
|
668
677
|
├── run.md # Created by init-run.cjs, updated by complete-run.cjs
|
|
669
678
|
├── plan.md # Created BEFORE implementation (ALL modes - required)
|
|
670
679
|
├── test-report.md # Created AFTER tests pass (required)
|
|
@@ -306,6 +306,9 @@ function updateRunLog(runLogPath, activeRun, params, completedTime, isFullComple
|
|
|
306
306
|
intent: item.intent,
|
|
307
307
|
mode: item.mode,
|
|
308
308
|
status: item.status,
|
|
309
|
+
current_phase: item.current_phase || null,
|
|
310
|
+
checkpoint_state: item.checkpoint_state || null,
|
|
311
|
+
current_checkpoint: item.current_checkpoint || null,
|
|
309
312
|
}));
|
|
310
313
|
}
|
|
311
314
|
|
|
@@ -429,6 +432,12 @@ function completeCurrentItem(rootPath, runId, params = {}, options = {}) {
|
|
|
429
432
|
if (workItems[i].id === currentItemId) {
|
|
430
433
|
workItems[i].status = 'completed';
|
|
431
434
|
workItems[i].completed_at = completedTime;
|
|
435
|
+
if (workItems[i].mode === 'confirm' || workItems[i].mode === 'validate') {
|
|
436
|
+
workItems[i].checkpoint_state = 'approved';
|
|
437
|
+
workItems[i].current_checkpoint = workItems[i].current_checkpoint || 'plan';
|
|
438
|
+
} else {
|
|
439
|
+
workItems[i].checkpoint_state = workItems[i].checkpoint_state || 'not_required';
|
|
440
|
+
}
|
|
432
441
|
currentItemIndex = i;
|
|
433
442
|
break;
|
|
434
443
|
}
|
|
@@ -458,6 +467,10 @@ function completeCurrentItem(rootPath, runId, params = {}, options = {}) {
|
|
|
458
467
|
if (workItems[i].status === 'pending') {
|
|
459
468
|
workItems[i].status = 'in_progress';
|
|
460
469
|
workItems[i].current_phase = 'plan';
|
|
470
|
+
workItems[i].checkpoint_state = 'none';
|
|
471
|
+
workItems[i].current_checkpoint = (workItems[i].mode === 'confirm' || workItems[i].mode === 'validate')
|
|
472
|
+
? 'plan'
|
|
473
|
+
: null;
|
|
461
474
|
nextItem = workItems[i];
|
|
462
475
|
break;
|
|
463
476
|
}
|
|
@@ -571,6 +584,12 @@ function completeRun(rootPath, runId, params = {}, options = {}) {
|
|
|
571
584
|
item.status = 'completed';
|
|
572
585
|
item.completed_at = completedTime;
|
|
573
586
|
}
|
|
587
|
+
if (item.mode === 'confirm' || item.mode === 'validate') {
|
|
588
|
+
item.checkpoint_state = 'approved';
|
|
589
|
+
item.current_checkpoint = item.current_checkpoint || 'plan';
|
|
590
|
+
} else {
|
|
591
|
+
item.checkpoint_state = item.checkpoint_state || 'not_required';
|
|
592
|
+
}
|
|
574
593
|
}
|
|
575
594
|
|
|
576
595
|
activeRun.work_items = workItems;
|
|
@@ -716,7 +735,7 @@ function printUsage() {
|
|
|
716
735
|
console.error('');
|
|
717
736
|
console.error('Arguments:');
|
|
718
737
|
console.error(' rootPath - Project root directory');
|
|
719
|
-
console.error(' runId - Run ID to complete (e.g., run-003)');
|
|
738
|
+
console.error(' runId - Run ID to complete (e.g., run-fabriqa-2026-003)');
|
|
720
739
|
console.error('');
|
|
721
740
|
console.error('Flags:');
|
|
722
741
|
console.error(' --complete-item - Complete only the current work item (batch/wide runs)');
|
|
@@ -731,8 +750,8 @@ function printUsage() {
|
|
|
731
750
|
console.error(' --coverage=N - Coverage percentage');
|
|
732
751
|
console.error('');
|
|
733
752
|
console.error('Examples:');
|
|
734
|
-
console.error(' node complete-run.cjs /project run-003 --complete-item');
|
|
735
|
-
console.error(' node complete-run.cjs /project run-003 --complete-run --tests=5 --coverage=85');
|
|
753
|
+
console.error(' node complete-run.cjs /project run-fabriqa-2026-003 --complete-item');
|
|
754
|
+
console.error(' node complete-run.cjs /project run-fabriqa-2026-003 --complete-run --tests=5 --coverage=85');
|
|
736
755
|
}
|
|
737
756
|
|
|
738
757
|
// =============================================================================
|
|
@@ -152,26 +152,61 @@ function writeState(statePath, state) {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// =============================================================================
|
|
155
|
-
// Run ID Generation (CRITICAL - checks
|
|
155
|
+
// Run ID Generation (CRITICAL - checks state and file system)
|
|
156
156
|
// =============================================================================
|
|
157
157
|
|
|
158
|
-
function
|
|
158
|
+
function sanitizeWorktreeToken(value) {
|
|
159
|
+
const normalized = String(value || '')
|
|
160
|
+
.toLowerCase()
|
|
161
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
162
|
+
.replace(/^-+|-+$/g, '');
|
|
163
|
+
return normalized || 'workspace';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolveWorktreeToken(rootPath) {
|
|
167
|
+
const baseName = path.basename(path.resolve(String(rootPath || '')));
|
|
168
|
+
return sanitizeWorktreeToken(baseName);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseRunSequence(runId, worktreeToken) {
|
|
172
|
+
if (typeof runId !== 'string' || runId.trim() === '') {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const legacyMatch = runId.match(/^run-(\d+)$/);
|
|
177
|
+
if (legacyMatch) {
|
|
178
|
+
const parsed = parseInt(legacyMatch[1], 10);
|
|
179
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const worktreeMatch = runId.match(/^run-([a-z0-9][a-z0-9-]*)-(\d+)$/);
|
|
183
|
+
if (worktreeMatch && worktreeMatch[1] === worktreeToken) {
|
|
184
|
+
const parsed = parseInt(worktreeMatch[2], 10);
|
|
185
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function generateRunId(rootPath, runsPath, state) {
|
|
159
192
|
// Ensure runs directory exists
|
|
160
193
|
if (!fs.existsSync(runsPath)) {
|
|
161
194
|
fs.mkdirSync(runsPath, { recursive: true });
|
|
162
195
|
}
|
|
163
196
|
|
|
164
|
-
|
|
165
|
-
let
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
197
|
+
const worktreeToken = resolveWorktreeToken(rootPath);
|
|
198
|
+
let maxFromState = 0;
|
|
199
|
+
|
|
200
|
+
// Source 1: Get max from state.yaml run history (active + completed)
|
|
201
|
+
const stateRuns = state?.runs || {};
|
|
202
|
+
const stateRunRecords = [
|
|
203
|
+
...(Array.isArray(stateRuns.active) ? stateRuns.active : []),
|
|
204
|
+
...(Array.isArray(stateRuns.completed) ? stateRuns.completed : [])
|
|
205
|
+
];
|
|
206
|
+
for (const run of stateRunRecords) {
|
|
207
|
+
const num = parseRunSequence(run?.id, worktreeToken);
|
|
208
|
+
if (num != null && num > maxFromState) {
|
|
209
|
+
maxFromState = num;
|
|
175
210
|
}
|
|
176
211
|
}
|
|
177
212
|
|
|
@@ -180,9 +215,9 @@ function generateRunId(runsPath, state) {
|
|
|
180
215
|
try {
|
|
181
216
|
const entries = fs.readdirSync(runsPath);
|
|
182
217
|
for (const entry of entries) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
218
|
+
const num = parseRunSequence(entry, worktreeToken);
|
|
219
|
+
if (num != null && num > maxFromFileSystem) {
|
|
220
|
+
maxFromFileSystem = num;
|
|
186
221
|
}
|
|
187
222
|
}
|
|
188
223
|
} catch (err) {
|
|
@@ -194,10 +229,10 @@ function generateRunId(runsPath, state) {
|
|
|
194
229
|
}
|
|
195
230
|
|
|
196
231
|
// Use MAX of both to ensure no duplicates
|
|
197
|
-
const maxNum = Math.max(
|
|
232
|
+
const maxNum = Math.max(maxFromState, maxFromFileSystem);
|
|
198
233
|
const nextNum = maxNum + 1;
|
|
199
234
|
|
|
200
|
-
return `run-${String(nextNum).padStart(3, '0')}`;
|
|
235
|
+
return `run-${worktreeToken}-${String(nextNum).padStart(3, '0')}`;
|
|
201
236
|
}
|
|
202
237
|
|
|
203
238
|
// =============================================================================
|
|
@@ -233,7 +268,11 @@ function createRunLog(runPath, runId, workItems, scope, startTime) {
|
|
|
233
268
|
// Format work items for run.md
|
|
234
269
|
const workItemsList = workItems.map((item, index) => {
|
|
235
270
|
const status = index === 0 ? 'in_progress' : 'pending';
|
|
236
|
-
|
|
271
|
+
const currentPhase = index === 0 ? 'plan' : 'null';
|
|
272
|
+
const currentCheckpoint = index === 0 && (item.mode === 'confirm' || item.mode === 'validate')
|
|
273
|
+
? 'plan'
|
|
274
|
+
: 'null';
|
|
275
|
+
return ` - id: ${item.id}\n intent: ${item.intent}\n mode: ${item.mode}\n status: ${status}\n current_phase: ${currentPhase}\n checkpoint_state: none\n current_checkpoint: ${currentCheckpoint}`;
|
|
237
276
|
}).join('\n');
|
|
238
277
|
|
|
239
278
|
const currentItem = workItems[0];
|
|
@@ -329,7 +368,7 @@ function initRun(rootPath, workItems, scope) {
|
|
|
329
368
|
}
|
|
330
369
|
|
|
331
370
|
// Generate run ID (checks both history AND file system)
|
|
332
|
-
const runId = generateRunId(runsPath, state);
|
|
371
|
+
const runId = generateRunId(rootPath, runsPath, state);
|
|
333
372
|
const runPath = path.join(runsPath, runId);
|
|
334
373
|
|
|
335
374
|
// Create run folder
|
|
@@ -346,6 +385,10 @@ function initRun(rootPath, workItems, scope) {
|
|
|
346
385
|
mode: item.mode,
|
|
347
386
|
status: index === 0 ? 'in_progress' : 'pending',
|
|
348
387
|
current_phase: index === 0 ? 'plan' : null,
|
|
388
|
+
checkpoint_state: 'none',
|
|
389
|
+
current_checkpoint: (index === 0 && (item.mode === 'confirm' || item.mode === 'validate'))
|
|
390
|
+
? 'plan'
|
|
391
|
+
: null,
|
|
349
392
|
}));
|
|
350
393
|
|
|
351
394
|
// Add to active runs list (supports multiple parallel runs)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FIRE Checkpoint State Update Script
|
|
5
|
+
*
|
|
6
|
+
* Tracks explicit approval-gate state for the active work item in a run.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node update-checkpoint.cjs <rootPath> <runId> <checkpointState> [--item=<workItemId>] [--checkpoint=<name>]
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* node update-checkpoint.cjs /project run-fabriqa-2026-001 awaiting_approval --checkpoint=plan
|
|
13
|
+
* node update-checkpoint.cjs /project run-fabriqa-2026-001 approved
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const yaml = require('yaml');
|
|
19
|
+
|
|
20
|
+
const VALID_STATES = ['awaiting_approval', 'approved', 'none', 'not_required'];
|
|
21
|
+
|
|
22
|
+
function fireError(message, code, suggestion) {
|
|
23
|
+
const err = new Error(`FIRE Error [${code}]: ${message} ${suggestion}`);
|
|
24
|
+
err.code = code;
|
|
25
|
+
err.suggestion = suggestion;
|
|
26
|
+
return err;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeCheckpointState(value) {
|
|
30
|
+
const normalized = String(value || '').toLowerCase().trim().replace(/[\s-]+/g, '_');
|
|
31
|
+
const map = {
|
|
32
|
+
waiting: 'awaiting_approval',
|
|
33
|
+
awaiting: 'awaiting_approval',
|
|
34
|
+
awaiting_approval: 'awaiting_approval',
|
|
35
|
+
pending_approval: 'awaiting_approval',
|
|
36
|
+
approval_needed: 'awaiting_approval',
|
|
37
|
+
approval_required: 'awaiting_approval',
|
|
38
|
+
approved: 'approved',
|
|
39
|
+
confirmed: 'approved',
|
|
40
|
+
accepted: 'approved',
|
|
41
|
+
resumed: 'approved',
|
|
42
|
+
none: 'none',
|
|
43
|
+
clear: 'none',
|
|
44
|
+
cleared: 'none',
|
|
45
|
+
reset: 'none',
|
|
46
|
+
not_required: 'not_required',
|
|
47
|
+
n_a: 'not_required',
|
|
48
|
+
na: 'not_required',
|
|
49
|
+
skipped: 'not_required'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return map[normalized] || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateInputs(rootPath, runId, checkpointState) {
|
|
56
|
+
if (!rootPath || typeof rootPath !== 'string' || rootPath.trim() === '') {
|
|
57
|
+
throw fireError('rootPath is required.', 'CHECKPOINT_001', 'Provide a valid project root path.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!runId || typeof runId !== 'string' || runId.trim() === '') {
|
|
61
|
+
throw fireError('runId is required.', 'CHECKPOINT_002', 'Provide the run ID.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const normalizedState = normalizeCheckpointState(checkpointState);
|
|
65
|
+
if (!normalizedState || !VALID_STATES.includes(normalizedState)) {
|
|
66
|
+
throw fireError(
|
|
67
|
+
`Invalid checkpointState: "${checkpointState}".`,
|
|
68
|
+
'CHECKPOINT_003',
|
|
69
|
+
`Valid states are: ${VALID_STATES.join(', ')}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(rootPath)) {
|
|
74
|
+
throw fireError(
|
|
75
|
+
`Project root not found: "${rootPath}".`,
|
|
76
|
+
'CHECKPOINT_004',
|
|
77
|
+
'Ensure the path exists and is accessible.'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return normalizedState;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateFireProject(rootPath) {
|
|
85
|
+
const fireDir = path.join(rootPath, '.specs-fire');
|
|
86
|
+
const statePath = path.join(fireDir, 'state.yaml');
|
|
87
|
+
|
|
88
|
+
if (!fs.existsSync(fireDir)) {
|
|
89
|
+
throw fireError(
|
|
90
|
+
`FIRE project not initialized at: "${rootPath}".`,
|
|
91
|
+
'CHECKPOINT_010',
|
|
92
|
+
'Run fire-init first to initialize the project.'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(statePath)) {
|
|
97
|
+
throw fireError(
|
|
98
|
+
`State file not found at: "${statePath}".`,
|
|
99
|
+
'CHECKPOINT_011',
|
|
100
|
+
'The project may be corrupted. Try re-initializing.'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { statePath };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readState(statePath) {
|
|
108
|
+
try {
|
|
109
|
+
const content = fs.readFileSync(statePath, 'utf8');
|
|
110
|
+
const state = yaml.parse(content);
|
|
111
|
+
if (!state || typeof state !== 'object') {
|
|
112
|
+
throw fireError('State file is empty or invalid.', 'CHECKPOINT_020', 'Check state.yaml format.');
|
|
113
|
+
}
|
|
114
|
+
return state;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err.code && err.code.startsWith('CHECKPOINT_')) throw err;
|
|
117
|
+
throw fireError(
|
|
118
|
+
`Failed to read state file: ${err.message}`,
|
|
119
|
+
'CHECKPOINT_021',
|
|
120
|
+
'Check file permissions and YAML syntax.'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function writeState(statePath, state) {
|
|
126
|
+
try {
|
|
127
|
+
fs.writeFileSync(statePath, yaml.stringify(state));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw fireError(
|
|
130
|
+
`Failed to write state file: ${err.message}`,
|
|
131
|
+
'CHECKPOINT_022',
|
|
132
|
+
'Check file permissions and disk space.'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function updateCheckpoint(rootPath, runId, checkpointState, options = {}) {
|
|
138
|
+
const normalizedState = validateInputs(rootPath, runId, checkpointState);
|
|
139
|
+
const { statePath } = validateFireProject(rootPath);
|
|
140
|
+
const state = readState(statePath);
|
|
141
|
+
|
|
142
|
+
const activeRuns = state.runs?.active || [];
|
|
143
|
+
const runIndex = activeRuns.findIndex((run) => run.id === runId);
|
|
144
|
+
if (runIndex === -1) {
|
|
145
|
+
throw fireError(
|
|
146
|
+
`Run "${runId}" not found in active runs.`,
|
|
147
|
+
'CHECKPOINT_030',
|
|
148
|
+
'The run may have already been completed or was never started.'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const activeRun = activeRuns[runIndex];
|
|
153
|
+
const workItems = Array.isArray(activeRun.work_items) ? activeRun.work_items : [];
|
|
154
|
+
const targetItemId = options.itemId || activeRun.current_item;
|
|
155
|
+
if (!targetItemId) {
|
|
156
|
+
throw fireError(
|
|
157
|
+
`Run "${runId}" has no current item.`,
|
|
158
|
+
'CHECKPOINT_031',
|
|
159
|
+
'Specify --item=<workItemId> explicitly.'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const itemIndex = workItems.findIndex((item) => item.id === targetItemId);
|
|
164
|
+
if (itemIndex === -1) {
|
|
165
|
+
throw fireError(
|
|
166
|
+
`Work item "${targetItemId}" not found in run "${runId}".`,
|
|
167
|
+
'CHECKPOINT_032',
|
|
168
|
+
'Check the work item ID or run state.'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const item = workItems[itemIndex];
|
|
173
|
+
const previousState = item.checkpoint_state || null;
|
|
174
|
+
item.checkpoint_state = normalizedState;
|
|
175
|
+
|
|
176
|
+
if (typeof options.checkpoint === 'string' && options.checkpoint.trim() !== '') {
|
|
177
|
+
item.current_checkpoint = options.checkpoint.trim();
|
|
178
|
+
} else if (!item.current_checkpoint && (normalizedState === 'awaiting_approval' || normalizedState === 'approved')) {
|
|
179
|
+
item.current_checkpoint = 'plan';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!item.current_phase && normalizedState === 'awaiting_approval') {
|
|
183
|
+
item.current_phase = 'plan';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
activeRun.work_items = workItems;
|
|
187
|
+
state.runs.active[runIndex] = activeRun;
|
|
188
|
+
writeState(statePath, state);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
success: true,
|
|
192
|
+
runId,
|
|
193
|
+
workItemId: targetItemId,
|
|
194
|
+
checkpointState: normalizedState,
|
|
195
|
+
previousCheckpointState: previousState,
|
|
196
|
+
currentCheckpoint: item.current_checkpoint || null
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseOptions(argv) {
|
|
201
|
+
const options = {};
|
|
202
|
+
for (const arg of argv) {
|
|
203
|
+
if (arg.startsWith('--item=')) {
|
|
204
|
+
options.itemId = arg.slice('--item='.length);
|
|
205
|
+
} else if (arg.startsWith('--checkpoint=')) {
|
|
206
|
+
options.checkpoint = arg.slice('--checkpoint='.length);
|
|
207
|
+
} else {
|
|
208
|
+
throw fireError(
|
|
209
|
+
`Unknown option: ${arg}`,
|
|
210
|
+
'CHECKPOINT_033',
|
|
211
|
+
'Use --item=<workItemId> or --checkpoint=<name>.'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return options;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function printUsage() {
|
|
219
|
+
console.error('Usage:');
|
|
220
|
+
console.error(' node update-checkpoint.cjs <rootPath> <runId> <checkpointState> [--item=<workItemId>] [--checkpoint=<name>]');
|
|
221
|
+
console.error('');
|
|
222
|
+
console.error('checkpointState:');
|
|
223
|
+
console.error(` ${VALID_STATES.join(', ')}`);
|
|
224
|
+
console.error('');
|
|
225
|
+
console.error('Examples:');
|
|
226
|
+
console.error(' node update-checkpoint.cjs /project run-fabriqa-2026-001 awaiting_approval --checkpoint=plan');
|
|
227
|
+
console.error(' node update-checkpoint.cjs /project run-fabriqa-2026-001 approved');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (require.main === module) {
|
|
231
|
+
const args = process.argv.slice(2);
|
|
232
|
+
if (args.length < 3) {
|
|
233
|
+
printUsage();
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const [rootPath, runId, checkpointState, ...optionArgs] = args;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const options = parseOptions(optionArgs);
|
|
241
|
+
const result = updateCheckpoint(rootPath, runId, checkpointState, options);
|
|
242
|
+
console.log(JSON.stringify(result, null, 2));
|
|
243
|
+
process.exit(0);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.error(err.message);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
VALID_STATES,
|
|
252
|
+
normalizeCheckpointState,
|
|
253
|
+
updateCheckpoint
|
|
254
|
+
};
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* node update-phase.cjs <rootPath> <runId> <phase>
|
|
11
11
|
*
|
|
12
12
|
* Examples:
|
|
13
|
-
* node update-phase.cjs /project run-001 execute
|
|
14
|
-
* node update-phase.cjs /project run-001 test
|
|
15
|
-
* node update-phase.cjs /project run-001 review
|
|
13
|
+
* node update-phase.cjs /project run-fabriqa-2026-001 execute
|
|
14
|
+
* node update-phase.cjs /project run-fabriqa-2026-001 test
|
|
15
|
+
* node update-phase.cjs /project run-fabriqa-2026-001 review
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
@@ -171,6 +171,17 @@ function updatePhase(rootPath, runId, phase) {
|
|
|
171
171
|
if (item.id === currentItemId) {
|
|
172
172
|
previousPhase = item.current_phase || 'plan';
|
|
173
173
|
item.current_phase = phase;
|
|
174
|
+
const mode = String(item.mode || '').toLowerCase();
|
|
175
|
+
const isApprovalMode = mode === 'confirm' || mode === 'validate';
|
|
176
|
+
|
|
177
|
+
if (isApprovalMode && phase !== 'plan') {
|
|
178
|
+
item.checkpoint_state = 'approved';
|
|
179
|
+
if (!item.current_checkpoint) {
|
|
180
|
+
item.current_checkpoint = 'plan';
|
|
181
|
+
}
|
|
182
|
+
} else if (!item.checkpoint_state) {
|
|
183
|
+
item.checkpoint_state = 'none';
|
|
184
|
+
}
|
|
174
185
|
updated = true;
|
|
175
186
|
break;
|
|
176
187
|
}
|
|
@@ -208,12 +219,12 @@ function printUsage() {
|
|
|
208
219
|
console.error('');
|
|
209
220
|
console.error('Arguments:');
|
|
210
221
|
console.error(' rootPath - Project root directory');
|
|
211
|
-
console.error(' runId - Run ID (e.g., run-001)');
|
|
222
|
+
console.error(' runId - Run ID (e.g., run-fabriqa-2026-001)');
|
|
212
223
|
console.error(' phase - New phase: plan, execute, test, review');
|
|
213
224
|
console.error('');
|
|
214
225
|
console.error('Examples:');
|
|
215
|
-
console.error(' node update-phase.cjs /project run-001 execute');
|
|
216
|
-
console.error(' node update-phase.cjs /project run-001 test');
|
|
226
|
+
console.error(' node update-phase.cjs /project run-fabriqa-2026-001 execute');
|
|
227
|
+
console.error(' node update-phase.cjs /project run-fabriqa-2026-001 test');
|
|
217
228
|
}
|
|
218
229
|
|
|
219
230
|
if (require.main === module) {
|