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 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
- return ` - id: ${item.id}\n intent: ${item.intent}\n mode: ${item.mode}\n status: ${status}`;
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: typeof frontmatter.current_item === 'string'
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 workItems = (stateActiveRun?.workItems && stateActiveRun.workItems.length > 0)
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
- : parsedRunLog.workItems);
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,