borisxdave 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

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.
boris.py CHANGED
@@ -202,15 +202,23 @@ def parse_args() -> argparse.Namespace:
202
202
  description="Boris - Autonomous Project Orchestrator. Breaks tasks into milestones and executes via Claude CLI.",
203
203
  )
204
204
  parser.add_argument("task", nargs="?", help="The task description string")
205
+ parser.add_argument(
206
+ "-f", "--file", help="Read task description from a file"
207
+ )
205
208
  parser.add_argument(
206
209
  "-d", "--dir", default=os.getcwd(), help="Working directory for the project (default: current dir)"
207
210
  )
208
211
  parser.add_argument(
209
212
  "-r", "--resume", action="store_true", help="Resume from last saved state"
210
213
  )
211
- parser.add_argument(
214
+ plan_group = parser.add_mutually_exclusive_group()
215
+ plan_group.add_argument(
212
216
  "--plan-only", action="store_true", help="Generate plan but don't execute"
213
217
  )
218
+ plan_group.add_argument(
219
+ "--exec-only", metavar="PLAN_FILE",
220
+ help="Skip planning and execute an existing plan file directly"
221
+ )
214
222
  parser.add_argument("--remote", help="Git remote URL to set up")
215
223
  parser.add_argument(
216
224
  "--no-git", action="store_true", help="Disable git management"
@@ -224,6 +232,18 @@ def parse_args() -> argparse.Namespace:
224
232
  parser.add_argument(
225
233
  "--skip-ui", action="store_true", help="Skip UI testing phase after structural milestones"
226
234
  )
235
+ parser.add_argument(
236
+ "--incremental", action="store_true",
237
+ help="Execute one milestone at a time, then pause. Use with -r to continue to the next milestone."
238
+ )
239
+ parser.add_argument(
240
+ "--turbo", action="store_true",
241
+ help="Enable parallel DaveLoop execution for independent milestones"
242
+ )
243
+ parser.add_argument(
244
+ "--stop-at", metavar="MILESTONE_ID", dest="stop_at",
245
+ help="Stop execution after completing this milestone ID (e.g. M4). Implies --incremental."
246
+ )
227
247
 
228
248
  return parser.parse_args()
229
249
 
@@ -400,9 +420,110 @@ def _run_ui_milestones(st, plan, project_dir, args, logger):
400
420
  logger.info("UI testing complete: %d/%d", ui_completed, ui_total)
401
421
 
402
422
 
423
+ def get_ready_milestones(plan: state_module.Plan) -> list[state_module.Milestone]:
424
+ """Return all pending milestones whose dependencies are all completed or skipped."""
425
+ done_statuses = {"completed", "skipped"}
426
+ done_ids = {m.id for m in plan.milestones if m.status in done_statuses}
427
+ ready = []
428
+ for m in plan.milestones:
429
+ if m.status != "pending":
430
+ continue
431
+ if all(dep in done_ids for dep in m.depends_on):
432
+ ready.append(m)
433
+ return ready
434
+
435
+
436
+ def _process_milestone_verdict(verdict_result, result, milestone, plan, st, project_dir, args, logger, prompt):
437
+ """Process a single milestone's verdict (corrections, retries). Used by both sequential and turbo modes."""
438
+ if verdict_result.verdict == Verdict.RESOLVED:
439
+ milestone.status = "completed"
440
+ milestone.completed_at = datetime.now().isoformat()
441
+ state_module.save(st)
442
+ print(f"[Boris] Milestone {milestone.id} COMPLETE", flush=True)
443
+ logger.info("Milestone %s completed", milestone.id)
444
+
445
+ elif verdict_result.verdict == Verdict.OFF_PLAN:
446
+ correction_count = 0
447
+ resolved = False
448
+
449
+ while correction_count < MAX_CORRECTIONS:
450
+ correction_count += 1
451
+ print(
452
+ f" [Boris] Off-plan detected. Correction attempt {correction_count}/{MAX_CORRECTIONS}...",
453
+ flush=True,
454
+ )
455
+ logger.info(
456
+ "Off-plan correction attempt %d for milestone %s",
457
+ correction_count,
458
+ milestone.id,
459
+ )
460
+
461
+ correction_prompt = prompts.craft_correction(
462
+ result.output, milestone, plan, verdict_result.reason
463
+ )
464
+ result = engine.run(correction_prompt, project_dir, args.max_iter, milestone=milestone)
465
+ verdict_result = engine.check(result, milestone)
466
+
467
+ if verdict_result.verdict == Verdict.RESOLVED:
468
+ milestone.status = "completed"
469
+ milestone.completed_at = datetime.now().isoformat()
470
+ state_module.save(st)
471
+ print(f"[Boris] Milestone {milestone.id} COMPLETE (after correction)", flush=True)
472
+ logger.info("Milestone %s completed after correction", milestone.id)
473
+ resolved = True
474
+ break
475
+
476
+ if not resolved:
477
+ print(f" [Boris] Milestone {milestone.id} could not be corrected. Skipping.", flush=True)
478
+ logger.warning("Milestone %s skipped after failed corrections", milestone.id)
479
+ milestone.status = "skipped"
480
+ state_module.save(st)
481
+
482
+ elif verdict_result.verdict == Verdict.FAILED:
483
+ retry_count = 0
484
+ resolved = False
485
+
486
+ while retry_count < MAX_RETRIES:
487
+ retry_count += 1
488
+ milestone.retry_count = retry_count
489
+ print(
490
+ f" [Boris] Failed. Retry {retry_count}/{MAX_RETRIES}...",
491
+ flush=True,
492
+ )
493
+ logger.info(
494
+ "Retry %d for milestone %s", retry_count, milestone.id
495
+ )
496
+
497
+ result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
498
+ verdict_result = engine.check(result, milestone)
499
+
500
+ if verdict_result.verdict == Verdict.RESOLVED:
501
+ milestone.status = "completed"
502
+ milestone.completed_at = datetime.now().isoformat()
503
+ state_module.save(st)
504
+ print(f"[Boris] Milestone {milestone.id} COMPLETE (after retry)", flush=True)
505
+ logger.info("Milestone %s completed after retry", milestone.id)
506
+ resolved = True
507
+ break
508
+
509
+ if not resolved:
510
+ print(
511
+ f" [Boris] Milestone {milestone.id} failed after {MAX_RETRIES} retries. Skipping.",
512
+ flush=True,
513
+ )
514
+ logger.warning("Milestone %s skipped after failed retries", milestone.id)
515
+ milestone.status = "skipped"
516
+ state_module.save(st)
517
+
518
+
403
519
  def main():
404
520
  """Main Boris orchestration loop."""
405
521
  args = parse_args()
522
+
523
+ # --stop-at implies incremental mode
524
+ if args.stop_at:
525
+ args.incremental = True
526
+
406
527
  logger = setup_logging()
407
528
 
408
529
  # Create required dirs
@@ -427,21 +548,66 @@ def main():
427
548
  else:
428
549
  print(f"[Boris] Resuming from milestone {start_index + 1}...", flush=True)
429
550
  logger.info("Resuming from milestone %d", start_index + 1)
551
+ elif args.exec_only:
552
+ # Exec-only mode - load an existing plan file and execute it
553
+ plan_file = os.path.abspath(args.exec_only)
554
+ print_banner()
555
+ print(f"[Boris] Exec-only mode: loading plan from {plan_file}", flush=True)
556
+ logger.info("Exec-only mode: loading plan from %s", plan_file)
557
+
558
+ try:
559
+ plan = prompts.load_plan_from_file(plan_file)
560
+ except (FileNotFoundError, RuntimeError) as e:
561
+ print(f"[Boris] Error loading plan file: {e}", flush=True)
562
+ logger.error("Failed to load plan file: %s", e)
563
+ sys.exit(1)
564
+
565
+ print(f"[Boris] Plan loaded with {len(plan.milestones)} milestones", flush=True)
566
+ logger.info("Plan loaded with %d milestones", len(plan.milestones))
567
+
568
+ print_plan_summary(plan)
569
+
570
+ # Create initial state from the loaded plan
571
+ st = state_module.State(
572
+ plan=plan,
573
+ current_milestone_index=0,
574
+ project_dir=project_dir,
575
+ git_remote=args.remote,
576
+ no_git=args.no_git,
577
+ turbo=args.turbo,
578
+ )
579
+ state_module.save(st)
580
+ start_index = 0
430
581
  else:
431
- # New task mode
432
- if not args.task:
433
- print("[Boris] Error: task is required (unless using --resume)", flush=True)
582
+ # New task mode - resolve task from args or file
583
+ task = args.task
584
+ if args.file:
585
+ try:
586
+ with open(args.file, "r", encoding="utf-8") as f:
587
+ task = f.read().strip()
588
+ print(f"[Boris] Read task from file: {args.file} ({len(task)} chars)", flush=True)
589
+ except FileNotFoundError:
590
+ print(f"[Boris] Error: File not found: {args.file}", flush=True)
591
+ sys.exit(1)
592
+ except Exception as e:
593
+ print(f"[Boris] Error reading file: {e}", flush=True)
594
+ sys.exit(1)
595
+
596
+ if not task:
597
+ print("[Boris] Error: task is required (pass as argument or use -f FILE)", flush=True)
434
598
  sys.exit(1)
435
599
 
600
+ args.task = task
601
+
436
602
  print_banner()
437
- logger.info("Starting Boris for task: %s", args.task)
603
+ logger.info("Starting Boris for task: %s", args.task[:200])
438
604
 
439
605
  # Create plan (retry once on timeout)
440
606
  print("[Boris] Creating plan...", flush=True)
441
607
  plan = None
442
608
  for attempt in range(2):
443
609
  try:
444
- plan = prompts.create_plan(args.task, project_dir)
610
+ plan = prompts.create_plan(args.task, project_dir, turbo=args.turbo)
445
611
  break
446
612
  except RuntimeError as e:
447
613
  if attempt == 0 and "timed out" in str(e).lower():
@@ -470,6 +636,7 @@ def main():
470
636
  project_dir=project_dir,
471
637
  git_remote=args.remote,
472
638
  no_git=args.no_git,
639
+ turbo=args.turbo,
473
640
  )
474
641
  state_module.save(st)
475
642
  start_index = 0
@@ -491,156 +658,304 @@ def main():
491
658
  print(f"[Boris] Setting up remote: {args.remote}", flush=True)
492
659
  git_manager.setup_remote(project_dir, args.remote)
493
660
 
661
+ # Determine turbo mode (from state on resume, from args on new run)
662
+ turbo = getattr(st, "turbo", False) or getattr(args, "turbo", False)
663
+
494
664
  # Phase 1: Structural milestones (skip if resuming into UI phase)
495
665
  try:
496
666
  if st.phase != "ui_testing":
497
- for i in range(start_index, len(plan.milestones)):
498
- milestone = plan.milestones[i]
499
-
500
- if milestone.status == "completed":
501
- logger.info("Skipping already completed milestone %s", milestone.id)
502
- continue
503
-
504
- if milestone.status == "skipped":
505
- logger.info("Skipping previously skipped milestone %s", milestone.id)
506
- continue
507
-
667
+ if turbo:
668
+ # === TURBO MODE: Parallel DaveLoop execution ===
508
669
  print(flush=True)
509
- print(f"=== Milestone {milestone.id}: {milestone.title} ===", flush=True)
510
- logger.info("Starting milestone %s: %s", milestone.id, milestone.title)
511
-
512
- # Mark in progress
513
- milestone.status = "in_progress"
514
- st.current_milestone_index = i
515
- state_module.save(st)
670
+ print("[Boris] TURBO MODE: Parallel DaveLoop execution enabled", flush=True)
671
+ logger.info("TURBO MODE enabled")
672
+
673
+ batch_num = 0
674
+ while True:
675
+ ready = get_ready_milestones(plan)
676
+ if not ready:
677
+ # Check if there are still pending milestones (deadlock detection)
678
+ pending = [m for m in plan.milestones if m.status == "pending"]
679
+ if pending:
680
+ pending_ids = [m.id for m in pending]
681
+ print(f"[Boris] TURBO: No milestones ready but {len(pending)} still pending: {pending_ids}", flush=True)
682
+ print("[Boris] TURBO: Possible dependency deadlock. Skipping remaining.", flush=True)
683
+ logger.warning("Turbo deadlock: pending=%s", pending_ids)
684
+ for m in pending:
685
+ m.status = "skipped"
686
+ state_module.save(st)
687
+ break
516
688
 
517
- # Craft prompt and execute
518
- print(f"[Boris] Crafting prompt for {milestone.id}...", flush=True)
519
- prompt = prompts.craft_prompt(milestone, plan, project_dir)
520
- print(f"[Boris] Prompt ready ({len(prompt)} chars). Spawning DaveLoop...", flush=True)
521
- result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
522
-
523
- # Check verdict
524
- print(f"[Boris] DaveLoop finished. Checking verdict for {milestone.id}...", flush=True)
525
- verdict_result = engine.check(result, milestone)
526
- logger.info(
527
- "Milestone %s verdict: %s - %s",
528
- milestone.id,
529
- verdict_result.verdict.value,
530
- verdict_result.reason,
531
- )
689
+ batch_num += 1
690
+ batch_ids = [m.id for m in ready]
691
+ print(flush=True)
692
+ print(f"[Boris] TURBO: Launching batch {batch_num} of {len(ready)} parallel DaveLoops: {batch_ids}", flush=True)
693
+ logger.info("TURBO batch %d: %s", batch_num, batch_ids)
532
694
 
533
- if verdict_result.verdict == Verdict.RESOLVED:
534
- milestone.status = "completed"
535
- milestone.completed_at = datetime.now().isoformat()
695
+ # Mark all in batch as in_progress
696
+ for m in ready:
697
+ m.status = "in_progress"
536
698
  state_module.save(st)
537
699
 
538
- if not st.no_git:
539
- print(f"[Boris] Committing milestone {milestone.id} to git...", flush=True)
540
- if git_manager.commit_milestone(project_dir, milestone):
541
- logger.info("Git commit succeeded for milestone %s", milestone.id)
542
- else:
543
- logger.warning("Git commit FAILED for milestone %s", milestone.id)
544
- print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
545
-
546
- print(f"[Boris] Milestone {milestone.id} COMPLETE", flush=True)
547
- logger.info("Milestone %s completed", milestone.id)
548
-
549
- elif verdict_result.verdict == Verdict.OFF_PLAN:
550
- # Attempt correction
551
- correction_count = 0
552
- resolved = False
553
-
554
- while correction_count < MAX_CORRECTIONS:
555
- correction_count += 1
556
- print(
557
- f" [Boris] Off-plan detected. Correction attempt {correction_count}/{MAX_CORRECTIONS}...",
558
- flush=True,
559
- )
700
+ # Craft prompts for all milestones in batch
701
+ tasks = []
702
+ prompt_map = {} # milestone.id -> prompt, for retries
703
+ for m in ready:
704
+ print(f"[Boris] TURBO: Crafting prompt for {m.id}...", flush=True)
705
+ # Compute sibling milestones (all others in this batch)
706
+ siblings = [sib for sib in ready if sib.id != m.id]
707
+ prompt = prompts.craft_prompt(m, plan, project_dir,
708
+ turbo=True, sibling_milestones=siblings)
709
+ print(f"[Boris] TURBO: Prompt ready for {m.id} ({len(prompt)} chars)", flush=True)
710
+ tasks.append((prompt, m))
711
+ prompt_map[m.id] = prompt
712
+
713
+ # Run all DaveLoops in parallel
714
+ parallel_results = engine.run_parallel(tasks, project_dir, args.max_iter)
715
+
716
+ # Process verdicts sequentially (corrections/retries run sequentially)
717
+ batch_summary = {}
718
+ for milestone, result in parallel_results:
719
+ print(f"[Boris] TURBO: Checking verdict for {milestone.id}...", flush=True)
720
+ verdict_result = engine.check(result, milestone)
560
721
  logger.info(
561
- "Off-plan correction attempt %d for milestone %s",
562
- correction_count,
722
+ "TURBO milestone %s verdict: %s - %s",
563
723
  milestone.id,
724
+ verdict_result.verdict.value,
725
+ verdict_result.reason,
564
726
  )
565
727
 
566
- correction_prompt = prompts.craft_correction(
567
- result.output, milestone, plan, verdict_result.reason
728
+ _process_milestone_verdict(
729
+ verdict_result, result, milestone, plan, st,
730
+ project_dir, args, logger, prompt_map[milestone.id]
568
731
  )
569
- result = engine.run(correction_prompt, project_dir, args.max_iter, milestone=milestone)
570
- verdict_result = engine.check(result, milestone)
732
+ batch_summary[milestone.id] = milestone.status.upper()
571
733
 
572
- if verdict_result.verdict == Verdict.RESOLVED:
573
- milestone.status = "completed"
574
- milestone.completed_at = datetime.now().isoformat()
575
- state_module.save(st)
576
-
577
- if not st.no_git:
578
- print(f"[Boris] Committing corrected milestone {milestone.id} to git...", flush=True)
734
+ # Git commits sequentially after entire batch
735
+ if not st.no_git:
736
+ for milestone, result in parallel_results:
737
+ if milestone.status == "completed":
738
+ print(f"[Boris] TURBO: Committing {milestone.id} to git...", flush=True)
579
739
  if git_manager.commit_milestone(project_dir, milestone):
580
- logger.info("Git commit succeeded for milestone %s (after correction)", milestone.id)
740
+ logger.info("Git commit succeeded for milestone %s", milestone.id)
581
741
  else:
582
- logger.warning("Git commit FAILED for milestone %s (after correction)", milestone.id)
742
+ logger.warning("Git commit FAILED for milestone %s", milestone.id)
583
743
  print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
584
744
 
585
- print(f"[Boris] Milestone {milestone.id} COMPLETE (after correction)", flush=True)
586
- logger.info("Milestone %s completed after correction", milestone.id)
587
- resolved = True
588
- break
589
-
590
- if not resolved:
591
- print(f" [Boris] Milestone {milestone.id} could not be corrected. Skipping.", flush=True)
592
- logger.warning("Milestone %s skipped after failed corrections", milestone.id)
593
- milestone.status = "skipped"
594
- state_module.save(st)
745
+ # Save state after whole batch
746
+ state_module.save(st)
595
747
 
596
- elif verdict_result.verdict == Verdict.FAILED:
597
- # Retry logic
598
- retry_count = 0
599
- resolved = False
600
-
601
- while retry_count < MAX_RETRIES:
602
- retry_count += 1
603
- milestone.retry_count = retry_count
604
- print(
605
- f" [Boris] Failed. Retry {retry_count}/{MAX_RETRIES}...",
606
- flush=True,
607
- )
608
- logger.info(
609
- "Retry %d for milestone %s", retry_count, milestone.id
610
- )
748
+ # Print batch summary
749
+ summary_parts = [f"{mid}={status}" for mid, status in batch_summary.items()]
750
+ print(f"[Boris] TURBO: Batch {batch_num} complete. Results: {', '.join(summary_parts)}", flush=True)
751
+ logger.info("TURBO batch %d complete: %s", batch_num, batch_summary)
752
+
753
+ # Incremental / --stop-at: pause after batch in turbo
754
+ should_pause = False
755
+ if args.stop_at:
756
+ # Pause only if the --stop-at milestone was in this batch
757
+ batch_completed_ids = {mid for mid, status in batch_summary.items()
758
+ if status in ("COMPLETED", "SKIPPED")}
759
+ if args.stop_at in batch_completed_ids:
760
+ should_pause = True
761
+ elif args.incremental:
762
+ should_pause = True
763
+
764
+ if should_pause:
765
+ remaining = sum(1 for m in plan.milestones if m.status == "pending")
766
+ if remaining > 0:
767
+ print(flush=True)
768
+ if args.stop_at:
769
+ print(f"[Boris] --stop-at {args.stop_at}: pausing after batch {batch_num}.", flush=True)
770
+ else:
771
+ print(f"[Boris] Incremental mode: pausing after batch {batch_num}.", flush=True)
772
+ print(f"[Boris] {remaining} milestone(s) remaining.", flush=True)
773
+ resume_flags = f"-r --turbo -d \"{project_dir}\""
774
+ if args.stop_at:
775
+ resume_flags += f" --stop-at {args.stop_at}"
776
+ elif args.incremental:
777
+ resume_flags += " --incremental"
778
+ print(f"[Boris] Resume with: boris {resume_flags}", flush=True)
779
+ logger.info("Pausing after batch %d. %d remaining.", batch_num, remaining)
780
+ state_module.save(st)
781
+ sys.exit(0)
782
+ else:
783
+ # === SEQUENTIAL MODE (original behavior) ===
784
+ for i in range(start_index, len(plan.milestones)):
785
+ milestone = plan.milestones[i]
786
+
787
+ if milestone.status == "completed":
788
+ logger.info("Skipping already completed milestone %s", milestone.id)
789
+ continue
790
+
791
+ if milestone.status == "skipped":
792
+ logger.info("Skipping previously skipped milestone %s", milestone.id)
793
+ continue
794
+
795
+ print(flush=True)
796
+ print(f"=== Milestone {milestone.id}: {milestone.title} ===", flush=True)
797
+ logger.info("Starting milestone %s: %s", milestone.id, milestone.title)
798
+
799
+ # Mark in progress
800
+ milestone.status = "in_progress"
801
+ st.current_milestone_index = i
802
+ state_module.save(st)
611
803
 
612
- result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
613
- verdict_result = engine.check(result, milestone)
804
+ # Craft prompt and execute
805
+ print(f"[Boris] Crafting prompt for {milestone.id}...", flush=True)
806
+ prompt = prompts.craft_prompt(milestone, plan, project_dir)
807
+ print(f"[Boris] Prompt ready ({len(prompt)} chars). Spawning DaveLoop...", flush=True)
808
+ result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
809
+
810
+ # Check verdict
811
+ print(f"[Boris] DaveLoop finished. Checking verdict for {milestone.id}...", flush=True)
812
+ verdict_result = engine.check(result, milestone)
813
+ logger.info(
814
+ "Milestone %s verdict: %s - %s",
815
+ milestone.id,
816
+ verdict_result.verdict.value,
817
+ verdict_result.reason,
818
+ )
819
+
820
+ if verdict_result.verdict == Verdict.RESOLVED:
821
+ milestone.status = "completed"
822
+ milestone.completed_at = datetime.now().isoformat()
823
+ state_module.save(st)
614
824
 
615
- if verdict_result.verdict == Verdict.RESOLVED:
616
- milestone.status = "completed"
617
- milestone.completed_at = datetime.now().isoformat()
825
+ if not st.no_git:
826
+ print(f"[Boris] Committing milestone {milestone.id} to git...", flush=True)
827
+ if git_manager.commit_milestone(project_dir, milestone):
828
+ logger.info("Git commit succeeded for milestone %s", milestone.id)
829
+ else:
830
+ logger.warning("Git commit FAILED for milestone %s", milestone.id)
831
+ print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
832
+
833
+ print(f"[Boris] Milestone {milestone.id} COMPLETE", flush=True)
834
+ logger.info("Milestone %s completed", milestone.id)
835
+
836
+ elif verdict_result.verdict == Verdict.OFF_PLAN:
837
+ # Attempt correction
838
+ correction_count = 0
839
+ resolved = False
840
+
841
+ while correction_count < MAX_CORRECTIONS:
842
+ correction_count += 1
843
+ print(
844
+ f" [Boris] Off-plan detected. Correction attempt {correction_count}/{MAX_CORRECTIONS}...",
845
+ flush=True,
846
+ )
847
+ logger.info(
848
+ "Off-plan correction attempt %d for milestone %s",
849
+ correction_count,
850
+ milestone.id,
851
+ )
852
+
853
+ correction_prompt = prompts.craft_correction(
854
+ result.output, milestone, plan, verdict_result.reason
855
+ )
856
+ result = engine.run(correction_prompt, project_dir, args.max_iter, milestone=milestone)
857
+ verdict_result = engine.check(result, milestone)
858
+
859
+ if verdict_result.verdict == Verdict.RESOLVED:
860
+ milestone.status = "completed"
861
+ milestone.completed_at = datetime.now().isoformat()
862
+ state_module.save(st)
863
+
864
+ if not st.no_git:
865
+ print(f"[Boris] Committing corrected milestone {milestone.id} to git...", flush=True)
866
+ if git_manager.commit_milestone(project_dir, milestone):
867
+ logger.info("Git commit succeeded for milestone %s (after correction)", milestone.id)
868
+ else:
869
+ logger.warning("Git commit FAILED for milestone %s (after correction)", milestone.id)
870
+ print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
871
+
872
+ print(f"[Boris] Milestone {milestone.id} COMPLETE (after correction)", flush=True)
873
+ logger.info("Milestone %s completed after correction", milestone.id)
874
+ resolved = True
875
+ break
876
+
877
+ if not resolved:
878
+ print(f" [Boris] Milestone {milestone.id} could not be corrected. Skipping.", flush=True)
879
+ logger.warning("Milestone %s skipped after failed corrections", milestone.id)
880
+ milestone.status = "skipped"
618
881
  state_module.save(st)
619
882
 
620
- if not st.no_git:
621
- print(f"[Boris] Committing retried milestone {milestone.id} to git...", flush=True)
622
- if git_manager.commit_milestone(project_dir, milestone):
623
- logger.info("Git commit succeeded for milestone %s (after retry)", milestone.id)
624
- else:
625
- logger.warning("Git commit FAILED for milestone %s (after retry)", milestone.id)
626
- print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
627
-
628
- print(f"[Boris] Milestone {milestone.id} COMPLETE (after retry)", flush=True)
629
- logger.info("Milestone %s completed after retry", milestone.id)
630
- resolved = True
631
- break
883
+ elif verdict_result.verdict == Verdict.FAILED:
884
+ # Retry logic
885
+ retry_count = 0
886
+ resolved = False
887
+
888
+ while retry_count < MAX_RETRIES:
889
+ retry_count += 1
890
+ milestone.retry_count = retry_count
891
+ print(
892
+ f" [Boris] Failed. Retry {retry_count}/{MAX_RETRIES}...",
893
+ flush=True,
894
+ )
895
+ logger.info(
896
+ "Retry %d for milestone %s", retry_count, milestone.id
897
+ )
898
+
899
+ result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
900
+ verdict_result = engine.check(result, milestone)
901
+
902
+ if verdict_result.verdict == Verdict.RESOLVED:
903
+ milestone.status = "completed"
904
+ milestone.completed_at = datetime.now().isoformat()
905
+ state_module.save(st)
906
+
907
+ if not st.no_git:
908
+ print(f"[Boris] Committing retried milestone {milestone.id} to git...", flush=True)
909
+ if git_manager.commit_milestone(project_dir, milestone):
910
+ logger.info("Git commit succeeded for milestone %s (after retry)", milestone.id)
911
+ else:
912
+ logger.warning("Git commit FAILED for milestone %s (after retry)", milestone.id)
913
+ print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
914
+
915
+ print(f"[Boris] Milestone {milestone.id} COMPLETE (after retry)", flush=True)
916
+ logger.info("Milestone %s completed after retry", milestone.id)
917
+ resolved = True
918
+ break
919
+
920
+ if not resolved:
921
+ print(
922
+ f" [Boris] Milestone {milestone.id} failed after {MAX_RETRIES} retries. Skipping.",
923
+ flush=True,
924
+ )
925
+ logger.warning("Milestone %s skipped after failed retries", milestone.id)
926
+ milestone.status = "skipped"
927
+ state_module.save(st)
632
928
 
633
- if not resolved:
634
- print(
635
- f" [Boris] Milestone {milestone.id} failed after {MAX_RETRIES} retries. Skipping.",
636
- flush=True,
637
- )
638
- logger.warning("Milestone %s skipped after failed retries", milestone.id)
639
- milestone.status = "skipped"
640
- state_module.save(st)
929
+ # Save state after each milestone outcome
930
+ state_module.save(st)
641
931
 
642
- # Save state after each milestone outcome
643
- state_module.save(st)
932
+ # Incremental mode: pause after each milestone
933
+ # --stop-at: only pause when the target milestone completes
934
+ should_pause = False
935
+ if args.stop_at and milestone.status in ("completed", "skipped"):
936
+ if milestone.id == args.stop_at:
937
+ should_pause = True
938
+ elif args.incremental and milestone.status in ("completed", "skipped"):
939
+ should_pause = True
940
+
941
+ if should_pause:
942
+ remaining = sum(1 for m in plan.milestones if m.status == "pending")
943
+ if remaining > 0:
944
+ print(flush=True)
945
+ if args.stop_at:
946
+ print(f"[Boris] --stop-at {args.stop_at}: pausing after {milestone.id}.", flush=True)
947
+ else:
948
+ print(f"[Boris] Incremental mode: pausing after {milestone.id}.", flush=True)
949
+ print(f"[Boris] {remaining} milestone(s) remaining.", flush=True)
950
+ resume_flags = f"-r -d \"{project_dir}\""
951
+ if args.stop_at:
952
+ resume_flags += f" --stop-at {args.stop_at}"
953
+ elif args.incremental:
954
+ resume_flags += " --incremental"
955
+ print(f"[Boris] Resume with: boris {resume_flags}", flush=True)
956
+ logger.info("Pausing after %s. %d remaining.", milestone.id, remaining)
957
+ state_module.save(st)
958
+ sys.exit(0)
644
959
 
645
960
  # Phase 2: UI Testing & Polish
646
961
  _run_ui_phase(st, plan, project_dir, args, logger)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: borisxdave
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Boris - Autonomous Project Orchestrator
5
5
  Requires-Python: >=3.8
6
6
 
@@ -0,0 +1,12 @@
1
+ boris.py,sha256=xStvDOxKqbvQiNsQORr4qcD5wEiruNIcm7v_fFPjwZ8,44667
2
+ config.py,sha256=KfFKyCGasdm1yBvIRFv-ykzA_oRo-zu1Euu9YC7V1Cg,324
3
+ engine.py,sha256=LuVUwtlE1j8K3MOIH_pozB6Vy04_kPJs7Q0l9g-ePa8,27875
4
+ git_manager.py,sha256=BuuTT4naPb5-jLhOik1xHM2ztzuKvJ_bnecZmlYgwFs,8493
5
+ planner.py,sha256=UrU--kBvzvyD1gOVxIn-kdbJiu8tt4rcowsln66WkGw,5670
6
+ prompts.py,sha256=oBVDZ0dG50FcSBa4F9GTkCPxmMB653BsK3RShoGcWUM,34182
7
+ state.py,sha256=8MeUiDwL9ecgD9N6RhNUQf_P-qE7HeG3WlgL_SbRQic,3160
8
+ borisxdave-0.3.0.dist-info/METADATA,sha256=qP3P-mpjm-BaQuJBXasveJ4L_wGNH9S-pIhNLAdNVd4,133
9
+ borisxdave-0.3.0.dist-info/WHEEL,sha256=hPN0AlP2dZM_3ZJZWP4WooepkmU9wzjGgCLCeFjkHLA,92
10
+ borisxdave-0.3.0.dist-info/entry_points.txt,sha256=a6FLWgxiQjGMJIRSV5sDxaaaaQchunm04ZuzX8N7-6I,61
11
+ borisxdave-0.3.0.dist-info/top_level.txt,sha256=zNzzkbLJWzWpTjJTQsnbdOBypcA4XpioE1dEgWZVBx4,54
12
+ borisxdave-0.3.0.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  boris = boris:main
3
+ borisxdave = boris:main
engine.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Boris engine - execution and monitoring (merged from executor + monitor)."""
2
+ import concurrent.futures
2
3
  import enum
3
4
  import logging
4
5
  import os
@@ -413,6 +414,32 @@ def _setup_log(project_dir: str) -> str:
413
414
  return os.path.join(_LOGS_DIR, f"boris_exec_{timestamp}.log")
414
415
 
415
416
 
417
+ def run_parallel(tasks: list, project_dir: str, max_iterations: int = None) -> list:
418
+ """Run multiple DaveLoop instances in parallel using ThreadPoolExecutor.
419
+
420
+ Args:
421
+ tasks: List of (prompt, milestone) tuples.
422
+ project_dir: Working directory for the project.
423
+ max_iterations: Max DaveLoop iterations per milestone.
424
+
425
+ Returns:
426
+ List of (milestone, ExecutionResult) tuples, one per input task.
427
+ """
428
+ results = []
429
+
430
+ def _run_one(prompt_milestone):
431
+ prompt, milestone = prompt_milestone
432
+ result = run(prompt, project_dir, max_iterations, milestone=milestone)
433
+ return (milestone, result)
434
+
435
+ with concurrent.futures.ThreadPoolExecutor(max_workers=len(tasks)) as executor:
436
+ futures = {executor.submit(_run_one, t): t for t in tasks}
437
+ for future in concurrent.futures.as_completed(futures):
438
+ results.append(future.result())
439
+
440
+ return results
441
+
442
+
416
443
  # --- Monitoring (from monitor.py) ---
417
444
 
418
445
 
planner.py CHANGED
@@ -43,7 +43,7 @@ def create_plan(task: str, project_dir: str) -> Plan:
43
43
 
44
44
  try:
45
45
  result = subprocess.run(
46
- [config.CLAUDE_CMD, "-p"],
46
+ [config.CLAUDE_CMD, "-p", "--output-format", "text"],
47
47
  input=prompt,
48
48
  capture_output=True,
49
49
  text=True,
@@ -63,6 +63,14 @@ def create_plan(task: str, project_dir: str) -> Plan:
63
63
  response = re.sub(r"\n?```\s*$", "", response)
64
64
  response = response.strip()
65
65
 
66
+ # Try to extract JSON array from response if it contains extra text
67
+ if not response.startswith("["):
68
+ match = re.search(r"\[.*\]", response, re.DOTALL)
69
+ if match:
70
+ response = match.group(0)
71
+ else:
72
+ logger.error("Claude response (no JSON found): %s", response[:500])
73
+
66
74
  milestones_data = json.loads(response)
67
75
 
68
76
  milestones = [
prompts.py CHANGED
@@ -87,14 +87,45 @@ def _list_project_files(project_dir: str, max_files: int = 500) -> str:
87
87
  # --- Planning (from planner.py) ---
88
88
 
89
89
 
90
- def create_plan(task: str, project_dir: str) -> Plan:
91
- """Break a task into milestones using Claude CLI."""
90
+ def create_plan(task: str, project_dir: str, turbo: bool = False) -> Plan:
91
+ """Break a task into milestones using Claude CLI.
92
+
93
+ Args:
94
+ task: The task description.
95
+ project_dir: Path to the project directory.
96
+ turbo: When True, emphasize minimal dependencies and maximum parallelism.
97
+ """
92
98
  file_listing = _list_project_files(project_dir)
93
99
 
100
+ if turbo:
101
+ dependency_instructions = (
102
+ "CRITICAL DEPENDENCY RULES (turbo/parallel mode):\n"
103
+ "- Only add a dependency if the milestone TRULY requires output from another milestone to function.\n"
104
+ "- Two milestones that share a common dependency should NOT depend on each other unless one genuinely needs the other's output.\n"
105
+ "- Maximize parallelism in the dependency graph. Independent features, pages, or modules that share the same foundation should run in parallel.\n"
106
+ "- Do NOT chain milestones linearly unless there is a real data/code dependency between them.\n\n"
107
+ "Example of WRONG dependencies (overly sequential):\n"
108
+ ' M1: Project Setup (depends_on: [])\n'
109
+ ' M2: Auth Backend (depends_on: ["M1"])\n'
110
+ ' M3: Login Page (depends_on: ["M2"])\n'
111
+ ' M4: Dashboard Page (depends_on: ["M3"]) <-- WRONG: only needs M2, not M3\n'
112
+ ' M5: Profile Page (depends_on: ["M4"]) <-- WRONG: only needs M2, not M4\n\n'
113
+ "Example of CORRECT dependencies (maximizes parallelism):\n"
114
+ ' M1: Project Setup (depends_on: [])\n'
115
+ ' M2: Auth Backend (depends_on: ["M1"])\n'
116
+ ' M3: Login Page (depends_on: ["M2"])\n'
117
+ ' M4: Dashboard Page (depends_on: ["M2"]) <-- parallel with M3\n'
118
+ ' M5: Profile Page (depends_on: ["M2"]) <-- parallel with M3, M4\n\n'
119
+ )
120
+ else:
121
+ dependency_instructions = (
122
+ "Order by dependency: foundation first, then features that depend on it.\n"
123
+ )
124
+
94
125
  prompt = (
95
126
  "You are a technical project planner. Break this task into ordered milestones.\n"
96
127
  "Each milestone must be a concrete, buildable unit that DaveLoop can execute independently.\n"
97
- "Order by dependency: foundation first, then features that depend on it.\n\n"
128
+ f"{dependency_instructions}\n"
98
129
  f"Task: {task}\n"
99
130
  f"Project directory: {project_dir}\n"
100
131
  f"Existing files:\n{file_listing}\n\n"
@@ -279,14 +310,116 @@ def _save_plan_markdown(plan: Plan, task: str) -> str:
279
310
  return filepath
280
311
 
281
312
 
313
+ def load_plan_from_file(filepath: str) -> Plan:
314
+ """Parse a saved plan markdown file back into a Plan object.
315
+
316
+ Reads the markdown format produced by _save_plan_markdown and reconstructs
317
+ the Plan with its Milestone objects.
318
+ """
319
+ if not os.path.exists(filepath):
320
+ raise FileNotFoundError(f"Plan file not found: {filepath}")
321
+
322
+ with open(filepath, "r", encoding="utf-8") as f:
323
+ content = f.read()
324
+
325
+ # Extract task from **Task:** line
326
+ task_match = re.search(r"\*\*Task:\*\*\s*(.+)", content)
327
+ task = task_match.group(1).strip() if task_match else "Unknown task"
328
+
329
+ # Extract created_at from **Created:** line
330
+ created_match = re.search(r"\*\*Created:\*\*\s*(.+)", content)
331
+ created_at = created_match.group(1).strip() if created_match else datetime.now().isoformat()
332
+
333
+ # Split into milestone sections by ## M<N>: headers
334
+ milestone_sections = re.split(r"(?=^## M\d+:)", content, flags=re.MULTILINE)
335
+
336
+ milestones = []
337
+ for section in milestone_sections:
338
+ # Only process sections that start with a milestone header
339
+ header_match = re.match(r"^## (M\d+):\s*(.+)", section)
340
+ if not header_match:
341
+ continue
342
+
343
+ m_id = header_match.group(1)
344
+ m_title = header_match.group(2).strip()
345
+
346
+ # Extract description: text between the header and the first **bold** field or ---
347
+ # Skip the header line, then collect lines until we hit a **field:** or ---
348
+ lines = section.split("\n")
349
+ desc_lines = []
350
+ in_desc = False
351
+ for line in lines[1:]: # skip header line
352
+ stripped = line.strip()
353
+ if not stripped:
354
+ if in_desc:
355
+ desc_lines.append("")
356
+ continue
357
+ if stripped.startswith("**") or stripped == "---":
358
+ break
359
+ desc_lines.append(stripped)
360
+ in_desc = True
361
+ description = "\n".join(desc_lines).strip()
362
+
363
+ # Extract depends_on
364
+ depends_match = re.search(r"\*\*Depends on:\*\*\s*(.+)", section)
365
+ depends_on = []
366
+ if depends_match:
367
+ deps_str = depends_match.group(1).strip()
368
+ depends_on = [d.strip() for d in deps_str.split(",") if d.strip()]
369
+
370
+ # Extract acceptance criteria (bulleted list after **Acceptance Criteria:**)
371
+ criteria = []
372
+ criteria_match = re.search(r"\*\*Acceptance Criteria:\*\*\s*\n((?:\s*- .+\n?)+)", section)
373
+ if criteria_match:
374
+ for line in criteria_match.group(1).strip().split("\n"):
375
+ line = line.strip()
376
+ if line.startswith("- "):
377
+ criteria.append(line[2:].strip())
378
+
379
+ # Extract files to create
380
+ files_create_match = re.search(r"\*\*Files to create:\*\*\s*(.+)", section)
381
+ files_to_create = []
382
+ if files_create_match:
383
+ files_to_create = [f.strip() for f in files_create_match.group(1).split(",") if f.strip()]
384
+
385
+ # Extract files to modify
386
+ files_modify_match = re.search(r"\*\*Files to modify:\*\*\s*(.+)", section)
387
+ files_to_modify = []
388
+ if files_modify_match:
389
+ files_to_modify = [f.strip() for f in files_modify_match.group(1).split(",") if f.strip()]
390
+
391
+ milestones.append(Milestone(
392
+ id=m_id,
393
+ title=m_title,
394
+ description=description,
395
+ depends_on=depends_on,
396
+ acceptance_criteria=criteria,
397
+ files_to_create=files_to_create,
398
+ files_to_modify=files_to_modify,
399
+ ))
400
+
401
+ if not milestones:
402
+ raise RuntimeError(f"No milestones found in plan file: {filepath}")
403
+
404
+ return Plan(task=task, milestones=milestones, created_at=created_at)
405
+
406
+
282
407
  # --- Prompt Crafting (from crafter.py) ---
283
408
 
284
409
 
285
- def craft_prompt(milestone: Milestone, plan: Plan, project_dir: str) -> str:
410
+ def craft_prompt(milestone: Milestone, plan: Plan, project_dir: str,
411
+ turbo: bool = False, sibling_milestones: list[Milestone] | None = None) -> str:
286
412
  """Build a detailed prompt for DaveLoop to execute a milestone.
287
413
 
288
414
  This is the most critical function in Boris. DaveLoop needs everything
289
415
  in ONE prompt - context, spec, integration points, acceptance criteria.
416
+
417
+ Args:
418
+ milestone: The milestone to build a prompt for.
419
+ plan: The full plan for context.
420
+ project_dir: Path to the project directory.
421
+ turbo: When True, adds parallel execution warnings.
422
+ sibling_milestones: Other milestones running concurrently in the same turbo batch.
290
423
  """
291
424
  sections = []
292
425
 
@@ -305,6 +438,37 @@ def craft_prompt(milestone: Milestone, plan: Plan, project_dir: str) -> str:
305
438
  sections.append("**Do NOT create or modify any other files.**")
306
439
  sections.append("")
307
440
 
441
+ # Turbo mode: parallel execution warning
442
+ if turbo and sibling_milestones:
443
+ sections.append("## PARALLEL EXECUTION WARNING")
444
+ sections.append("")
445
+ sections.append("**You are running in PARALLEL with other DaveLoops.** Other milestones")
446
+ sections.append("are being built AT THE SAME TIME by sibling DaveLoop processes.")
447
+ sections.append("Modifying files outside your scope WILL cause conflicts or silent overwrites.")
448
+ sections.append("")
449
+ sections.append("**Sibling milestones running concurrently:**")
450
+ for sib in sibling_milestones:
451
+ sib_files = (sib.files_to_create or []) + (sib.files_to_modify or [])
452
+ sib_files_str = ", ".join(sib_files) if sib_files else "(no specific files)"
453
+ sections.append(f"- **{sib.id}: {sib.title}** - Files: {sib_files_str}")
454
+ sections.append("")
455
+ # Collect all sibling files into a flat list for the explicit prohibition
456
+ all_sibling_files = []
457
+ for sib in sibling_milestones:
458
+ all_sibling_files.extend(sib.files_to_create or [])
459
+ all_sibling_files.extend(sib.files_to_modify or [])
460
+ if all_sibling_files:
461
+ unique_sibling_files = sorted(set(all_sibling_files))
462
+ sections.append("**DO NOT touch these files (owned by sibling milestones):**")
463
+ for sf in unique_sibling_files:
464
+ sections.append(f"- {sf}")
465
+ sections.append("")
466
+ sections.append("**Rules:**")
467
+ sections.append("1. ONLY modify files listed in YOUR files_to_create/files_to_modify scope above.")
468
+ sections.append("2. Do NOT modify shared config files (package.json, routing tables, etc.) unless they are explicitly in YOUR scope.")
469
+ sections.append("3. If you need a shared file changed, add a TODO comment instead of editing it.")
470
+ sections.append("")
471
+
308
472
  # What to build
309
473
  sections.append(f"## What to Build")
310
474
  sections.append(milestone.description)
state.py CHANGED
@@ -63,6 +63,7 @@ class State:
63
63
  updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
64
64
  phase: str = "structural" # structural | ui_testing
65
65
  ui_plan: Optional[UIPlan] = None
66
+ turbo: bool = False
66
67
 
67
68
 
68
69
  def save(state: State) -> None:
@@ -98,6 +99,7 @@ def load(project_dir: str) -> Optional[State]:
98
99
  no_git=data.get("no_git", False),
99
100
  started_at=data.get("started_at", ""),
100
101
  updated_at=data.get("updated_at", ""),
102
+ turbo=data.get("turbo", False),
101
103
  )
102
104
  except (json.JSONDecodeError, KeyError, TypeError):
103
105
  return None
@@ -1,12 +0,0 @@
1
- boris.py,sha256=0AvcqWPhvfoR1U8zUGhcnCJImpcy8cITcD4zzJpx9Jk,28715
2
- config.py,sha256=KfFKyCGasdm1yBvIRFv-ykzA_oRo-zu1Euu9YC7V1Cg,324
3
- engine.py,sha256=PGRtbnsvr6egikqo0ENpA8WnXKdQ1kW2XWUFgJ-AXzk,26889
4
- git_manager.py,sha256=BuuTT4naPb5-jLhOik1xHM2ztzuKvJ_bnecZmlYgwFs,8493
5
- planner.py,sha256=HHrw2lpcZhRBzmCapk8Uw8ivUpB7fSz2V6_FaJLHlU4,5297
6
- prompts.py,sha256=6QHAftDjDqk_e2WCq5NkTH5Jd41PgFk68fkg4nMsy-Q,26228
7
- state.py,sha256=erggbuwJu-Ks6Y7hLQpFwIEN4LjqzBPpy8kTLZtk0ZM,3092
8
- borisxdave-0.2.0.dist-info/METADATA,sha256=adf_nOv8TymIA4ez-Hb7EEzzDXaFkAmVMJ512pY21Po,133
9
- borisxdave-0.2.0.dist-info/WHEEL,sha256=hPN0AlP2dZM_3ZJZWP4WooepkmU9wzjGgCLCeFjkHLA,92
10
- borisxdave-0.2.0.dist-info/entry_points.txt,sha256=fuO7JxKFLOm6xp6m3JHRA1UO_QW1dYU-F0IooA1NqQs,37
11
- borisxdave-0.2.0.dist-info/top_level.txt,sha256=zNzzkbLJWzWpTjJTQsnbdOBypcA4XpioE1dEgWZVBx4,54
12
- borisxdave-0.2.0.dist-info/RECORD,,