@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 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 buildAll(projectRoot, parsedArgs);
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 buildAll(projectRoot, args);
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 generateTaskPrompt(task, projectRoot) {
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 all remaining tasks
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 buildAll(projectRoot, args = {}) {
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 build' to start.${c.reset}`);
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}Building all ${stats.pending} remaining tasks...${c.reset}
380
- ${c.dim}Press Ctrl+C to stop at any time${c.reset}
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
- // Start the full loop - force to clear any stale session
384
- const loop = require('./loop');
385
- await loop.runLoop(projectRoot, {
386
- source: 'build',
387
- iterations: args.iterations || 50,
388
- live: true, // Always show live output so user can see Claude working
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, args = {}) {
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, stats) {
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
- To continue the build loop:
562
- ${c.cyan}bootspring loop start --source=build${c.reset}
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} Interactive mode (recommended)
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} and select a task
776
- 2. Claude Code executes the task prompt
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} Show next task prompt
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
  `);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "contractVersion": "v1",
3
3
  "packageName": "@girardmedia/bootspring",
4
- "packageVersion": "2.0.15",
4
+ "packageVersion": "2.0.16",
5
5
  "tools": [
6
6
  {
7
7
  "name": "bootspring_assist",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@girardmedia/bootspring",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
4
4
  "description": "Development scaffolding with intelligence - AI-powered context, agents, and workflows for any MCP-compatible assistant",
5
5
  "keywords": [
6
6
  "ai",