specsmd 0.0.0-dev.55 → 0.0.0-dev.56

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.
@@ -3,15 +3,24 @@
3
3
  /**
4
4
  * FIRE Run Completion Script
5
5
  *
6
- * Finalizes a run by:
7
- * 1. Recording the completed run in state.yaml runs.completed history
8
- * 2. Updating work item status to completed
9
- * 3. Clearing active_run
10
- * 4. Updating run.md with completion data
6
+ * Supports both single and batch/wide runs.
11
7
  *
12
- * Usage: node complete-run.js <rootPath> <runId> [--files-created=JSON] [--files-modified=JSON] [--decisions=JSON] [--tests=N] [--coverage=N]
8
+ * For single runs: Completes the run and clears active_run.
9
+ * For batch/wide runs:
10
+ * - --complete-item: Marks current work item done, moves to next
11
+ * - --complete-run: Marks all items done and finalizes entire run
13
12
  *
14
- * Example: node complete-run.js /project run-003 --tests=5 --coverage=85
13
+ * Usage:
14
+ * Complete current item: node complete-run.js <rootPath> <runId> --complete-item [options]
15
+ * Complete entire run: node complete-run.js <rootPath> <runId> --complete-run [options]
16
+ * Complete (single/auto): node complete-run.js <rootPath> <runId> [options]
17
+ *
18
+ * Options:
19
+ * --files-created=JSON - JSON array of {path, purpose}
20
+ * --files-modified=JSON - JSON array of {path, changes}
21
+ * --decisions=JSON - JSON array of {decision, choice, rationale}
22
+ * --tests=N - Number of tests added
23
+ * --coverage=N - Coverage percentage
15
24
  */
16
25
 
17
26
  const fs = require('fs');
