@girardmedia/bootspring 2.0.6 → 2.0.7
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/cli/build.js +32 -7
- package/cli/loop.js +118 -12
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +1 -1
package/cli/build.js
CHANGED
|
@@ -56,9 +56,11 @@ async function run(args) {
|
|
|
56
56
|
return showBuildStatus(projectRoot, parsedArgs);
|
|
57
57
|
|
|
58
58
|
case 'pause':
|
|
59
|
-
case 'stop':
|
|
60
59
|
return pauseBuildLoop(projectRoot);
|
|
61
60
|
|
|
61
|
+
case 'stop':
|
|
62
|
+
return gracefulStop(projectRoot);
|
|
63
|
+
|
|
62
64
|
case 'resume':
|
|
63
65
|
case 'continue':
|
|
64
66
|
return resumeBuildLoop(projectRoot);
|
|
@@ -189,13 +191,14 @@ Some tasks may be blocked. Run ${c.cyan}bootspring build status${c.reset} for de
|
|
|
189
191
|
console.log(`${c.bold}What would you like to do?${c.reset}
|
|
190
192
|
|
|
191
193
|
${c.cyan}1${c.reset} Start this task (shows prompt for Claude Code)
|
|
192
|
-
${c.cyan}2${c.reset}
|
|
193
|
-
${c.cyan}3${c.reset}
|
|
194
|
-
${c.cyan}4${c.reset}
|
|
194
|
+
${c.cyan}2${c.reset} Loop all tasks (autonomous - runs Claude until complete)
|
|
195
|
+
${c.cyan}3${c.reset} View task details
|
|
196
|
+
${c.cyan}4${c.reset} Skip this task
|
|
197
|
+
${c.cyan}5${c.reset} View full plan
|
|
195
198
|
${c.cyan}q${c.reset} Quit
|
|
196
199
|
`);
|
|
197
200
|
|
|
198
|
-
const choice = await askQuestion('Choose [1-
|
|
201
|
+
const choice = await askQuestion('Choose [1-5, q]: ');
|
|
199
202
|
|
|
200
203
|
switch (choice.trim()) {
|
|
201
204
|
case '1':
|
|
@@ -203,17 +206,20 @@ Some tasks may be blocked. Run ${c.cyan}bootspring build status${c.reset} for de
|
|
|
203
206
|
return buildNextTask(projectRoot, args);
|
|
204
207
|
|
|
205
208
|
case '2':
|
|
209
|
+
return buildAll(projectRoot, args);
|
|
210
|
+
|
|
211
|
+
case '3':
|
|
206
212
|
showCurrentTask(projectRoot, { prompt: true });
|
|
207
213
|
console.log('');
|
|
208
214
|
return interactiveBuild(projectRoot, args);
|
|
209
215
|
|
|
210
|
-
case '
|
|
216
|
+
case '4': {
|
|
211
217
|
const reason = await askQuestion('Skip reason (optional): ');
|
|
212
218
|
skipCurrentTask(projectRoot, { reason: reason || 'Skipped by user' });
|
|
213
219
|
return interactiveBuild(projectRoot, args);
|
|
214
220
|
}
|
|
215
221
|
|
|
216
|
-
case '
|
|
222
|
+
case '5':
|
|
217
223
|
showPlan(projectRoot);
|
|
218
224
|
return interactiveBuild(projectRoot, args);
|
|
219
225
|
|
|
@@ -547,6 +553,24 @@ Or start fresh:
|
|
|
547
553
|
`);
|
|
548
554
|
}
|
|
549
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Graceful stop - finish current task then pause
|
|
558
|
+
*/
|
|
559
|
+
function gracefulStop(projectRoot) {
|
|
560
|
+
const loop = require('./loop');
|
|
561
|
+
|
|
562
|
+
console.log(`
|
|
563
|
+
${c.yellow}${c.bold}Graceful stop requested${c.reset}
|
|
564
|
+
|
|
565
|
+
The loop will pause after the current task completes.
|
|
566
|
+
This ensures no work is interrupted mid-task.
|
|
567
|
+
|
|
568
|
+
${c.dim}Waiting for current task to finish...${c.reset}
|
|
569
|
+
`);
|
|
570
|
+
|
|
571
|
+
loop.setStopSignal(projectRoot);
|
|
572
|
+
}
|
|
573
|
+
|
|
550
574
|
/**
|
|
551
575
|
* Show current task
|
|
552
576
|
*/
|
|
@@ -745,6 +769,7 @@ ${c.bold}Commands:${c.reset}
|
|
|
745
769
|
${c.cyan}bootspring build next${c.reset} Show next task prompt
|
|
746
770
|
${c.cyan}bootspring build done${c.reset} Mark current task complete
|
|
747
771
|
${c.cyan}bootspring build skip${c.reset} Skip current task
|
|
772
|
+
${c.cyan}bootspring build stop${c.reset} Graceful stop (finish current task, then pause)
|
|
748
773
|
${c.cyan}bootspring build status${c.reset} View detailed progress
|
|
749
774
|
${c.cyan}bootspring build task${c.reset} View current task details
|
|
750
775
|
${c.cyan}bootspring build plan${c.reset} View the full master plan
|
package/cli/loop.js
CHANGED
|
@@ -457,17 +457,25 @@ ${task.acceptance ? `\n### Acceptance Criteria\n${task.acceptance.map(a => `- ${
|
|
|
457
457
|
- No TypeScript/lint errors
|
|
458
458
|
- No security vulnerabilities introduced
|
|
459
459
|
|
|
460
|
-
##
|
|
461
|
-
When you
|
|
462
|
-
<loop-status>TASK_COMPLETE</loop-status>
|
|
460
|
+
## Status Reporting
|
|
461
|
+
When you complete work, include a status block at the end of your response:
|
|
463
462
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
463
|
+
\`\`\`
|
|
464
|
+
BOOTSPRING_STATUS: COMPLETE
|
|
465
|
+
\`\`\`
|
|
466
|
+
|
|
467
|
+
If you cannot proceed (missing info, blocked, need human input):
|
|
468
|
+
\`\`\`
|
|
469
|
+
BOOTSPRING_STATUS: BLOCKED
|
|
470
|
+
BOOTSPRING_REASON: [brief explanation]
|
|
471
|
+
\`\`\`
|
|
467
472
|
|
|
468
|
-
If
|
|
469
|
-
|
|
470
|
-
|
|
473
|
+
If ALL tasks in the build are finished:
|
|
474
|
+
\`\`\`
|
|
475
|
+
BOOTSPRING_STATUS: EXIT
|
|
476
|
+
\`\`\`
|
|
477
|
+
|
|
478
|
+
IMPORTANT: Always include one of these status blocks at the end of your work.
|
|
471
479
|
|
|
472
480
|
## Session Info
|
|
473
481
|
- Session: ${session.state.sessionId}
|
|
@@ -543,7 +551,17 @@ async function runIteration(session, taskInfo, options = {}) {
|
|
|
543
551
|
|
|
544
552
|
if (session.state.tool === 'claude') {
|
|
545
553
|
aiCmd = 'claude';
|
|
546
|
-
aiArgs = ['
|
|
554
|
+
aiArgs = ['-p']; // Print mode for non-interactive
|
|
555
|
+
|
|
556
|
+
// Continue session if not first iteration (maintains context)
|
|
557
|
+
if (session.state.iteration > 0) {
|
|
558
|
+
aiArgs.push('-c'); // Continue most recent conversation
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Use JSON output for structured parsing when not in live mode
|
|
562
|
+
if (!options.live) {
|
|
563
|
+
aiArgs.push('--output-format', 'json');
|
|
564
|
+
}
|
|
547
565
|
} else if (session.state.tool === 'amp') {
|
|
548
566
|
aiCmd = 'amp';
|
|
549
567
|
aiArgs = [];
|
|
@@ -552,9 +570,12 @@ async function runIteration(session, taskInfo, options = {}) {
|
|
|
552
570
|
return;
|
|
553
571
|
}
|
|
554
572
|
|
|
573
|
+
// For live mode, inherit stdout/stderr so user sees Claude working
|
|
574
|
+
// For non-live mode, capture output for parsing
|
|
555
575
|
const child = spawn(aiCmd, aiArgs, {
|
|
556
576
|
cwd: session.projectRoot,
|
|
557
|
-
stdio: options.live ? ['pipe', 'inherit', 'inherit'] : ['pipe', 'pipe', 'pipe']
|
|
577
|
+
stdio: options.live ? ['pipe', 'inherit', 'inherit'] : ['pipe', 'pipe', 'pipe'],
|
|
578
|
+
env: { ...process.env }
|
|
558
579
|
});
|
|
559
580
|
|
|
560
581
|
// Send prompt via stdin
|
|
@@ -577,6 +598,28 @@ async function runIteration(session, taskInfo, options = {}) {
|
|
|
577
598
|
}
|
|
578
599
|
|
|
579
600
|
child.on('close', (code) => {
|
|
601
|
+
// For live mode, we assume success if exit code is 0
|
|
602
|
+
// Claude will have output directly to terminal
|
|
603
|
+
if (options.live) {
|
|
604
|
+
if (code !== 0) {
|
|
605
|
+
session.recordTaskEnd('error', { exitCode: code });
|
|
606
|
+
resolve({ status: 'error', exitCode: code });
|
|
607
|
+
} else {
|
|
608
|
+
session.recordTaskEnd('complete');
|
|
609
|
+
resolve({ status: 'complete', output: '[live mode - output shown above]' });
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Parse JSON output if available
|
|
615
|
+
let parsedOutput = null;
|
|
616
|
+
try {
|
|
617
|
+
parsedOutput = JSON.parse(output);
|
|
618
|
+
output = parsedOutput.result || parsedOutput.content || output;
|
|
619
|
+
} catch {
|
|
620
|
+
// Not JSON, use raw output
|
|
621
|
+
}
|
|
622
|
+
|
|
580
623
|
// Check exit conditions
|
|
581
624
|
const exitCheck = session.checkExitConditions(output);
|
|
582
625
|
|
|
@@ -586,7 +629,27 @@ async function runIteration(session, taskInfo, options = {}) {
|
|
|
586
629
|
return;
|
|
587
630
|
}
|
|
588
631
|
|
|
589
|
-
// Check for
|
|
632
|
+
// Check for BOOTSPRING_STATUS block (like Ralph's RALPH_STATUS)
|
|
633
|
+
const statusMatch = output.match(/BOOTSPRING_STATUS:\s*(\w+)/i);
|
|
634
|
+
if (statusMatch) {
|
|
635
|
+
const status = statusMatch[1].toUpperCase();
|
|
636
|
+
if (status === 'COMPLETE' || status === 'DONE') {
|
|
637
|
+
session.recordTaskEnd('complete');
|
|
638
|
+
resolve({ status: 'complete', output });
|
|
639
|
+
return;
|
|
640
|
+
} else if (status === 'BLOCKED' || status === 'STUCK') {
|
|
641
|
+
const reasonMatch = output.match(/BOOTSPRING_REASON:\s*(.+)/i);
|
|
642
|
+
session.recordTaskEnd('blocked', { reason: reasonMatch?.[1] || 'Unknown' });
|
|
643
|
+
resolve({ status: 'blocked', reason: reasonMatch?.[1], output });
|
|
644
|
+
return;
|
|
645
|
+
} else if (status === 'EXIT' || status === 'FINISHED') {
|
|
646
|
+
session.recordTaskEnd('complete', { exitReason: 'All tasks complete' });
|
|
647
|
+
resolve({ status: 'all_complete', reason: 'All tasks complete', output });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Legacy status tags
|
|
590
653
|
if (output.includes('<loop-status>TASK_COMPLETE</loop-status>')) {
|
|
591
654
|
session.recordTaskEnd('complete');
|
|
592
655
|
resolve({ status: 'complete', output });
|
|
@@ -618,6 +681,37 @@ function sleep(ms) {
|
|
|
618
681
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
619
682
|
}
|
|
620
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Check if graceful stop signal is set
|
|
686
|
+
*/
|
|
687
|
+
function checkStopSignal(projectRoot) {
|
|
688
|
+
const stopFile = path.join(projectRoot, '.bootspring', 'STOP_AFTER_TASK');
|
|
689
|
+
return fs.existsSync(stopFile);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Set graceful stop signal (stop after current task completes)
|
|
694
|
+
*/
|
|
695
|
+
function setStopSignal(projectRoot) {
|
|
696
|
+
const bootspringDir = path.join(projectRoot, '.bootspring');
|
|
697
|
+
if (!fs.existsSync(bootspringDir)) {
|
|
698
|
+
fs.mkdirSync(bootspringDir, { recursive: true });
|
|
699
|
+
}
|
|
700
|
+
const stopFile = path.join(bootspringDir, 'STOP_AFTER_TASK');
|
|
701
|
+
fs.writeFileSync(stopFile, new Date().toISOString());
|
|
702
|
+
return stopFile;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Clear graceful stop signal
|
|
707
|
+
*/
|
|
708
|
+
function clearStopSignal(projectRoot) {
|
|
709
|
+
const stopFile = path.join(projectRoot, '.bootspring', 'STOP_AFTER_TASK');
|
|
710
|
+
if (fs.existsSync(stopFile)) {
|
|
711
|
+
fs.unlinkSync(stopFile);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
621
715
|
/**
|
|
622
716
|
* Run the main loop
|
|
623
717
|
*/
|
|
@@ -729,6 +823,16 @@ ${c.bold}Configuration${c.reset}
|
|
|
729
823
|
}
|
|
730
824
|
}
|
|
731
825
|
|
|
826
|
+
// Check for graceful stop signal (stop after completing current task)
|
|
827
|
+
if (checkStopSignal(projectRoot)) {
|
|
828
|
+
console.log(`\n${c.yellow}${c.bold}⏸ Graceful stop requested. Pausing after this task.${c.reset}`);
|
|
829
|
+
session.state.status = 'paused';
|
|
830
|
+
session.save();
|
|
831
|
+
clearStopSignal(projectRoot);
|
|
832
|
+
console.log(`${c.dim}Resume with: bootspring build${c.reset}\n`);
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
|
|
732
836
|
// Delay between iterations
|
|
733
837
|
await sleep(LOOP_CONFIG.minDelayBetweenCalls);
|
|
734
838
|
}
|
|
@@ -1331,5 +1435,7 @@ module.exports = {
|
|
|
1331
1435
|
showStatus,
|
|
1332
1436
|
showPRDStatus,
|
|
1333
1437
|
getTasks,
|
|
1438
|
+
setStopSignal,
|
|
1439
|
+
clearStopSignal,
|
|
1334
1440
|
LOOP_CONFIG
|
|
1335
1441
|
};
|
package/package.json
CHANGED