specsmd 0.1.45 → 0.1.47
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/bin/cli.js +1 -0
- 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/git/worktrees.js +248 -0
- package/lib/dashboard/index.js +473 -7
- package/lib/dashboard/runtime/watch-runtime.js +18 -9
- package/lib/dashboard/tui/app.js +529 -69
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ program
|
|
|
24
24
|
.description('Live terminal dashboard for flow state (FIRE first)')
|
|
25
25
|
.option('--flow <flow>', 'Flow to inspect (fire|aidlc|simple), default auto-detect')
|
|
26
26
|
.option('--path <dir>', 'Workspace path', process.cwd())
|
|
27
|
+
.option('--worktree <nameOrPath>', 'Initial git worktree (branch name, worktree name, id, or absolute path)')
|
|
27
28
|
.option('--refresh-ms <n>', 'Fallback refresh interval in milliseconds (default: 1000)', '1000')
|
|
28
29
|
.option('--no-watch', 'Render once and exit')
|
|
29
30
|
.action((options) => dashboard.run(options));
|
|
@@ -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,
|