specsmd 0.1.45 → 0.1.46
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/flows/fire/agents/builder/skills/run-execute/SKILL.md +9 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/complete-run.cjs +19 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/init-run.cjs +9 -1
- 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 +11 -0
- package/lib/dashboard/fire/model.js +53 -4
- package/lib/dashboard/fire/parser.js +91 -7
- package/lib/dashboard/tui/app.js +106 -40
- package/package.json +1 -1
|
@@ -204,6 +204,8 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
204
204
|
<action>Save plan IMMEDIATELY using template: templates/plan.md.hbs</action>
|
|
205
205
|
<action>Write to: .specs-fire/runs/{run-id}/plan.md</action>
|
|
206
206
|
<output>Plan saved to: .specs-fire/runs/{run-id}/plan.md</output>
|
|
207
|
+
<action>Mark checkpoint as waiting:</action>
|
|
208
|
+
<code>node scripts/update-checkpoint.cjs {rootPath} {runId} awaiting_approval --checkpoint=plan</code>
|
|
207
209
|
|
|
208
210
|
<checkpoint>
|
|
209
211
|
<template_output section="plan">
|
|
@@ -232,6 +234,8 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
232
234
|
<action>Update plan.md with changes</action>
|
|
233
235
|
<goto step="3b"/>
|
|
234
236
|
</check>
|
|
237
|
+
<action>Mark checkpoint approved:</action>
|
|
238
|
+
<code>node scripts/update-checkpoint.cjs {rootPath} {runId} approved --checkpoint=plan</code>
|
|
235
239
|
<goto step="5"/>
|
|
236
240
|
</step>
|
|
237
241
|
|
|
@@ -242,6 +246,8 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
242
246
|
<action>Write to: .specs-fire/runs/{run-id}/plan.md</action>
|
|
243
247
|
<action>Include reference to design doc in plan</action>
|
|
244
248
|
<output>Plan saved to: .specs-fire/runs/{run-id}/plan.md</output>
|
|
249
|
+
<action>Mark checkpoint as waiting:</action>
|
|
250
|
+
<code>node scripts/update-checkpoint.cjs {rootPath} {runId} awaiting_approval --checkpoint=plan</code>
|
|
245
251
|
|
|
246
252
|
<checkpoint>
|
|
247
253
|
<template_output section="plan">
|
|
@@ -270,6 +276,8 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
270
276
|
<action>Update plan.md with changes</action>
|
|
271
277
|
<goto step="3c"/>
|
|
272
278
|
</check>
|
|
279
|
+
<action>Mark checkpoint approved:</action>
|
|
280
|
+
<code>node scripts/update-checkpoint.cjs {rootPath} {runId} approved --checkpoint=plan</code>
|
|
273
281
|
<goto step="5"/>
|
|
274
282
|
</step>
|
|
275
283
|
|
|
@@ -520,6 +528,7 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
520
528
|
| Script | Purpose | Usage |
|
|
521
529
|
|--------|---------|-------|
|
|
522
530
|
| `scripts/init-run.cjs` | Initialize run record and folder | Creates run.md with all work items |
|
|
531
|
+
| `scripts/update-checkpoint.cjs` | Mark approval gate state for active item | `awaiting_approval` / `approved` |
|
|
523
532
|
| `scripts/update-phase.cjs` | Update current work item's phase | `node scripts/update-phase.cjs {rootPath} {runId} {phase}` |
|
|
524
533
|
| `scripts/complete-run.cjs` | Finalize run and update state | `--complete-item` or `--complete-run` |
|
|
525
534
|
|
|
@@ -305,6 +305,9 @@ function updateRunLog(runLogPath, activeRun, params, completedTime, isFullComple
|
|
|
305
305
|
intent: item.intent,
|
|
306
306
|
mode: item.mode,
|
|
307
307
|
status: item.status,
|
|
308
|
+
current_phase: item.current_phase || null,
|
|
309
|
+
checkpoint_state: item.checkpoint_state || null,
|
|
310
|
+
current_checkpoint: item.current_checkpoint || null,
|
|
308
311
|
}));
|
|
309
312
|
}
|
|
310
313
|
|
|
@@ -427,6 +430,12 @@ function completeCurrentItem(rootPath, runId, params = {}) {
|
|
|
427
430
|
if (workItems[i].id === currentItemId) {
|
|
428
431
|
workItems[i].status = 'completed';
|
|
429
432
|
workItems[i].completed_at = completedTime;
|
|
433
|
+
if (workItems[i].mode === 'confirm' || workItems[i].mode === 'validate') {
|
|
434
|
+
workItems[i].checkpoint_state = 'approved';
|
|
435
|
+
workItems[i].current_checkpoint = workItems[i].current_checkpoint || 'plan';
|
|
436
|
+
} else {
|
|
437
|
+
workItems[i].checkpoint_state = workItems[i].checkpoint_state || 'not_required';
|
|
438
|
+
}
|
|
430
439
|
currentItemIndex = i;
|
|
431
440
|
break;
|
|
432
441
|
}
|
|
@@ -446,6 +455,10 @@ function completeCurrentItem(rootPath, runId, params = {}) {
|
|
|
446
455
|
if (workItems[i].status === 'pending') {
|
|
447
456
|
workItems[i].status = 'in_progress';
|
|
448
457
|
workItems[i].current_phase = 'plan';
|
|
458
|
+
workItems[i].checkpoint_state = 'none';
|
|
459
|
+
workItems[i].current_checkpoint = (workItems[i].mode === 'confirm' || workItems[i].mode === 'validate')
|
|
460
|
+
? 'plan'
|
|
461
|
+
: null;
|
|
449
462
|
nextItem = workItems[i];
|
|
450
463
|
break;
|
|
451
464
|
}
|
|
@@ -543,6 +556,12 @@ function completeRun(rootPath, runId, params = {}) {
|
|
|
543
556
|
item.status = 'completed';
|
|
544
557
|
item.completed_at = completedTime;
|
|
545
558
|
}
|
|
559
|
+
if (item.mode === 'confirm' || item.mode === 'validate') {
|
|
560
|
+
item.checkpoint_state = 'approved';
|
|
561
|
+
item.current_checkpoint = item.current_checkpoint || 'plan';
|
|
562
|
+
} else {
|
|
563
|
+
item.checkpoint_state = item.checkpoint_state || 'not_required';
|
|
564
|
+
}
|
|
546
565
|
}
|
|
547
566
|
|
|
548
567
|
activeRun.work_items = workItems;
|
|
@@ -233,7 +233,11 @@ function createRunLog(runPath, runId, workItems, scope, startTime) {
|
|
|
233
233
|
// Format work items for run.md
|
|
234
234
|
const workItemsList = workItems.map((item, index) => {
|
|
235
235
|
const status = index === 0 ? 'in_progress' : 'pending';
|
|
236
|
-
|
|
236
|
+
const currentPhase = index === 0 ? 'plan' : 'null';
|
|
237
|
+
const currentCheckpoint = index === 0 && (item.mode === 'confirm' || item.mode === 'validate')
|
|
238
|
+
? 'plan'
|
|
239
|
+
: 'null';
|
|
240
|
+
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
241
|
}).join('\n');
|
|
238
242
|
|
|
239
243
|
const currentItem = workItems[0];
|
|
@@ -346,6 +350,10 @@ function initRun(rootPath, workItems, scope) {
|
|
|
346
350
|
mode: item.mode,
|
|
347
351
|
status: index === 0 ? 'in_progress' : 'pending',
|
|
348
352
|
current_phase: index === 0 ? 'plan' : null,
|
|
353
|
+
checkpoint_state: 'none',
|
|
354
|
+
current_checkpoint: (index === 0 && (item.mode === 'confirm' || item.mode === 'validate'))
|
|
355
|
+
? 'plan'
|
|
356
|
+
: null,
|
|
349
357
|
}));
|
|
350
358
|
|
|
351
359
|
// 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-001 awaiting_approval --checkpoint=plan
|
|
13
|
+
* node update-checkpoint.cjs /project run-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-001 awaiting_approval --checkpoint=plan');
|
|
227
|
+
console.error(' node update-checkpoint.cjs /project run-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
|
+
};
|
|
@@ -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
|
}
|
|
@@ -78,6 +78,15 @@ function normalizeTimestamp(value) {
|
|
|
78
78
|
return String(value);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function normalizeCheckpointState(value) {
|
|
82
|
+
if (typeof value !== 'string') {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const normalized = value.toLowerCase().trim().replace(/[\s-]+/g, '_');
|
|
87
|
+
return normalized === '' ? undefined : normalized;
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
function parseDependencies(raw) {
|
|
82
91
|
if (Array.isArray(raw)) {
|
|
83
92
|
return raw.filter((item) => typeof item === 'string' && item.trim() !== '');
|
|
@@ -97,7 +106,9 @@ function normalizeRunWorkItem(raw, fallbackIntentId = '') {
|
|
|
97
106
|
intentId: fallbackIntentId,
|
|
98
107
|
mode: 'confirm',
|
|
99
108
|
status: 'pending',
|
|
100
|
-
currentPhase: undefined
|
|
109
|
+
currentPhase: undefined,
|
|
110
|
+
checkpointState: undefined,
|
|
111
|
+
currentCheckpoint: undefined
|
|
101
112
|
};
|
|
102
113
|
}
|
|
103
114
|
|
|
@@ -107,7 +118,9 @@ function normalizeRunWorkItem(raw, fallbackIntentId = '') {
|
|
|
107
118
|
intentId: fallbackIntentId,
|
|
108
119
|
mode: 'confirm',
|
|
109
120
|
status: 'pending',
|
|
110
|
-
currentPhase: undefined
|
|
121
|
+
currentPhase: undefined,
|
|
122
|
+
checkpointState: undefined,
|
|
123
|
+
currentCheckpoint: undefined
|
|
111
124
|
};
|
|
112
125
|
}
|
|
113
126
|
|
|
@@ -121,13 +134,27 @@ function normalizeRunWorkItem(raw, fallbackIntentId = '') {
|
|
|
121
134
|
const currentPhase = typeof raw.current_phase === 'string'
|
|
122
135
|
? raw.current_phase
|
|
123
136
|
: (typeof raw.currentPhase === 'string' ? raw.currentPhase : undefined);
|
|
137
|
+
const checkpointState = typeof raw.checkpoint_state === 'string'
|
|
138
|
+
? raw.checkpoint_state
|
|
139
|
+
: (typeof raw.checkpointState === 'string'
|
|
140
|
+
? raw.checkpointState
|
|
141
|
+
: (typeof raw.approval_state === 'string'
|
|
142
|
+
? raw.approval_state
|
|
143
|
+
: (typeof raw.approvalState === 'string' ? raw.approvalState : undefined)));
|
|
144
|
+
const currentCheckpoint = typeof raw.current_checkpoint === 'string'
|
|
145
|
+
? raw.current_checkpoint
|
|
146
|
+
: (typeof raw.currentCheckpoint === 'string'
|
|
147
|
+
? raw.currentCheckpoint
|
|
148
|
+
: (typeof raw.checkpoint === 'string' ? raw.checkpoint : undefined));
|
|
124
149
|
|
|
125
150
|
return {
|
|
126
151
|
id,
|
|
127
152
|
intentId,
|
|
128
153
|
mode,
|
|
129
154
|
status,
|
|
130
|
-
currentPhase
|
|
155
|
+
currentPhase,
|
|
156
|
+
checkpointState,
|
|
157
|
+
currentCheckpoint
|
|
131
158
|
};
|
|
132
159
|
}
|
|
133
160
|
|
|
@@ -206,7 +233,18 @@ function normalizeState(rawState) {
|
|
|
206
233
|
currentItem: typeof run.current_item === 'string'
|
|
207
234
|
? run.current_item
|
|
208
235
|
: (typeof run.currentItem === 'string' ? run.currentItem : ''),
|
|
209
|
-
started: normalizeTimestamp(run.started) || ''
|
|
236
|
+
started: normalizeTimestamp(run.started) || '',
|
|
237
|
+
checkpointState: normalizeCheckpointState(
|
|
238
|
+
run.checkpoint_state
|
|
239
|
+
|| run.checkpointState
|
|
240
|
+
|| run.approval_state
|
|
241
|
+
|| run.approvalState
|
|
242
|
+
),
|
|
243
|
+
currentCheckpoint: typeof run.current_checkpoint === 'string'
|
|
244
|
+
? run.current_checkpoint
|
|
245
|
+
: (typeof run.currentCheckpoint === 'string'
|
|
246
|
+
? run.currentCheckpoint
|
|
247
|
+
: (typeof run.checkpoint === 'string' ? run.checkpoint : undefined))
|
|
210
248
|
};
|
|
211
249
|
}).filter(Boolean);
|
|
212
250
|
|
|
@@ -221,6 +259,17 @@ function normalizeState(rawState) {
|
|
|
221
259
|
return {
|
|
222
260
|
id: typeof run.id === 'string' ? run.id : '',
|
|
223
261
|
workItems: workItemsRaw.map((item) => normalizeRunWorkItem(item, fallbackIntentId)).filter((item) => item.id !== ''),
|
|
262
|
+
checkpointState: normalizeCheckpointState(
|
|
263
|
+
run.checkpoint_state
|
|
264
|
+
|| run.checkpointState
|
|
265
|
+
|| run.approval_state
|
|
266
|
+
|| run.approvalState
|
|
267
|
+
),
|
|
268
|
+
currentCheckpoint: typeof run.current_checkpoint === 'string'
|
|
269
|
+
? run.current_checkpoint
|
|
270
|
+
: (typeof run.currentCheckpoint === 'string'
|
|
271
|
+
? run.currentCheckpoint
|
|
272
|
+
: (typeof run.checkpoint === 'string' ? run.checkpoint : undefined)),
|
|
224
273
|
completed: normalizeTimestamp(run.completed) || ''
|
|
225
274
|
};
|
|
226
275
|
}).filter(Boolean);
|
|
@@ -65,6 +65,21 @@ function listMarkdownFiles(dirPath) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function getFirstStringValue(record, keys) {
|
|
69
|
+
if (!record || typeof record !== 'object') {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
const value = record[key];
|
|
75
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
function parseRunLog(runLogPath) {
|
|
69
84
|
const content = readFileSafe(runLogPath);
|
|
70
85
|
if (!content) {
|
|
@@ -73,15 +88,40 @@ function parseRunLog(runLogPath) {
|
|
|
73
88
|
workItems: [],
|
|
74
89
|
currentItem: null,
|
|
75
90
|
startedAt: undefined,
|
|
76
|
-
completedAt: undefined
|
|
91
|
+
completedAt: undefined,
|
|
92
|
+
checkpointState: undefined,
|
|
93
|
+
currentCheckpoint: undefined
|
|
77
94
|
};
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
const frontmatter = parseFrontmatter(content);
|
|
98
|
+
const currentItem = getFirstStringValue(frontmatter, ['current_item', 'currentItem', 'work_item', 'workItem']);
|
|
99
|
+
const itemMode = getFirstStringValue(frontmatter, ['mode']);
|
|
100
|
+
const itemStatus = getFirstStringValue(frontmatter, ['status']);
|
|
101
|
+
const itemPhase = getFirstStringValue(frontmatter, ['current_phase', 'currentPhase']);
|
|
102
|
+
const itemCheckpointState = getFirstStringValue(frontmatter, [
|
|
103
|
+
'checkpoint_state',
|
|
104
|
+
'checkpointState',
|
|
105
|
+
'approval_state',
|
|
106
|
+
'approvalState'
|
|
107
|
+
]);
|
|
108
|
+
const itemCheckpoint = getFirstStringValue(frontmatter, ['current_checkpoint', 'currentCheckpoint', 'checkpoint']);
|
|
109
|
+
|
|
81
110
|
const workItemsRaw = Array.isArray(frontmatter.work_items)
|
|
82
111
|
? frontmatter.work_items
|
|
83
112
|
: (Array.isArray(frontmatter.workItems) ? frontmatter.workItems : []);
|
|
84
113
|
|
|
114
|
+
if (workItemsRaw.length === 0 && typeof currentItem === 'string' && currentItem !== '') {
|
|
115
|
+
workItemsRaw.push({
|
|
116
|
+
id: currentItem,
|
|
117
|
+
mode: itemMode,
|
|
118
|
+
status: itemStatus,
|
|
119
|
+
current_phase: itemPhase,
|
|
120
|
+
checkpoint_state: itemCheckpointState,
|
|
121
|
+
current_checkpoint: itemCheckpoint
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
85
125
|
const workItems = workItemsRaw
|
|
86
126
|
.map((item) => normalizeRunWorkItem(item))
|
|
87
127
|
.filter((item) => item.id !== '');
|
|
@@ -89,16 +129,54 @@ function parseRunLog(runLogPath) {
|
|
|
89
129
|
return {
|
|
90
130
|
scope: normalizeScope(frontmatter.scope),
|
|
91
131
|
workItems,
|
|
92
|
-
currentItem:
|
|
93
|
-
? frontmatter.current_item
|
|
94
|
-
: (typeof frontmatter.currentItem === 'string' ? frontmatter.currentItem : null),
|
|
132
|
+
currentItem: currentItem || null,
|
|
95
133
|
startedAt: typeof frontmatter.started === 'string' ? frontmatter.started : undefined,
|
|
96
134
|
completedAt: typeof frontmatter.completed === 'string'
|
|
97
135
|
? frontmatter.completed
|
|
98
|
-
: undefined
|
|
136
|
+
: undefined,
|
|
137
|
+
checkpointState: itemCheckpointState,
|
|
138
|
+
currentCheckpoint: itemCheckpoint
|
|
99
139
|
};
|
|
100
140
|
}
|
|
101
141
|
|
|
142
|
+
function mergeRunWorkItems(primaryItems, fallbackItems) {
|
|
143
|
+
const primary = Array.isArray(primaryItems) ? primaryItems : [];
|
|
144
|
+
const fallback = Array.isArray(fallbackItems) ? fallbackItems : [];
|
|
145
|
+
|
|
146
|
+
if (primary.length === 0) {
|
|
147
|
+
return fallback;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (fallback.length === 0) {
|
|
151
|
+
return primary;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const fallbackById = new Map(fallback.map((item) => [item.id, item]));
|
|
155
|
+
const merged = primary.map((item) => {
|
|
156
|
+
const fallbackItem = fallbackById.get(item.id);
|
|
157
|
+
if (!fallbackItem) {
|
|
158
|
+
return item;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
...fallbackItem,
|
|
163
|
+
...item,
|
|
164
|
+
checkpointState: item.checkpointState || fallbackItem.checkpointState,
|
|
165
|
+
currentCheckpoint: item.currentCheckpoint || fallbackItem.currentCheckpoint,
|
|
166
|
+
currentPhase: item.currentPhase || fallbackItem.currentPhase
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const knownIds = new Set(merged.map((item) => item.id));
|
|
171
|
+
for (const fallbackItem of fallback) {
|
|
172
|
+
if (!knownIds.has(fallbackItem.id)) {
|
|
173
|
+
merged.push(fallbackItem);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return merged;
|
|
178
|
+
}
|
|
179
|
+
|
|
102
180
|
function scanWorkItems(intentPath, intentId, stateWorkItems, warnings) {
|
|
103
181
|
const workItemsPath = path.join(intentPath, 'work-items');
|
|
104
182
|
const fileWorkItemIds = listMarkdownFiles(workItemsPath)
|
|
@@ -202,11 +280,15 @@ function scanRuns(rootPath, normalizedState) {
|
|
|
202
280
|
const stateActiveRun = stateActiveMap.get(runId);
|
|
203
281
|
const stateCompletedRun = stateCompletedMap.get(runId);
|
|
204
282
|
|
|
205
|
-
const
|
|
283
|
+
const stateRunWorkItems = (stateActiveRun?.workItems && stateActiveRun.workItems.length > 0)
|
|
206
284
|
? stateActiveRun.workItems
|
|
207
285
|
: ((stateCompletedRun?.workItems && stateCompletedRun.workItems.length > 0)
|
|
208
286
|
? stateCompletedRun.workItems
|
|
209
|
-
:
|
|
287
|
+
: []);
|
|
288
|
+
const workItems = mergeRunWorkItems(
|
|
289
|
+
stateRunWorkItems.length > 0 ? stateRunWorkItems : parsedRunLog.workItems,
|
|
290
|
+
parsedRunLog.workItems
|
|
291
|
+
);
|
|
210
292
|
|
|
211
293
|
const completedAt = stateCompletedRun?.completed || parsedRunLog.completedAt || undefined;
|
|
212
294
|
|
|
@@ -215,6 +297,8 @@ function scanRuns(rootPath, normalizedState) {
|
|
|
215
297
|
scope: stateActiveRun?.scope || parsedRunLog.scope || 'single',
|
|
216
298
|
workItems,
|
|
217
299
|
currentItem: stateActiveRun?.currentItem || parsedRunLog.currentItem || null,
|
|
300
|
+
checkpointState: stateActiveRun?.checkpointState || parsedRunLog.checkpointState,
|
|
301
|
+
currentCheckpoint: stateActiveRun?.currentCheckpoint || parsedRunLog.currentCheckpoint,
|
|
218
302
|
folderPath,
|
|
219
303
|
startedAt: stateActiveRun?.started || parsedRunLog.startedAt || '',
|
|
220
304
|
completedAt: completedAt === 'null' ? undefined : completedAt,
|
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -310,62 +310,138 @@ function extractFrontmatterValue(frontmatterBlock, key) {
|
|
|
310
310
|
.trim();
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
|
|
313
|
+
const FIRE_AWAITING_APPROVAL_STATES = new Set([
|
|
314
|
+
'awaiting_approval',
|
|
315
|
+
'waiting',
|
|
316
|
+
'pending_approval',
|
|
317
|
+
'approval_needed',
|
|
318
|
+
'approval_required',
|
|
319
|
+
'checkpoint_pending'
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
const FIRE_APPROVED_STATES = new Set([
|
|
323
|
+
'approved',
|
|
324
|
+
'confirmed',
|
|
325
|
+
'accepted',
|
|
326
|
+
'resumed',
|
|
327
|
+
'done',
|
|
328
|
+
'completed',
|
|
329
|
+
'cleared',
|
|
330
|
+
'none',
|
|
331
|
+
'not_required',
|
|
332
|
+
'skipped'
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
function parseFirePlanCheckpointMetadata(run) {
|
|
314
336
|
if (!run || typeof run.folderPath !== 'string' || run.folderPath.trim() === '') {
|
|
315
|
-
return { hasPlan: false,
|
|
337
|
+
return { hasPlan: false, checkpointState: null, checkpoint: null };
|
|
316
338
|
}
|
|
317
339
|
|
|
318
340
|
const planPath = path.join(run.folderPath, 'plan.md');
|
|
319
341
|
if (!fileExists(planPath)) {
|
|
320
|
-
return { hasPlan: false,
|
|
342
|
+
return { hasPlan: false, checkpointState: null, checkpoint: null };
|
|
321
343
|
}
|
|
322
344
|
|
|
323
345
|
const content = readFileTextSafe(planPath);
|
|
324
346
|
const frontmatter = extractFrontmatterBlock(content);
|
|
325
347
|
if (!frontmatter) {
|
|
326
|
-
return { hasPlan: true,
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
''
|
|
333
|
-
'
|
|
334
|
-
'
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
'
|
|
338
|
-
'
|
|
339
|
-
|
|
340
|
-
'assumed-from-user-n'
|
|
341
|
-
]);
|
|
342
|
-
const normalizedApprovedAt = normalizeToken(approvedAt || '');
|
|
348
|
+
return { hasPlan: true, checkpointState: null, checkpoint: null };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const checkpointState = normalizeToken(
|
|
352
|
+
extractFrontmatterValue(frontmatter, 'checkpoint_state')
|
|
353
|
+
|| extractFrontmatterValue(frontmatter, 'checkpointState')
|
|
354
|
+
|| extractFrontmatterValue(frontmatter, 'approval_state')
|
|
355
|
+
|| extractFrontmatterValue(frontmatter, 'approvalState')
|
|
356
|
+
|| ''
|
|
357
|
+
) || null;
|
|
358
|
+
const checkpoint = extractFrontmatterValue(frontmatter, 'current_checkpoint')
|
|
359
|
+
|| extractFrontmatterValue(frontmatter, 'currentCheckpoint')
|
|
360
|
+
|| extractFrontmatterValue(frontmatter, 'checkpoint')
|
|
361
|
+
|| null;
|
|
343
362
|
|
|
344
363
|
return {
|
|
345
364
|
hasPlan: true,
|
|
346
|
-
|
|
347
|
-
checkpoint
|
|
365
|
+
checkpointState,
|
|
366
|
+
checkpoint
|
|
348
367
|
};
|
|
349
368
|
}
|
|
350
369
|
|
|
351
|
-
function
|
|
370
|
+
function resolveFireApprovalState(run, currentWorkItem) {
|
|
371
|
+
const itemState = normalizeToken(
|
|
372
|
+
currentWorkItem?.checkpointState
|
|
373
|
+
|| currentWorkItem?.checkpoint_state
|
|
374
|
+
|| currentWorkItem?.approvalState
|
|
375
|
+
|| currentWorkItem?.approval_state
|
|
376
|
+
|| ''
|
|
377
|
+
);
|
|
378
|
+
const runState = normalizeToken(
|
|
379
|
+
run?.checkpointState
|
|
380
|
+
|| run?.checkpoint_state
|
|
381
|
+
|| run?.approvalState
|
|
382
|
+
|| run?.approval_state
|
|
383
|
+
|| ''
|
|
384
|
+
);
|
|
385
|
+
const planState = parseFirePlanCheckpointMetadata(run);
|
|
386
|
+
const state = itemState || runState || planState.checkpointState || null;
|
|
387
|
+
const checkpoint = currentWorkItem?.currentCheckpoint
|
|
388
|
+
|| currentWorkItem?.current_checkpoint
|
|
389
|
+
|| run?.currentCheckpoint
|
|
390
|
+
|| run?.current_checkpoint
|
|
391
|
+
|| planState.checkpoint
|
|
392
|
+
|| null;
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
state,
|
|
396
|
+
checkpoint,
|
|
397
|
+
source: itemState
|
|
398
|
+
? 'item-state'
|
|
399
|
+
: (runState
|
|
400
|
+
? 'run-state'
|
|
401
|
+
: (planState.checkpointState ? 'plan-frontmatter' : null))
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getFireRunApprovalGate(run, currentWorkItem) {
|
|
352
406
|
const mode = normalizeToken(currentWorkItem?.mode);
|
|
353
407
|
const status = normalizeToken(currentWorkItem?.status);
|
|
354
408
|
if (!['confirm', 'validate'].includes(mode) || status !== 'in_progress') {
|
|
355
|
-
return
|
|
409
|
+
return null;
|
|
356
410
|
}
|
|
357
411
|
|
|
358
412
|
const phase = normalizeToken(getCurrentPhaseLabel(run, currentWorkItem));
|
|
359
413
|
if (phase !== 'plan') {
|
|
360
|
-
return
|
|
414
|
+
return null;
|
|
361
415
|
}
|
|
362
416
|
|
|
363
|
-
const
|
|
364
|
-
if (!
|
|
365
|
-
return
|
|
417
|
+
const resolvedApproval = resolveFireApprovalState(run, currentWorkItem);
|
|
418
|
+
if (!resolvedApproval.state) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (FIRE_APPROVED_STATES.has(resolvedApproval.state)) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!FIRE_AWAITING_APPROVAL_STATES.has(resolvedApproval.state)) {
|
|
427
|
+
return null;
|
|
366
428
|
}
|
|
367
429
|
|
|
368
|
-
|
|
430
|
+
const modeLabel = String(currentWorkItem?.mode || 'confirm').toUpperCase();
|
|
431
|
+
const itemId = String(currentWorkItem?.id || run.currentItem || 'unknown-item');
|
|
432
|
+
const checkpointLabel = String(resolvedApproval.checkpoint || 'plan').replace(/[_\s]+/g, '-');
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
flow: 'fire',
|
|
436
|
+
title: 'Approval Needed',
|
|
437
|
+
message: `${run.id}: ${itemId} (${modeLabel}) is waiting at ${checkpointLabel} checkpoint`,
|
|
438
|
+
checkpoint: checkpointLabel,
|
|
439
|
+
source: resolvedApproval.source
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isFireRunAwaitingApproval(run, currentWorkItem) {
|
|
444
|
+
return Boolean(getFireRunApprovalGate(run, currentWorkItem));
|
|
369
445
|
}
|
|
370
446
|
|
|
371
447
|
function detectFireRunApprovalGate(snapshot) {
|
|
@@ -379,17 +455,7 @@ function detectFireRunApprovalGate(snapshot) {
|
|
|
379
455
|
return null;
|
|
380
456
|
}
|
|
381
457
|
|
|
382
|
-
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const mode = String(currentWorkItem?.mode || 'confirm').toUpperCase();
|
|
387
|
-
const itemId = String(currentWorkItem?.id || run.currentItem || 'unknown-item');
|
|
388
|
-
return {
|
|
389
|
-
flow: 'fire',
|
|
390
|
-
title: 'Approval Needed',
|
|
391
|
-
message: `${run.id}: ${itemId} (${mode}) is waiting at plan checkpoint`
|
|
392
|
-
};
|
|
458
|
+
return getFireRunApprovalGate(run, currentWorkItem);
|
|
393
459
|
}
|
|
394
460
|
|
|
395
461
|
function normalizeStageName(stage) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specsmd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
|
|
5
5
|
"main": "lib/installer.js",
|
|
6
6
|
"bin": {
|