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.
Files changed (42) hide show
  1. package/README.md +15 -0
  2. package/bin/cli.js +15 -1
  3. package/flows/fire/agents/builder/agent.md +2 -2
  4. package/flows/fire/agents/builder/skills/code-review/SKILL.md +1 -1
  5. package/flows/fire/agents/builder/skills/run-execute/SKILL.md +16 -7
  6. package/flows/fire/agents/builder/skills/run-execute/scripts/complete-run.cjs +22 -3
  7. package/flows/fire/agents/builder/skills/run-execute/scripts/init-run.cjs +63 -20
  8. package/flows/fire/agents/builder/skills/run-execute/scripts/update-checkpoint.cjs +254 -0
  9. package/flows/fire/agents/builder/skills/run-execute/scripts/update-phase.cjs +17 -6
  10. package/flows/fire/agents/builder/skills/run-status/SKILL.md +1 -1
  11. package/flows/fire/agents/orchestrator/agent.md +1 -1
  12. package/flows/fire/agents/orchestrator/skills/status/SKILL.md +2 -2
  13. package/flows/fire/memory-bank.yaml +4 -4
  14. package/lib/dashboard/aidlc/parser.js +581 -0
  15. package/lib/dashboard/fire/model.js +382 -0
  16. package/lib/dashboard/fire/parser.js +470 -0
  17. package/lib/dashboard/flow-detect.js +86 -0
  18. package/lib/dashboard/git/changes.js +362 -0
  19. package/lib/dashboard/git/worktrees.js +248 -0
  20. package/lib/dashboard/index.js +709 -0
  21. package/lib/dashboard/runtime/watch-runtime.js +122 -0
  22. package/lib/dashboard/simple/parser.js +293 -0
  23. package/lib/dashboard/tui/app.js +1675 -0
  24. package/lib/dashboard/tui/components/error-banner.js +35 -0
  25. package/lib/dashboard/tui/components/header.js +60 -0
  26. package/lib/dashboard/tui/components/help-footer.js +15 -0
  27. package/lib/dashboard/tui/components/stats-strip.js +35 -0
  28. package/lib/dashboard/tui/file-entries.js +383 -0
  29. package/lib/dashboard/tui/flow-builders.js +991 -0
  30. package/lib/dashboard/tui/git-builders.js +218 -0
  31. package/lib/dashboard/tui/helpers.js +236 -0
  32. package/lib/dashboard/tui/overlays.js +242 -0
  33. package/lib/dashboard/tui/preview.js +220 -0
  34. package/lib/dashboard/tui/renderer.js +76 -0
  35. package/lib/dashboard/tui/row-builders.js +797 -0
  36. package/lib/dashboard/tui/sections.js +45 -0
  37. package/lib/dashboard/tui/store.js +44 -0
  38. package/lib/dashboard/tui/views/overview-view.js +61 -0
  39. package/lib/dashboard/tui/views/runs-view.js +93 -0
  40. package/lib/dashboard/tui/worktree-builders.js +229 -0
  41. package/lib/installers/CodexInstaller.js +72 -1
  42. 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.parse(process.argv);
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-XXX">
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">
@@ -42,7 +42,7 @@ Review code written during a run, auto-fix no-brainer issues, and suggest improv
42
42
  - path: src/routes/index.ts
43
43
  changes: Added login route
44
44
 
45
- run_id: run-001
45
+ run_id: run-fabriqa-2026-001
46
46
  intent_id: user-auth
47
47
  ```
48
48
 
@@ -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 both history and file system)
155
+ // Run ID Generation (CRITICAL - checks state and file system)
156
156
  // =============================================================================
157
157
 
158
- function generateRunId(runsPath, state) {
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
- // Source 1: Get max from state.yaml runs.completed history
165
- let maxFromHistory = 0;
166
- if (state.runs && Array.isArray(state.runs.completed)) {
167
- for (const run of state.runs.completed) {
168
- if (run.id) {
169
- const match = run.id.match(/^run-(\d+)$/);
170
- if (match) {
171
- const num = parseInt(match[1], 10);
172
- if (num > maxFromHistory) maxFromHistory = num;
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
- if (/^run-\d{3,}$/.test(entry)) {
184
- const num = parseInt(entry.replace('run-', ''), 10);
185
- if (num > maxFromFileSystem) maxFromFileSystem = num;
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(maxFromHistory, maxFromFileSystem);
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
- return ` - id: ${item.id}\n intent: ${item.intent}\n mode: ${item.mode}\n status: ${status}`;
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) {
@@ -58,7 +58,7 @@ Display current run status and progress.
58
58
  <example_output>
59
59
 
60
60
  ```
61
- ## Run Status: run-003
61
+ ## Run Status: run-fabriqa-2026-003
62
62
 
63
63
  **Work Item**: Add session management
64
64
  **Intent**: User Authentication