specsmd 0.1.44 → 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 +343 -8
- 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
|
@@ -253,6 +253,313 @@ function getCurrentRun(snapshot) {
|
|
|
253
253
|
return activeRuns[0] || null;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
function normalizeToken(value) {
|
|
257
|
+
if (typeof value !== 'string') {
|
|
258
|
+
return '';
|
|
259
|
+
}
|
|
260
|
+
return value.toLowerCase().trim().replace(/[\s-]+/g, '_');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getCurrentFireWorkItem(run) {
|
|
264
|
+
const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
|
|
265
|
+
if (workItems.length === 0) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return workItems.find((item) => item.id === run.currentItem)
|
|
269
|
+
|| workItems.find((item) => normalizeToken(item?.status) === 'in_progress')
|
|
270
|
+
|| workItems[0]
|
|
271
|
+
|| null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function readFileTextSafe(filePath) {
|
|
275
|
+
try {
|
|
276
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function extractFrontmatterBlock(content) {
|
|
283
|
+
if (typeof content !== 'string') {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
287
|
+
return match ? match[1] : null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function extractFrontmatterValue(frontmatterBlock, key) {
|
|
291
|
+
if (typeof frontmatterBlock !== 'string' || typeof key !== 'string' || key === '') {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
296
|
+
const expression = new RegExp(`^${escapedKey}\\s*:\\s*(.+)$`, 'mi');
|
|
297
|
+
const match = frontmatterBlock.match(expression);
|
|
298
|
+
if (!match) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const raw = String(match[1] || '').trim();
|
|
303
|
+
if (raw === '') {
|
|
304
|
+
return '';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return raw
|
|
308
|
+
.replace(/^["']/, '')
|
|
309
|
+
.replace(/["']$/, '')
|
|
310
|
+
.trim();
|
|
311
|
+
}
|
|
312
|
+
|
|
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) {
|
|
336
|
+
if (!run || typeof run.folderPath !== 'string' || run.folderPath.trim() === '') {
|
|
337
|
+
return { hasPlan: false, checkpointState: null, checkpoint: null };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const planPath = path.join(run.folderPath, 'plan.md');
|
|
341
|
+
if (!fileExists(planPath)) {
|
|
342
|
+
return { hasPlan: false, checkpointState: null, checkpoint: null };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const content = readFileTextSafe(planPath);
|
|
346
|
+
const frontmatter = extractFrontmatterBlock(content);
|
|
347
|
+
if (!frontmatter) {
|
|
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;
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
hasPlan: true,
|
|
365
|
+
checkpointState,
|
|
366
|
+
checkpoint
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
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) {
|
|
406
|
+
const mode = normalizeToken(currentWorkItem?.mode);
|
|
407
|
+
const status = normalizeToken(currentWorkItem?.status);
|
|
408
|
+
if (!['confirm', 'validate'].includes(mode) || status !== 'in_progress') {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const phase = normalizeToken(getCurrentPhaseLabel(run, currentWorkItem));
|
|
413
|
+
if (phase !== 'plan') {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
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;
|
|
428
|
+
}
|
|
429
|
+
|
|
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));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function detectFireRunApprovalGate(snapshot) {
|
|
448
|
+
const run = getCurrentRun(snapshot);
|
|
449
|
+
if (!run) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const currentWorkItem = getCurrentFireWorkItem(run);
|
|
454
|
+
if (!currentWorkItem) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return getFireRunApprovalGate(run, currentWorkItem);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function normalizeStageName(stage) {
|
|
462
|
+
return normalizeToken(stage).replace(/_/g, '-');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function getAidlcCheckpointSignalFiles(boltType, stageName) {
|
|
466
|
+
const normalizedType = normalizeToken(boltType).replace(/_/g, '-');
|
|
467
|
+
const normalizedStage = normalizeStageName(stageName);
|
|
468
|
+
|
|
469
|
+
if (normalizedType === 'simple-construction-bolt') {
|
|
470
|
+
if (normalizedStage === 'plan') return ['implementation-plan.md'];
|
|
471
|
+
if (normalizedStage === 'implement') return ['implementation-walkthrough.md'];
|
|
472
|
+
if (normalizedStage === 'test') return ['test-walkthrough.md'];
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (normalizedType === 'ddd-construction-bolt') {
|
|
477
|
+
if (normalizedStage === 'model') return ['ddd-01-domain-model.md'];
|
|
478
|
+
if (normalizedStage === 'design') return ['ddd-02-technical-design.md'];
|
|
479
|
+
if (normalizedStage === 'implement') return ['implementation-walkthrough.md'];
|
|
480
|
+
if (normalizedStage === 'test') return ['ddd-03-test-report.md'];
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (normalizedType === 'spike-bolt') {
|
|
485
|
+
if (normalizedStage === 'explore') return ['spike-exploration.md'];
|
|
486
|
+
if (normalizedStage === 'document') return ['spike-report.md'];
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function hasAidlcCheckpointSignal(bolt, stageName) {
|
|
494
|
+
const fileNames = Array.isArray(bolt?.files) ? bolt.files : [];
|
|
495
|
+
const lowerNames = new Set(fileNames.map((name) => String(name || '').toLowerCase()));
|
|
496
|
+
const expectedFiles = getAidlcCheckpointSignalFiles(bolt?.type, stageName)
|
|
497
|
+
.map((name) => String(name).toLowerCase());
|
|
498
|
+
|
|
499
|
+
for (const expectedFile of expectedFiles) {
|
|
500
|
+
if (lowerNames.has(expectedFile)) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (normalizeStageName(stageName) === 'adr') {
|
|
506
|
+
for (const name of lowerNames) {
|
|
507
|
+
if (/^adr-[\w-]+\.md$/.test(name)) {
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function isAidlcBoltAwaitingApproval(bolt) {
|
|
517
|
+
if (!bolt || normalizeToken(bolt.status) !== 'in_progress') {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const currentStage = normalizeStageName(bolt.currentStage);
|
|
522
|
+
if (!currentStage) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
|
|
527
|
+
const stageMeta = stages.find((stage) => normalizeStageName(stage?.name) === currentStage);
|
|
528
|
+
if (normalizeToken(stageMeta?.status) === 'completed') {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return hasAidlcCheckpointSignal(bolt, currentStage);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function detectAidlcBoltApprovalGate(snapshot) {
|
|
536
|
+
const bolt = getCurrentBolt(snapshot);
|
|
537
|
+
if (!bolt) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!isAidlcBoltAwaitingApproval(bolt)) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
flow: 'aidlc',
|
|
547
|
+
title: 'Approval Needed',
|
|
548
|
+
message: `${bolt.id}: ${bolt.currentStage || 'current'} stage is waiting for confirmation`
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function detectDashboardApprovalGate(snapshot, flow) {
|
|
553
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
554
|
+
if (effectiveFlow === 'fire') {
|
|
555
|
+
return detectFireRunApprovalGate(snapshot);
|
|
556
|
+
}
|
|
557
|
+
if (effectiveFlow === 'aidlc') {
|
|
558
|
+
return detectAidlcBoltApprovalGate(snapshot);
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
256
563
|
function getCurrentPhaseLabel(run, currentWorkItem) {
|
|
257
564
|
const phase = currentWorkItem?.currentPhase || '';
|
|
258
565
|
if (typeof phase === 'string' && phase !== '') {
|
|
@@ -1185,10 +1492,8 @@ function buildFireCurrentRunGroups(snapshot) {
|
|
|
1185
1492
|
|
|
1186
1493
|
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
1187
1494
|
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
1188
|
-
const currentWorkItem =
|
|
1189
|
-
|
|
1190
|
-
|| workItems[0]
|
|
1191
|
-
|| null;
|
|
1495
|
+
const currentWorkItem = getCurrentFireWorkItem(run);
|
|
1496
|
+
const awaitingApproval = isFireRunAwaitingApproval(run, currentWorkItem);
|
|
1192
1497
|
|
|
1193
1498
|
const currentPhase = getCurrentPhaseLabel(run, currentWorkItem);
|
|
1194
1499
|
const phaseTrack = buildPhaseTrack(currentPhase);
|
|
@@ -1232,7 +1537,7 @@ function buildFireCurrentRunGroups(snapshot) {
|
|
|
1232
1537
|
return [
|
|
1233
1538
|
{
|
|
1234
1539
|
key: `current:run:${run.id}:summary`,
|
|
1235
|
-
label: `${run.id} [${run.scope}] ${completed}/${workItems.length} items`,
|
|
1540
|
+
label: `${run.id} [${run.scope}] ${completed}/${workItems.length} items${awaitingApproval ? ' [APPROVAL]' : ''}`,
|
|
1236
1541
|
files: []
|
|
1237
1542
|
},
|
|
1238
1543
|
{
|
|
@@ -1258,9 +1563,10 @@ function buildCurrentGroups(snapshot, flow) {
|
|
|
1258
1563
|
}
|
|
1259
1564
|
const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
|
|
1260
1565
|
const completedStages = stages.filter((stage) => stage.status === 'completed').length;
|
|
1566
|
+
const awaitingApproval = isAidlcBoltAwaitingApproval(bolt);
|
|
1261
1567
|
return [{
|
|
1262
1568
|
key: `current:bolt:${bolt.id}`,
|
|
1263
|
-
label: `${bolt.id} [${bolt.type}] ${completedStages}/${stages.length} stages`,
|
|
1569
|
+
label: `${bolt.id} [${bolt.type}] ${completedStages}/${stages.length} stages${awaitingApproval ? ' [APPROVAL]' : ''}`,
|
|
1264
1570
|
files: filterExistingFiles([
|
|
1265
1571
|
...collectAidlcBoltFiles(bolt),
|
|
1266
1572
|
...collectAidlcIntentContextFiles(snapshot, bolt.intent)
|
|
@@ -2403,6 +2709,10 @@ function createDashboardApp(deps) {
|
|
|
2403
2709
|
}, [showErrorPanelForSections]);
|
|
2404
2710
|
|
|
2405
2711
|
const effectiveFlow = getEffectiveFlow(activeFlow, snapshot);
|
|
2712
|
+
const approvalGate = detectDashboardApprovalGate(snapshot, activeFlow);
|
|
2713
|
+
const approvalGateLine = approvalGate
|
|
2714
|
+
? `[APPROVAL NEEDED] ${approvalGate.message}`
|
|
2715
|
+
: '';
|
|
2406
2716
|
const currentGroups = buildCurrentGroups(snapshot, activeFlow);
|
|
2407
2717
|
const currentExpandedGroups = { ...expandedGroups };
|
|
2408
2718
|
for (const group of currentGroups) {
|
|
@@ -2411,11 +2721,24 @@ function createDashboardApp(deps) {
|
|
|
2411
2721
|
}
|
|
2412
2722
|
}
|
|
2413
2723
|
|
|
2414
|
-
const
|
|
2724
|
+
const currentRunRowsBase = toExpandableRows(
|
|
2415
2725
|
currentGroups,
|
|
2416
2726
|
getNoCurrentMessage(effectiveFlow),
|
|
2417
2727
|
currentExpandedGroups
|
|
2418
2728
|
);
|
|
2729
|
+
const currentRunRows = approvalGate
|
|
2730
|
+
? [
|
|
2731
|
+
{
|
|
2732
|
+
kind: 'info',
|
|
2733
|
+
key: 'approval-gate',
|
|
2734
|
+
label: approvalGateLine,
|
|
2735
|
+
color: 'yellow',
|
|
2736
|
+
bold: true,
|
|
2737
|
+
selectable: false
|
|
2738
|
+
},
|
|
2739
|
+
...currentRunRowsBase
|
|
2740
|
+
]
|
|
2741
|
+
: currentRunRowsBase;
|
|
2419
2742
|
const shouldHydrateSecondaryTabs = deferredTabsReady || ui.view !== 'runs';
|
|
2420
2743
|
const runFileGroups = buildRunFileEntityGroups(snapshot, activeFlow, {
|
|
2421
2744
|
includeBacklog: shouldHydrateSecondaryTabs
|
|
@@ -3027,12 +3350,14 @@ function createDashboardApp(deps) {
|
|
|
3027
3350
|
const showErrorPanel = Boolean(error) && rows >= 18;
|
|
3028
3351
|
const showGlobalErrorPanel = showErrorPanel && ui.view !== 'health' && !ui.showHelp;
|
|
3029
3352
|
const showErrorInline = Boolean(error) && !showErrorPanel;
|
|
3353
|
+
const showApprovalBanner = approvalGateLine !== '' && !ui.showHelp;
|
|
3030
3354
|
const showStatusLine = statusLine !== '';
|
|
3031
3355
|
const densePanels = rows <= 28 || cols <= 120;
|
|
3032
3356
|
|
|
3033
3357
|
const reservedRows =
|
|
3034
3358
|
2 +
|
|
3035
3359
|
(showFlowBar ? 1 : 0) +
|
|
3360
|
+
(showApprovalBanner ? 1 : 0) +
|
|
3036
3361
|
(showFooterHelpLine ? 1 : 0) +
|
|
3037
3362
|
(showGlobalErrorPanel ? 5 : 0) +
|
|
3038
3363
|
(showErrorInline ? 1 : 0) +
|
|
@@ -3266,6 +3591,13 @@ function createDashboardApp(deps) {
|
|
|
3266
3591
|
React.createElement(Text, { color: 'cyan' }, buildHeaderLine(snapshot, activeFlow, watchEnabled, watchStatus, lastRefreshAt, ui.view, fullWidth)),
|
|
3267
3592
|
React.createElement(FlowBar, { activeFlow, width: fullWidth, flowIds: availableFlowIds }),
|
|
3268
3593
|
React.createElement(TabsBar, { view: ui.view, width: fullWidth, icons, flow: activeFlow }),
|
|
3594
|
+
showApprovalBanner
|
|
3595
|
+
? React.createElement(
|
|
3596
|
+
Text,
|
|
3597
|
+
{ color: 'black', backgroundColor: 'yellow', bold: true },
|
|
3598
|
+
truncate(approvalGateLine, fullWidth)
|
|
3599
|
+
)
|
|
3600
|
+
: null,
|
|
3269
3601
|
showErrorInline
|
|
3270
3602
|
? React.createElement(Text, { color: 'red' }, truncate(buildErrorLines(error, fullWidth)[0] || 'Error', fullWidth))
|
|
3271
3603
|
: null,
|
|
@@ -3300,5 +3632,8 @@ module.exports = {
|
|
|
3300
3632
|
truncate,
|
|
3301
3633
|
fitLines,
|
|
3302
3634
|
safeJsonHash,
|
|
3303
|
-
allocateSingleColumnPanels
|
|
3635
|
+
allocateSingleColumnPanels,
|
|
3636
|
+
detectDashboardApprovalGate,
|
|
3637
|
+
detectFireRunApprovalGate,
|
|
3638
|
+
detectAidlcBoltApprovalGate
|
|
3304
3639
|
};
|
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": {
|