@@ -131,7 +140,7 @@ function writeState(statePath, state) {
131
140
  // Run Log Operations
132
141
  // =============================================================================
133
142
 
134
- function updateRunLog(runLogPath, params, completedTime) {
143
+ function updateRunLog(runLogPath, activeRun, params, completedTime, isFullCompletion) {
135
144
  let content;
136
145
  try {
137
146
  content = fs.readFileSync(runLogPath, 'utf8');
@@ -143,40 +152,72 @@ function updateRunLog(runLogPath, params, completedTime) {
143
152
  );
144
153
  }
145
154
 
146
- // Update status
147
- content = content.replace(/status: in_progress/, 'status: completed');
148
- content = content.replace(/completed: null/, `completed: ${completedTime}`);
155
+ // If full completion, update run status
156
+ if (isFullCompletion) {
157
+ content = content.replace(/status: in_progress/, 'status: completed');
158
+ content = content.replace(/completed: null/, `completed: ${completedTime}`);
159
+ }
160
+
161
+ // Update work items status in frontmatter
162
+ if (activeRun.work_items && Array.isArray(activeRun.work_items)) {
163
+ for (const item of activeRun.work_items) {
164
+ // Update status in YAML frontmatter
165
+ const statusPattern = new RegExp(`(id: ${item.id}\\n\\s+intent: [^\\n]+\\n\\s+mode: [^\\n]+\\n\\s+status: )(\\w+)`);
166
+ content = content.replace(statusPattern, `$1${item.status}`);
167
+
168
+ // Update status in markdown body
169
+ const bodyPattern = new RegExp(`(\\*\\*${item.id}\\*\\* \\([^)]+\\) — )(\\w+)`);
170
+ content = content.replace(bodyPattern, `$1${item.status}`);
171
+ }
172
+
173
+ // Update current_item in frontmatter
174
+ content = content.replace(/current_item: [^\n]+/, `current_item: ${activeRun.current_item || 'none'}`);
175
+
176
+ // Update Current Item section
177
+ if (activeRun.current_item) {
178
+ const currentItem = activeRun.work_items.find(i => i.id === activeRun.current_item);
179
+ if (currentItem) {
180
+ content = content.replace(/## Current Item\n[^\n]+/, `## Current Item\n${currentItem.id} (${currentItem.mode})`);
181
+ }
182
+ } else {
183
+ content = content.replace(/## Current Item\n[^\n]+/, `## Current Item\n(all completed)`);
184
+ }
185
+ }
149
186
 
150
- // Format file lists
151
- const filesCreatedText = params.filesCreated.length > 0
152
- ? params.filesCreated.map(f => `- \`${f.path}\`: ${f.purpose || '(no purpose)'}`).join('\n')
153
- : '(none)';
187
+ // Format file lists (only on full completion)
188
+ if (isFullCompletion) {
189
+ const filesCreatedText = params.filesCreated.length > 0
190
+ ? params.filesCreated.map(f => `- \`${f.path}\`: ${f.purpose || '(no purpose)'}`).join('\n')
191
+ : '(none)';
154
192
 
155
- const filesModifiedText = params.filesModified.length > 0
156
- ? params.filesModified.map(f => `- \`${f.path}\`: ${f.changes || '(no changes)'}`).join('\n')
157
- : '(none)';
193
+ const filesModifiedText = params.filesModified.length > 0
194
+ ? params.filesModified.map(f => `- \`${f.path}\`: ${f.changes || '(no changes)'}`).join('\n')
195
+ : '(none)';
158
196
 
159
- const decisionsText = params.decisions.length > 0
160
- ? params.decisions.map(d => `- **${d.decision}**: ${d.choice} (${d.rationale || 'no rationale'})`).join('\n')
161
- : '(none)';
197
+ const decisionsText = params.decisions.length > 0
198
+ ? params.decisions.map(d => `- **${d.decision}**: ${d.choice} (${d.rationale || 'no rationale'})`).join('\n')
199
+ : '(none)';
162
200
 
163
- // Replace placeholder sections
164
- content = content.replace('## Files Created\n(none yet)', `## Files Created\n${filesCreatedText}`);
165
- content = content.replace('## Files Modified\n(none yet)', `## Files Modified\n${filesModifiedText}`);
166
- content = content.replace('## Decisions\n(none yet)', `## Decisions\n${decisionsText}`);
201
+ // Replace placeholder sections
202
+ content = content.replace('## Files Created\n(none yet)', `## Files Created\n${filesCreatedText}`);
203
+ content = content.replace('## Files Modified\n(none yet)', `## Files Modified\n${filesModifiedText}`);
204
+ content = content.replace('## Decisions\n(none yet)', `## Decisions\n${decisionsText}`);
167
205
 
168
- // Add summary if not present
169
- if (!content.includes('## Summary')) {
170
- content += `
206
+ // Add summary if not present
207
+ if (!content.includes('## Summary')) {
208
+ const itemCount = activeRun.work_items ? activeRun.work_items.length : 1;
209
+ content += `
171
210
 
172
211
  ## Summary
173
212
 
213
+ - Work items completed: ${itemCount}
174
214
  - Files created: ${params.filesCreated.length}
175
215
  - Files modified: ${params.filesModified.length}
176
216
  - Tests added: ${params.testsAdded}
177
217
  - Coverage: ${params.coverage}%
178
218
  - Completed: ${completedTime}
179
219
  `;
220
+ }
180
221
  }
181
222
 
182
223
  try {
@@ -191,11 +232,10 @@ function updateRunLog(runLogPath, params, completedTime) {
191
232
  }
192
233
 
193
234
  // =============================================================================
194
- // Main Function
235
+ // Complete Current Item (for batch runs)
195
236
  // =============================================================================
196
237
 
197
- function completeRun(rootPath, runId, params = {}) {
198
- // Defaults for optional params
238
+ function completeCurrentItem(rootPath, runId, params = {}) {
199
239
  const completionParams = {
200
240
  filesCreated: params.filesCreated || [],
201
241
  filesModified: params.filesModified || [],
@@ -204,16 +244,97 @@ function completeRun(rootPath, runId, params = {}) {
204
244
  coverage: params.coverage || 0,
205
245
  };
206
246
 
207
- // Validate inputs
208
247
  validateInputs(rootPath, runId);
209
-
210
- // Validate FIRE project structure
211
248
  const { statePath, runLogPath } = validateFireProject(rootPath, runId);
249
+ const state = readState(statePath);
250
+
251
+ if (!state.active_run) {
252
+ throw fireError(
253
+ 'No active run found in state.yaml.',
254
+ 'COMPLETE_040',
255
+ 'The run may have already been completed or was never started.'
256
+ );
257
+ }
258
+
259
+ if (state.active_run.id !== runId) {
260
+ throw fireError(
261
+ `Run ID mismatch. Active run is "${state.active_run.id}" but trying to complete "${runId}".`,
262
+ 'COMPLETE_041',
263
+ `Complete the active run "${state.active_run.id}" first.`
264
+ );
265
+ }
266
+
267
+ const completedTime = new Date().toISOString();
268
+ const workItems = state.active_run.work_items || [];
269
+ const currentItemId = state.active_run.current_item;
270
+
271
+ // Find and mark current item as completed
272
+ let currentItemIndex = -1;
273
+ for (let i = 0; i < workItems.length; i++) {
274
+ if (workItems[i].id === currentItemId) {
275
+ workItems[i].status = 'completed';
276
+ workItems[i].completed_at = completedTime;
277
+ currentItemIndex = i;
278
+ break;
279
+ }
280
+ }
281
+
282
+ if (currentItemIndex === -1) {
283
+ throw fireError(
284
+ `Current item "${currentItemId}" not found in work items.`,
285
+ 'COMPLETE_050',
286
+ 'The run state may be corrupted.'
287
+ );
288
+ }
212
289
 
213
- // Read state
290
+ // Find next pending item
291
+ let nextItem = null;
292
+ for (let i = currentItemIndex + 1; i < workItems.length; i++) {
293
+ if (workItems[i].status === 'pending') {
294
+ workItems[i].status = 'in_progress';
295
+ nextItem = workItems[i];
296
+ break;
297
+ }
298
+ }
299
+
300
+ // Update state
301
+ state.active_run.work_items = workItems;
302
+ state.active_run.current_item = nextItem ? nextItem.id : null;
303
+
304
+ // Update run log
305
+ updateRunLog(runLogPath, state.active_run, completionParams, completedTime, false);
306
+
307
+ // Save state
308
+ writeState(statePath, state);
309
+
310
+ return {
311
+ success: true,
312
+ runId: runId,
313
+ completedItem: currentItemId,
314
+ nextItem: nextItem ? nextItem.id : null,
315
+ remainingItems: workItems.filter(i => i.status === 'pending').length,
316
+ allItemsCompleted: nextItem === null,
317
+ completedAt: completedTime,
318
+ };
319
+ }
320
+
321
+ // =============================================================================
322
+ // Complete Entire Run
323
+ // =============================================================================
324
+
325
+ function completeRun(rootPath, runId, params = {}) {
326
+ const completionParams = {
327
+ filesCreated: params.filesCreated || [],
328
+ filesModified: params.filesModified || [],
329
+ decisions: params.decisions || [],
330
+ testsAdded: params.testsAdded || 0,
331
+ coverage: params.coverage || 0,
332
+ };
333
+
334
+ validateInputs(rootPath, runId);
335
+ const { statePath, runLogPath } = validateFireProject(rootPath, runId);
214
336
  const state = readState(statePath);
215
337
 
216
- // Validate active run matches
217
338
  if (!state.active_run) {
218
339
  throw fireError(
219
340
  'No active run found in state.yaml.',
@@ -230,19 +351,33 @@ function completeRun(rootPath, runId, params = {}) {
230
351
  );
231
352
  }
232
353
 
233
- // Extract work item and intent from active run
234
- const workItemId = state.active_run.work_item;
235
- const intentId = state.active_run.intent;
236
354
  const completedTime = new Date().toISOString();
355
+ const workItems = state.active_run.work_items || [];
356
+ const scope = state.active_run.scope || 'single';
357
+
358
+ // Mark all items as completed
359
+ for (const item of workItems) {
360
+ if (item.status !== 'completed') {
361
+ item.status = 'completed';
362
+ item.completed_at = completedTime;
363
+ }
364
+ }
365
+
366
+ state.active_run.work_items = workItems;
367
+ state.active_run.current_item = null;
237
368
 
238
369
  // Update run log
239
- updateRunLog(runLogPath, completionParams, completedTime);
370
+ updateRunLog(runLogPath, state.active_run, completionParams, completedTime, true);
240
371
 
241
372
  // Build completed run record
242
373
  const completedRun = {
243
374
  id: runId,
244
- work_item: workItemId,
245
- intent: intentId,
375
+ scope: scope,
376
+ work_items: workItems.map(i => ({
377
+ id: i.id,
378
+ intent: i.intent,
379
+ mode: i.mode,
380
+ })),
246
381
  completed: completedTime,
247
382
  };
248
383
 
@@ -255,16 +390,17 @@ function completeRun(rootPath, runId, params = {}) {
255
390
 
256
391
  // Update work item status in intents
257
392
  if (Array.isArray(state.intents)) {
258
- for (const intent of state.intents) {
259
- if (intent.id === intentId && Array.isArray(intent.work_items)) {
260
- for (const workItem of intent.work_items) {
261
- if (workItem.id === workItemId) {
262
- workItem.status = 'completed';
263
- workItem.run_id = runId;
264
- break;
393
+ for (const workItem of workItems) {
394
+ for (const intent of state.intents) {
395
+ if (intent.id === workItem.intent && Array.isArray(intent.work_items)) {
396
+ for (const wi of intent.work_items) {
397
+ if (wi.id === workItem.id) {
398
+ wi.status = 'completed';
399
+ wi.run_id = runId;
400
+ break;
401
+ }
265
402
  }
266
403
  }
267
- break;
268
404
  }
269
405
  }
270
406
  }
@@ -278,12 +414,11 @@ function completeRun(rootPath, runId, params = {}) {
278
414
  // Save state
279
415
  writeState(statePath, state);
280
416
 
281
- // Return result
282
417
  return {
283
418
  success: true,
284
419
  runId: runId,
285
- workItemId: workItemId,
286
- intentId: intentId,
420
+ scope: scope,
421
+ workItemsCompleted: workItems.length,
287
422
  completedAt: completedTime,
288
423
  filesCreated: completionParams.filesCreated.length,
289
424
  filesModified: completionParams.filesModified.length,
@@ -300,6 +435,8 @@ function parseArgs(args) {
300
435
  const result = {
301
436
  rootPath: args[0],
302
437
  runId: args[1],
438
+ completeItem: false,
439
+ completeRunFlag: false,
303
440
  filesCreated: [],
304
441
  filesModified: [],
305
442
  decisions: [],
@@ -309,7 +446,11 @@ function parseArgs(args) {
309
446
 
310
447
  for (let i = 2; i < args.length; i++) {
311
448
  const arg = args[i];
312
- if (arg.startsWith('--files-created=')) {
449
+ if (arg === '--complete-item') {
450
+ result.completeItem = true;
451
+ } else if (arg === '--complete-run') {
452
+ result.completeRunFlag = true;
453
+ } else if (arg.startsWith('--files-created=')) {
313
454
  try {
314
455
  result.filesCreated = JSON.parse(arg.substring('--files-created='.length));
315
456
  } catch (e) {
@@ -337,6 +478,32 @@ function parseArgs(args) {
337
478
  return result;
338
479
  }
339
480
 
481
+ function printUsage() {
482
+ console.error('Usage:');
483
+ console.error(' Complete current item: node complete-run.js <rootPath> <runId> --complete-item [options]');
484
+ console.error(' Complete entire run: node complete-run.js <rootPath> <runId> --complete-run [options]');
485
+ console.error(' Auto (single runs): node complete-run.js <rootPath> <runId> [options]');
486
+ console.error('');
487
+ console.error('Arguments:');
488
+ console.error(' rootPath - Project root directory');
489
+ console.error(' runId - Run ID to complete (e.g., run-003)');
490
+ console.error('');
491
+ console.error('Flags:');
492
+ console.error(' --complete-item - Complete only the current work item (batch/wide runs)');
493
+ console.error(' --complete-run - Complete the entire run');
494
+ console.error('');
495
+ console.error('Options:');
496
+ console.error(' --files-created=JSON - JSON array of {path, purpose}');
497
+ console.error(' --files-modified=JSON - JSON array of {path, changes}');
498
+ console.error(' --decisions=JSON - JSON array of {decision, choice, rationale}');
499
+ console.error(' --tests=N - Number of tests added');
500
+ console.error(' --coverage=N - Coverage percentage');
501
+ console.error('');
502
+ console.error('Examples:');
503
+ console.error(' node complete-run.js /project run-003 --complete-item');
504
+ console.error(' node complete-run.js /project run-003 --complete-run --tests=5 --coverage=85');
505
+ }
506
+
340
507
  // =============================================================================
341
508
  // CLI Interface
342
509
  // =============================================================================
@@ -345,34 +512,32 @@ if (require.main === module) {
345
512
  const args = process.argv.slice(2);
346
513
 
347
514
  if (args.length < 2) {
348
- console.error('Usage: node complete-run.js <rootPath> <runId> [options]');
349
- console.error('');
350
- console.error('Arguments:');
351
- console.error(' rootPath - Project root directory');
352
- console.error(' runId - Run ID to complete (e.g., run-003)');
353
- console.error('');
354
- console.error('Options:');
355
- console.error(' --files-created=JSON - JSON array of {path, purpose}');
356
- console.error(' --files-modified=JSON - JSON array of {path, changes}');
357
- console.error(' --decisions=JSON - JSON array of {decision, choice, rationale}');
358
- console.error(' --tests=N - Number of tests added');
359
- console.error(' --coverage=N - Coverage percentage');
360
- console.error('');
361
- console.error('Example:');
362
- console.error(' node complete-run.js /my/project run-003 --tests=5 --coverage=85');
515
+ printUsage();
363
516
  process.exit(1);
364
517
  }
365
518
 
366
519
  const params = parseArgs(args);
367
520
 
368
521
  try {
369
- const result = completeRun(params.rootPath, params.runId, {
370
- filesCreated: params.filesCreated,
371
- filesModified: params.filesModified,
372
- decisions: params.decisions,
373
- testsAdded: params.testsAdded,
374
- coverage: params.coverage,
375
- });
522
+ let result;
523
+ if (params.completeItem) {
524
+ result = completeCurrentItem(params.rootPath, params.runId, {
525
+ filesCreated: params.filesCreated,
526
+ filesModified: params.filesModified,
527
+ decisions: params.decisions,
528
+ testsAdded: params.testsAdded,
529
+ coverage: params.coverage,
530
+ });
531
+ } else {
532
+ // Default: complete entire run
533
+ result = completeRun(params.rootPath, params.runId, {
534
+ filesCreated: params.filesCreated,
535
+ filesModified: params.filesModified,
536
+ decisions: params.decisions,
537
+ testsAdded: params.testsAdded,
538
+ coverage: params.coverage,
539
+ });
540
+ }
376
541
  console.log(JSON.stringify(result, null, 2));
377
542
  process.exit(0);
378
543
  } catch (err) {
@@ -381,4 +546,4 @@ if (require.main === module) {
381
546
  }
382
547
  }
383
548
 
384
- module.exports = { completeRun };
549
+ module.exports = { completeRun, completeCurrentItem };
@@ -4,13 +4,19 @@
4
4
  * FIRE Run Initialization Script
5
5
  *
6
6
  * Creates run record in state.yaml and run folder structure.
7
+ * Supports both single work item and batch/wide runs with multiple items.
8
+ *
7
9
  * Ensures deterministic run ID generation by checking BOTH:
8
10
  * - runs.completed history in state.yaml
9
11
  * - existing run folders in .specs-fire/runs/
10
12
  *
11
- * Usage: node init-run.js <rootPath> <workItemId> <intentId> <mode>
13
+ * Usage:
14
+ * Single item: node init-run.js <rootPath> <workItemId> <intentId> <mode>
15
+ * Batch/Wide: node init-run.js <rootPath> --batch '<workItemsJson>'
12
16
  *
13
- * Example: node init-run.js /project login-endpoint user-auth confirm
17
+ * Examples:
18
+ * node init-run.js /project login-endpoint user-auth confirm
19
+ * node init-run.js /project --batch '[{"id":"wi-1","intent":"int-1","mode":"autopilot"}]'
14
20
  */
15
21
 
16
22
  const fs = require('fs');
@@ -33,35 +39,58 @@ function fireError(message, code, suggestion) {
33
39
  // =============================================================================
34
40
 
35
41
  const VALID_MODES = ['autopilot', 'confirm', 'validate'];
42
+ const VALID_SCOPES = ['single', 'batch', 'wide'];
36
43
 
37
- function validateInputs(rootPath, workItemId, intentId, mode) {
44
+ function validateRootPath(rootPath) {
38
45
  if (!rootPath || typeof rootPath !== 'string' || rootPath.trim() === '') {
39
46
  throw fireError('rootPath is required.', 'INIT_001', 'Provide a valid project root path.');
40
47
  }
41
48
 
42
- if (!workItemId || typeof workItemId !== 'string' || workItemId.trim() === '') {
43
- throw fireError('workItemId is required.', 'INIT_010', 'Provide a valid work item ID.');
49
+ if (!fs.existsSync(rootPath)) {
50
+ throw fireError(
51
+ `Project root not found: "${rootPath}".`,
52
+ 'INIT_040',
53
+ 'Ensure the path exists and is accessible.'
54
+ );
55
+ }
56
+ }
57
+
58
+ function validateWorkItem(item, index) {
59
+ if (!item.id || typeof item.id !== 'string' || item.id.trim() === '') {
60
+ throw fireError(
61
+ `Work item at index ${index} missing 'id'.`,
62
+ 'INIT_010',
63
+ 'Each work item must have an id.'
64
+ );
44
65
  }
45
66
 
46
- if (!intentId || typeof intentId !== 'string' || intentId.trim() === '') {
47
- throw fireError('intentId is required.', 'INIT_020', 'Provide a valid intent ID.');
67
+ if (!item.intent || typeof item.intent !== 'string' || item.intent.trim() === '') {
68
+ throw fireError(
69
+ `Work item "${item.id}" missing 'intent'.`,
70
+ 'INIT_020',
71
+ 'Each work item must have an intent.'
72
+ );
48
73
  }
49
74
 
50
- if (!mode || !VALID_MODES.includes(mode)) {
75
+ if (!item.mode || !VALID_MODES.includes(item.mode)) {
51
76
  throw fireError(
52
- `Invalid mode: "${mode}".`,
77
+ `Work item "${item.id}" has invalid mode: "${item.mode}".`,
53
78
  'INIT_030',
54
79
  `Valid modes are: ${VALID_MODES.join(', ')}`
55
80
  );
56
81
  }
82
+ }
57
83
 
58
- if (!fs.existsSync(rootPath)) {
84
+ function validateWorkItems(workItems) {
85
+ if (!Array.isArray(workItems) || workItems.length === 0) {
59
86
  throw fireError(
60
- `Project root not found: "${rootPath}".`,
61
- 'INIT_040',
62
- 'Ensure the path exists and is accessible.'
87
+ 'Work items array is empty or invalid.',
88
+ 'INIT_011',
89
+ 'Provide at least one work item.'
63
90
  );
64
91
  }
92
+
93
+ workItems.forEach((item, index) => validateWorkItem(item, index));
65
94
  }
66
95
 
67
96
  function validateFireProject(rootPath) {
@@ -171,6 +200,19 @@ function generateRunId(runsPath, state) {
171
200
  return `run-${String(nextNum).padStart(3, '0')}`;
172
201
  }
173
202
 
203
+ // =============================================================================
204
+ // Scope Detection
205
+ // =============================================================================
206
+
207
+ function detectScope(workItems) {
208
+ if (workItems.length === 1) {
209
+ return 'single';
210
+ }
211
+ // For multiple items, default to batch
212
+ // (wide would be explicitly set by the caller if all compatible items are included)
213
+ return 'batch';
214
+ }
215
+
174
216
  // =============================================================================
175
217
  // Run Folder Creation
176
218
  // =============================================================================
@@ -187,12 +229,21 @@ function createRunFolder(runPath) {
187
229
  }
188
230
  }
189
231
 
190
- function createRunLog(runPath, runId, workItemId, intentId, mode, startTime) {
232
+ function createRunLog(runPath, runId, workItems, scope, startTime) {
233
+ // Format work items for run.md
234
+ const workItemsList = workItems.map((item, index) => {
235
+ const status = index === 0 ? 'in_progress' : 'pending';
236
+ return ` - id: ${item.id}\n intent: ${item.intent}\n mode: ${item.mode}\n status: ${status}`;
237
+ }).join('\n');
238
+
239
+ const currentItem = workItems[0];
240
+
191
241
  const runLog = `---
192
242
  id: ${runId}
193
- work_item: ${workItemId}
194
- intent: ${intentId}
195
- mode: ${mode}
243
+ scope: ${scope}
244
+ work_items:
245
+ ${workItemsList}
246
+ current_item: ${currentItem.id}
196
247
  status: in_progress
197
248
  started: ${startTime}
198
249
  completed: null
@@ -200,8 +251,14 @@ completed: null
200
251
 
201
252
  # Run: ${runId}
202
253
 
203
- ## Work Item
204
- ${workItemId}
254
+ ## Scope
255
+ ${scope} (${workItems.length} work item${workItems.length > 1 ? 's' : ''})
256
+
257
+ ## Work Items
258
+ ${workItems.map((item, i) => `${i + 1}. **${item.id}** (${item.mode}) — ${i === 0 ? 'in_progress' : 'pending'}`).join('\n')}
259
+
260
+ ## Current Item
261
+ ${currentItem.id} (${currentItem.mode})
205
262
 
206
263
  ## Files Created
207
264
  (none yet)
@@ -229,9 +286,30 @@ ${workItemId}
229
286
  // Main Function
230
287
  // =============================================================================
231
288
 
232
- function initRun(rootPath, workItemId, intentId, mode) {
233
- // Validate inputs
234
- validateInputs(rootPath, workItemId, intentId, mode);
289
+ /**
290
+ * Initialize a run with one or more work items.
291
+ *
292
+ * @param {string} rootPath - Project root directory
293
+ * @param {Array<{id: string, intent: string, mode: string}>} workItems - Work items to include in run
294
+ * @param {string} [scope] - Optional scope override ('single', 'batch', 'wide')
295
+ * @returns {object} Result with runId, runPath, workItems, scope, started
296
+ */
297
+ function initRun(rootPath, workItems, scope) {
298
+ // Validate root path
299
+ validateRootPath(rootPath);
300
+
301
+ // Validate work items
302
+ validateWorkItems(workItems);
303
+
304
+ // Detect or validate scope
305
+ const detectedScope = scope || detectScope(workItems);
306
+ if (scope && !VALID_SCOPES.includes(scope)) {
307
+ throw fireError(
308
+ `Invalid scope: "${scope}".`,
309
+ 'INIT_035',
310
+ `Valid scopes are: ${VALID_SCOPES.join(', ')}`
311
+ );
312
+ }
235
313
 
236
314
  // Validate FIRE project structure
237
315
  const { statePath, runsPath } = validateFireProject(rootPath);
@@ -257,14 +335,22 @@ function initRun(rootPath, workItemId, intentId, mode) {
257
335
 
258
336
  // Create run log
259
337
  const startTime = new Date().toISOString();
260
- createRunLog(runPath, runId, workItemId, intentId, mode, startTime);
338
+ createRunLog(runPath, runId, workItems, detectedScope, startTime);
339
+
340
+ // Prepare work items for state with status tracking
341
+ const stateWorkItems = workItems.map((item, index) => ({
342
+ id: item.id,
343
+ intent: item.intent,
344
+ mode: item.mode,
345
+ status: index === 0 ? 'in_progress' : 'pending',
346
+ }));
261
347
 
262
348
  // Update state with active run
263
349
  state.active_run = {
264
350
  id: runId,
265
- work_item: workItemId,
266
- intent: intentId,
267
- mode: mode,
351
+ scope: detectedScope,
352
+ work_items: stateWorkItems,
353
+ current_item: workItems[0].id,
268
354
  started: startTime,
269
355
  };
270
356
 
@@ -276,9 +362,9 @@ function initRun(rootPath, workItemId, intentId, mode) {
276
362
  success: true,
277
363
  runId: runId,
278
364
  runPath: runPath,
279
- workItemId: workItemId,
280
- intentId: intentId,
281
- mode: mode,
365
+ scope: detectedScope,
366
+ workItems: stateWorkItems,
367
+ currentItem: workItems[0].id,
282
368
  started: startTime,
283
369
  };
284
370
  }
@@ -287,27 +373,76 @@ function initRun(rootPath, workItemId, intentId, mode) {
287
373
  // CLI Interface
288
374
  // =============================================================================
289
375
 
376
+ function printUsage() {
377
+ console.error('Usage:');
378
+ console.error(' Single item: node init-run.js <rootPath> <workItemId> <intentId> <mode>');
379
+ console.error(' Batch/Wide: node init-run.js <rootPath> --batch \'<workItemsJson>\' [--scope=<scope>]');
380
+ console.error('');
381
+ console.error('Arguments:');
382
+ console.error(' rootPath - Project root directory');
383
+ console.error(' workItemId - Work item ID (single mode)');
384
+ console.error(' intentId - Intent ID (single mode)');
385
+ console.error(' mode - Execution mode: autopilot, confirm, validate');
386
+ console.error('');
387
+ console.error('Options:');
388
+ console.error(' --batch - JSON array of work items');
389
+ console.error(' --scope - Override scope: single, batch, wide');
390
+ console.error('');
391
+ console.error('Work item JSON format:');
392
+ console.error(' [{"id": "wi-1", "intent": "int-1", "mode": "autopilot"}, ...]');
393
+ console.error('');
394
+ console.error('Examples:');
395
+ console.error(' node init-run.js /project login-endpoint user-auth confirm');
396
+ console.error(' node init-run.js /project --batch \'[{"id":"wi-1","intent":"int-1","mode":"autopilot"}]\'');
397
+ }
398
+
290
399
  if (require.main === module) {
291
400
  const args = process.argv.slice(2);
292
401
 
293
- if (args.length < 4) {
294
- console.error('Usage: node init-run.js <rootPath> <workItemId> <intentId> <mode>');
295
- console.error('');
296
- console.error('Arguments:');
297
- console.error(' rootPath - Project root directory');
298
- console.error(' workItemId - Work item ID to execute');
299
- console.error(' intentId - Intent ID containing the work item');
300
- console.error(' mode - Execution mode (autopilot, confirm, validate)');
301
- console.error('');
302
- console.error('Example:');
303
- console.error(' node init-run.js /my/project login-endpoint user-auth confirm');
402
+ if (args.length < 2) {
403
+ printUsage();
304
404
  process.exit(1);
305
405
  }
306
406
 
307
- const [rootPath, workItemId, intentId, mode] = args;
407
+ const rootPath = args[0];
408
+ let workItems = [];
409
+ let scope = null;
410
+
411
+ // Check if batch mode
412
+ if (args[1] === '--batch') {
413
+ if (args.length < 3) {
414
+ console.error('Error: --batch requires a JSON array of work items');
415
+ printUsage();
416
+ process.exit(1);
417
+ }
418
+
419
+ try {
420
+ workItems = JSON.parse(args[2]);
421
+ } catch (err) {
422
+ console.error(`Error: Failed to parse work items JSON: ${err.message}`);
423
+ process.exit(1);
424
+ }
425
+
426
+ // Check for --scope option
427
+ for (let i = 3; i < args.length; i++) {
428
+ if (args[i].startsWith('--scope=')) {
429
+ scope = args[i].substring('--scope='.length);
430
+ }
431
+ }
432
+ } else {
433
+ // Single item mode (backwards compatible)
434
+ if (args.length < 4) {
435
+ printUsage();
436
+ process.exit(1);
437
+ }
438
+
439
+ const [, workItemId, intentId, mode] = args;
440
+ workItems = [{ id: workItemId, intent: intentId, mode: mode }];
441
+ scope = 'single';
442
+ }
308
443
 
309
444
  try {
310
- const result = initRun(rootPath, workItemId, intentId, mode);
445
+ const result = initRun(rootPath, workItems, scope);
311
446
  console.log(JSON.stringify(result, null, 2));
312
447
  process.exit(0);
313
448
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.0.0-dev.55",
3
+ "version": "0.0.0-dev.56",
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": {