@girardmedia/bootspring 2.0.15 → 2.0.16
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 +216 -29
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +1 -1
package/cli/build.js
CHANGED
|
@@ -84,8 +84,9 @@ async function run(args) {
|
|
|
84
84
|
case 'complete':
|
|
85
85
|
return markTaskDone(projectRoot, parsedArgs);
|
|
86
86
|
|
|
87
|
+
case 'loop':
|
|
87
88
|
case 'all':
|
|
88
|
-
return
|
|
89
|
+
return buildLoop(projectRoot, parsedArgs);
|
|
89
90
|
|
|
90
91
|
case 'help':
|
|
91
92
|
case '-h':
|
|
@@ -102,7 +103,6 @@ async function run(args) {
|
|
|
102
103
|
* Interactive build mode - guides user through options
|
|
103
104
|
*/
|
|
104
105
|
async function interactiveBuild(projectRoot, args = {}) {
|
|
105
|
-
const readline = require('readline');
|
|
106
106
|
const state = buildState.load(projectRoot);
|
|
107
107
|
|
|
108
108
|
console.log(`
|
|
@@ -206,7 +206,7 @@ Some tasks may be blocked. Run ${c.cyan}bootspring build status${c.reset} for de
|
|
|
206
206
|
return buildNextTask(projectRoot, args);
|
|
207
207
|
|
|
208
208
|
case '2':
|
|
209
|
-
return
|
|
209
|
+
return buildLoop(projectRoot, args);
|
|
210
210
|
|
|
211
211
|
case '3':
|
|
212
212
|
showCurrentTask(projectRoot, { prompt: true });
|
|
@@ -273,7 +273,7 @@ ${c.dim}When done, run: bootspring build done${c.reset}
|
|
|
273
273
|
/**
|
|
274
274
|
* Generate a prompt for Claude Code to execute
|
|
275
275
|
*/
|
|
276
|
-
function
|
|
276
|
+
function _generateTaskPrompt(task, _projectRoot) {
|
|
277
277
|
const lines = [];
|
|
278
278
|
|
|
279
279
|
lines.push(`**Objective:** ${task.title}`);
|
|
@@ -331,6 +331,15 @@ async function markTaskDone(projectRoot, _args = {}) {
|
|
|
331
331
|
|
|
332
332
|
console.log(`${c.green}✓${c.reset} Completed: ${c.bold}${inProgressTask.title}${c.reset}`);
|
|
333
333
|
|
|
334
|
+
// Check if loop mode is active - if so, commit and push
|
|
335
|
+
const loopState = loadLoopState(projectRoot);
|
|
336
|
+
if (loopState && loopState.active) {
|
|
337
|
+
await commitAndPush(projectRoot, inProgressTask, {
|
|
338
|
+
noCommit: loopState.noCommit,
|
|
339
|
+
noPush: loopState.noPush
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
334
343
|
// Get stats
|
|
335
344
|
const stats = buildState.getStats(projectRoot);
|
|
336
345
|
console.log(`${c.dim}Progress: ${stats.completed}/${stats.total} tasks (${Math.round(stats.completedPercent)}%)${c.reset}`);
|
|
@@ -342,6 +351,15 @@ async function markTaskDone(projectRoot, _args = {}) {
|
|
|
342
351
|
// Mark as in progress
|
|
343
352
|
buildState.updateProgress(projectRoot, nextTask.id, 'in_progress');
|
|
344
353
|
|
|
354
|
+
// Update loop state with new task
|
|
355
|
+
if (loopState && loopState.active) {
|
|
356
|
+
saveLoopState(projectRoot, {
|
|
357
|
+
...loopState,
|
|
358
|
+
currentTaskId: nextTask.id,
|
|
359
|
+
lastTaskCompletedAt: new Date().toISOString()
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
345
363
|
console.log(`
|
|
346
364
|
${c.cyan}${c.bold}▶ Next: ${nextTask.title}${c.reset} (${nextTask.id})
|
|
347
365
|
|
|
@@ -349,6 +367,15 @@ ${c.bold}Continue building:${c.reset} Read planning/TASK_QUEUE.md, implement ${n
|
|
|
349
367
|
${c.dim}Then run: bootspring build done${c.reset}
|
|
350
368
|
`);
|
|
351
369
|
} else {
|
|
370
|
+
// Clear loop state - build complete
|
|
371
|
+
if (loopState && loopState.active) {
|
|
372
|
+
saveLoopState(projectRoot, {
|
|
373
|
+
...loopState,
|
|
374
|
+
active: false,
|
|
375
|
+
completedAt: new Date().toISOString()
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
352
379
|
console.log(`
|
|
353
380
|
${c.green}${c.bold}🎉 MVP BUILD COMPLETE!${c.reset}
|
|
354
381
|
|
|
@@ -363,34 +390,171 @@ ${c.bold}Next steps:${c.reset}
|
|
|
363
390
|
}
|
|
364
391
|
|
|
365
392
|
/**
|
|
366
|
-
* Build
|
|
393
|
+
* Build loop - autonomous task execution with commit/push after each task
|
|
394
|
+
*
|
|
395
|
+
* Usage:
|
|
396
|
+
* bootspring build loop # Start/resume the loop
|
|
397
|
+
* bootspring build loop --no-push # Commit but don't push
|
|
398
|
+
* bootspring build loop --no-commit # No git operations
|
|
367
399
|
*/
|
|
368
|
-
async function
|
|
400
|
+
async function buildLoop(projectRoot, args = {}) {
|
|
369
401
|
const state = buildState.load(projectRoot);
|
|
370
402
|
|
|
371
403
|
if (!state) {
|
|
372
|
-
console.log(`${c.yellow}No build state found. Run 'bootspring
|
|
404
|
+
console.log(`${c.yellow}No build state found. Run 'bootspring seed synthesize' first.${c.reset}`);
|
|
373
405
|
return;
|
|
374
406
|
}
|
|
375
407
|
|
|
376
408
|
const stats = buildState.getStats(projectRoot);
|
|
409
|
+
const noCommit = args['no-commit'] || args.noCommit;
|
|
410
|
+
const noPush = args['no-push'] || args.noPush || noCommit;
|
|
411
|
+
|
|
412
|
+
// Check for in-progress task (crash recovery)
|
|
413
|
+
const inProgress = state.implementationQueue.find(t => t.status === 'in_progress');
|
|
414
|
+
|
|
415
|
+
if (inProgress) {
|
|
416
|
+
console.log(`
|
|
417
|
+
${c.yellow}${c.bold}⚠ Resuming from crash${c.reset}
|
|
418
|
+
|
|
419
|
+
Found in-progress task: ${c.cyan}${inProgress.title}${c.reset} (${inProgress.id})
|
|
420
|
+
|
|
421
|
+
${c.dim}This task was in progress when the loop stopped.
|
|
422
|
+
Continue implementing it, then run 'bootspring build done'.${c.reset}
|
|
423
|
+
`);
|
|
424
|
+
} else {
|
|
425
|
+
// Queue next task
|
|
426
|
+
const nextTask = buildState.getNextTask(projectRoot);
|
|
427
|
+
if (!nextTask) {
|
|
428
|
+
console.log(`${c.green}${c.bold}🎉 All tasks complete!${c.reset}`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
buildState.updateProgress(projectRoot, nextTask.id, 'in_progress');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const currentTask = state.implementationQueue.find(t => t.status === 'in_progress')
|
|
435
|
+
|| buildState.getNextTask(projectRoot);
|
|
377
436
|
|
|
378
437
|
console.log(`
|
|
379
|
-
${c.cyan}${c.bold}
|
|
380
|
-
|
|
438
|
+
${c.cyan}${c.bold}⚡ Bootspring Build Loop${c.reset}
|
|
439
|
+
|
|
440
|
+
${c.bold}Current Task:${c.reset} ${currentTask?.title || 'None'} (${currentTask?.id || '-'})
|
|
441
|
+
${c.bold}Progress:${c.reset} ${stats.completed}/${stats.total} tasks (${Math.round(stats.completedPercent)}%)
|
|
442
|
+
${c.bold}Git Mode:${c.reset} ${noCommit ? 'Disabled' : noPush ? 'Commit only' : 'Commit + Push'}
|
|
443
|
+
|
|
444
|
+
${c.bold}Workflow:${c.reset}
|
|
445
|
+
1. Read planning/TASK_QUEUE.md, find ${currentTask?.id || 'current task'}
|
|
446
|
+
2. Implement the task
|
|
447
|
+
3. Run: ${c.cyan}bootspring build done${c.reset}${!noCommit ? `
|
|
448
|
+
(Auto-commits: "feat(${currentTask?.id}): ${currentTask?.title?.slice(0, 40) || 'task'}...")${!noPush ? `
|
|
449
|
+
(Auto-pushes to origin)` : ''}` : ''}
|
|
450
|
+
4. Continue to next task...
|
|
451
|
+
|
|
452
|
+
${c.dim}Press Ctrl+C to stop. Run 'bootspring build loop' to resume.${c.reset}
|
|
381
453
|
`);
|
|
382
454
|
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
verbose: args.verbose || args.v,
|
|
390
|
-
force: true // Always force to avoid "session already running" issues
|
|
455
|
+
// Save loop state for crash recovery
|
|
456
|
+
saveLoopState(projectRoot, {
|
|
457
|
+
active: true,
|
|
458
|
+
startedAt: new Date().toISOString(),
|
|
459
|
+
noCommit,
|
|
460
|
+
noPush
|
|
391
461
|
});
|
|
392
462
|
}
|
|
393
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Save loop state for crash recovery
|
|
466
|
+
*/
|
|
467
|
+
function saveLoopState(projectRoot, state) {
|
|
468
|
+
const loopFile = path.join(projectRoot, '.bootspring', 'loop-state.json');
|
|
469
|
+
const dir = path.dirname(loopFile);
|
|
470
|
+
if (!fs.existsSync(dir)) {
|
|
471
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
472
|
+
}
|
|
473
|
+
fs.writeFileSync(loopFile, JSON.stringify(state, null, 2));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Load loop state
|
|
478
|
+
*/
|
|
479
|
+
function loadLoopState(projectRoot) {
|
|
480
|
+
const loopFile = path.join(projectRoot, '.bootspring', 'loop-state.json');
|
|
481
|
+
if (fs.existsSync(loopFile)) {
|
|
482
|
+
try {
|
|
483
|
+
return JSON.parse(fs.readFileSync(loopFile, 'utf-8'));
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Clear loop state (used when stopping or completing)
|
|
493
|
+
*/
|
|
494
|
+
function _clearLoopState(projectRoot) {
|
|
495
|
+
const loopFile = path.join(projectRoot, '.bootspring', 'loop-state.json');
|
|
496
|
+
if (fs.existsSync(loopFile)) {
|
|
497
|
+
fs.unlinkSync(loopFile);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Commit and push changes for a completed task
|
|
503
|
+
*/
|
|
504
|
+
async function commitAndPush(projectRoot, task, options = {}) {
|
|
505
|
+
const { noCommit, noPush } = options;
|
|
506
|
+
|
|
507
|
+
if (noCommit) return { committed: false, pushed: false };
|
|
508
|
+
|
|
509
|
+
const { execSync } = require('child_process');
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
// Check if there are changes to commit
|
|
513
|
+
const status = execSync('git status --porcelain', { cwd: projectRoot, encoding: 'utf-8' });
|
|
514
|
+
if (!status.trim()) {
|
|
515
|
+
console.log(`${c.dim}No changes to commit${c.reset}`);
|
|
516
|
+
return { committed: false, pushed: false };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Stage all changes
|
|
520
|
+
execSync('git add -A', { cwd: projectRoot });
|
|
521
|
+
|
|
522
|
+
// Create commit message
|
|
523
|
+
const shortTitle = task.title.length > 50 ? task.title.slice(0, 47) + '...' : task.title;
|
|
524
|
+
const commitMsg = `feat(${task.id}): ${shortTitle}
|
|
525
|
+
|
|
526
|
+
Task: ${task.title}
|
|
527
|
+
Phase: ${task.phase || 'MVP'}
|
|
528
|
+
Source: ${task.source || 'planning'}
|
|
529
|
+
|
|
530
|
+
Acceptance Criteria:
|
|
531
|
+
${(task.acceptanceCriteria || []).map(ac => `- [x] ${ac}`).join('\n')}
|
|
532
|
+
|
|
533
|
+
Built with Bootspring autonomous loop.`;
|
|
534
|
+
|
|
535
|
+
// Commit
|
|
536
|
+
execSync(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { cwd: projectRoot });
|
|
537
|
+
console.log(`${c.green}✓${c.reset} Committed: ${c.bold}${task.id}${c.reset}`);
|
|
538
|
+
|
|
539
|
+
if (noPush) {
|
|
540
|
+
return { committed: true, pushed: false };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Push
|
|
544
|
+
try {
|
|
545
|
+
execSync('git push', { cwd: projectRoot, stdio: 'pipe' });
|
|
546
|
+
console.log(`${c.green}✓${c.reset} Pushed to origin`);
|
|
547
|
+
return { committed: true, pushed: true };
|
|
548
|
+
} catch (pushError) {
|
|
549
|
+
console.log(`${c.yellow}⚠${c.reset} Push failed (commit saved locally): ${pushError.message}`);
|
|
550
|
+
return { committed: true, pushed: false };
|
|
551
|
+
}
|
|
552
|
+
} catch (error) {
|
|
553
|
+
console.log(`${c.yellow}⚠${c.reset} Git operation failed: ${error.message}`);
|
|
554
|
+
return { committed: false, pushed: false };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
394
558
|
/**
|
|
395
559
|
* Ask a question and get user input
|
|
396
560
|
*/
|
|
@@ -412,7 +576,7 @@ function askQuestion(question) {
|
|
|
412
576
|
/**
|
|
413
577
|
* Show build status
|
|
414
578
|
*/
|
|
415
|
-
function showBuildStatus(projectRoot,
|
|
579
|
+
function showBuildStatus(projectRoot, _args = {}) {
|
|
416
580
|
const state = buildState.load(projectRoot);
|
|
417
581
|
|
|
418
582
|
if (!state) {
|
|
@@ -476,7 +640,7 @@ ${c.bold}Loop Session${c.reset}
|
|
|
476
640
|
/**
|
|
477
641
|
* Show suggested next actions
|
|
478
642
|
*/
|
|
479
|
-
function showNextActions(state,
|
|
643
|
+
function showNextActions(state, _stats) {
|
|
480
644
|
console.log(`${c.bold}Next Actions:${c.reset}`);
|
|
481
645
|
|
|
482
646
|
if (state.status === 'paused') {
|
|
@@ -523,6 +687,16 @@ function pauseBuildLoop(projectRoot) {
|
|
|
523
687
|
|
|
524
688
|
buildState.pause(projectRoot);
|
|
525
689
|
|
|
690
|
+
// Update loop state to inactive
|
|
691
|
+
const loopState = loadLoopState(projectRoot);
|
|
692
|
+
if (loopState) {
|
|
693
|
+
saveLoopState(projectRoot, {
|
|
694
|
+
...loopState,
|
|
695
|
+
active: false,
|
|
696
|
+
pausedAt: new Date().toISOString()
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
526
700
|
console.log(`
|
|
527
701
|
${c.green}Build paused.${c.reset}
|
|
528
702
|
|
|
@@ -555,14 +729,21 @@ function resumeBuildLoop(projectRoot) {
|
|
|
555
729
|
|
|
556
730
|
buildState.resume(projectRoot);
|
|
557
731
|
|
|
732
|
+
// Restore loop state if it exists
|
|
733
|
+
const loopState = loadLoopState(projectRoot);
|
|
734
|
+
if (loopState) {
|
|
735
|
+
saveLoopState(projectRoot, {
|
|
736
|
+
...loopState,
|
|
737
|
+
active: true,
|
|
738
|
+
resumedAt: new Date().toISOString()
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
558
742
|
console.log(`
|
|
559
743
|
${c.green}Build resumed.${c.reset}
|
|
560
744
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
Or start fresh:
|
|
565
|
-
${c.cyan}bootspring seed build --loop${c.reset}
|
|
745
|
+
Continue with: ${c.cyan}bootspring build next${c.reset}
|
|
746
|
+
Or run loop: ${c.cyan}bootspring build loop${c.reset}
|
|
566
747
|
`);
|
|
567
748
|
}
|
|
568
749
|
|
|
@@ -769,18 +950,19 @@ ${c.cyan}${c.bold}Bootspring Build${c.reset}
|
|
|
769
950
|
${c.dim}Task-driven build system for Claude Code${c.reset}
|
|
770
951
|
|
|
771
952
|
${c.bold}Quick Start:${c.reset}
|
|
772
|
-
${c.cyan}bootspring build${c.reset}
|
|
953
|
+
${c.cyan}bootspring build next${c.reset} Get next task and start building
|
|
773
954
|
|
|
774
955
|
${c.bold}Workflow:${c.reset}
|
|
775
|
-
1. Run ${c.cyan}bootspring build${c.reset}
|
|
776
|
-
2.
|
|
956
|
+
1. Run ${c.cyan}bootspring build next${c.reset} to get the next task
|
|
957
|
+
2. Implement the task in your codebase
|
|
777
958
|
3. Run ${c.cyan}bootspring build done${c.reset} when complete
|
|
778
959
|
4. Repeat until MVP is complete
|
|
779
960
|
|
|
780
961
|
${c.bold}Commands:${c.reset}
|
|
781
962
|
${c.cyan}bootspring build${c.reset} Start interactive build
|
|
782
|
-
${c.cyan}bootspring build next${c.reset}
|
|
963
|
+
${c.cyan}bootspring build next${c.reset} Get next task (start here!)
|
|
783
964
|
${c.cyan}bootspring build done${c.reset} Mark current task complete
|
|
965
|
+
${c.cyan}bootspring build loop${c.reset} Start autonomous loop (commit+push each task)
|
|
784
966
|
${c.cyan}bootspring build skip${c.reset} Skip current task
|
|
785
967
|
${c.cyan}bootspring build stop${c.reset} Graceful stop (finish current task, then pause)
|
|
786
968
|
${c.cyan}bootspring build status${c.reset} View detailed progress
|
|
@@ -788,7 +970,12 @@ ${c.bold}Commands:${c.reset}
|
|
|
788
970
|
${c.cyan}bootspring build plan${c.reset} View the full master plan
|
|
789
971
|
${c.cyan}bootspring build reset${c.reset} Start over
|
|
790
972
|
|
|
791
|
-
${c.bold}Options:${c.reset}
|
|
973
|
+
${c.bold}Loop Options:${c.reset}
|
|
974
|
+
${c.cyan}bootspring build loop${c.reset} Commit + push after each task
|
|
975
|
+
${c.cyan}bootspring build loop --no-push${c.reset} Commit only (no push)
|
|
976
|
+
${c.cyan}bootspring build loop --no-commit${c.reset} No git operations
|
|
977
|
+
|
|
978
|
+
${c.bold}Other Options:${c.reset}
|
|
792
979
|
--prompt Show AI prompt for current task
|
|
793
980
|
--force Force reset even if in progress
|
|
794
981
|
`);
|
package/package.json
CHANGED