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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
//
|
|
235
|
+
// Complete Current Item (for batch runs)
|
|
195
236
|
// =============================================================================
|
|
196
237
|
|
|
197
|
-
function
|
|
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
|
-
//
|
|
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
|
-
|
|
245
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
workItem.
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
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 (!
|
|
43
|
-
throw fireError(
|
|
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 (!
|
|
47
|
-
throw fireError(
|
|
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
|
-
`
|
|
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
|
-
|
|
84
|
+
function validateWorkItems(workItems) {
|
|
85
|
+
if (!Array.isArray(workItems) || workItems.length === 0) {
|
|
59
86
|
throw fireError(
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
'
|
|
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,
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
##
|
|
204
|
-
${
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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,
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 <
|
|
294
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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": {
|