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 +446 -131
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.0.dist-info}/METADATA +1 -1
- borisxdave-0.3.0.dist-info/RECORD +12 -0
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.0.dist-info}/entry_points.txt +1 -0
- engine.py +27 -0
- planner.py +9 -1
- prompts.py +168 -4
- state.py +2 -0
- borisxdave-0.2.0.dist-info/RECORD +0 -12
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.0.dist-info}/WHEEL +0 -0
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.0.dist-info}/top_level.txt +0 -0
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.
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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(
|
|
510
|
-
logger.info("
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
567
|
-
result
|
|
728
|
+
_process_milestone_verdict(
|
|
729
|
+
verdict_result, result, milestone, plan, st,
|
|
730
|
+
project_dir, args, logger, prompt_map[milestone.id]
|
|
568
731
|
)
|
|
569
|
-
|
|
570
|
-
verdict_result = engine.check(result, milestone)
|
|
732
|
+
batch_summary[milestone.id] = milestone.status.upper()
|
|
571
733
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|
740
|
+
logger.info("Git commit succeeded for milestone %s", milestone.id)
|
|
581
741
|
else:
|
|
582
|
-
logger.warning("Git commit FAILED for milestone %s
|
|
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
|
-
|
|
586
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
613
|
-
|
|
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
|
|
616
|
-
milestone.
|
|
617
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
print(
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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
|
-
|
|
643
|
-
|
|
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)
|
|
@@ -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,,
|
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
|
-
"
|
|
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
|
|
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,,
|
|
File without changes
|
|
File without changes
|