borisxdave 0.2.0__py3-none-any.whl → 0.3.1__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.
- Users/david/AppData/Local/Programs/Python/Python313/Lib/site-packages/boris_prompt.md +191 -0
- boris.py +871 -130
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.1.dist-info}/METADATA +1 -1
- borisxdave-0.3.1.dist-info/RECORD +14 -0
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.1.dist-info}/entry_points.txt +1 -0
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.1.dist-info}/top_level.txt +1 -0
- engine.py +229 -4
- file_lock.py +123 -0
- planner.py +9 -1
- prompts.py +191 -12
- state.py +82 -1
- borisxdave-0.2.0.dist-info/RECORD +0 -12
- {borisxdave-0.2.0.dist-info → borisxdave-0.3.1.dist-info}/WHEEL +0 -0
boris.py
CHANGED
|
@@ -28,6 +28,123 @@ DEFAULT_MAX_ITERATIONS = 15
|
|
|
28
28
|
MAX_CORRECTIONS = 2
|
|
29
29
|
MAX_RETRIES = 1
|
|
30
30
|
|
|
31
|
+
# Swarm mode configuration presets
|
|
32
|
+
SWARM_PRESETS = {
|
|
33
|
+
"conservative": {
|
|
34
|
+
"swarm_budget": 3,
|
|
35
|
+
"swarm_depth": 1,
|
|
36
|
+
"isolation": "worktree",
|
|
37
|
+
"no_converge": False,
|
|
38
|
+
},
|
|
39
|
+
"balanced": {
|
|
40
|
+
"swarm_budget": 5,
|
|
41
|
+
"swarm_depth": 1,
|
|
42
|
+
"isolation": "worktree",
|
|
43
|
+
"no_converge": False,
|
|
44
|
+
},
|
|
45
|
+
"aggressive": {
|
|
46
|
+
"swarm_budget": 10,
|
|
47
|
+
"swarm_depth": 2,
|
|
48
|
+
"isolation": "worktree",
|
|
49
|
+
"no_converge": False,
|
|
50
|
+
},
|
|
51
|
+
"yolo": { # Closest to the original emergent behavior
|
|
52
|
+
"swarm_budget": 999,
|
|
53
|
+
"swarm_depth": 2,
|
|
54
|
+
"isolation": "none",
|
|
55
|
+
"no_converge": True,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TokenEstimator:
|
|
61
|
+
"""Estimates token usage for Boris orchestration.
|
|
62
|
+
|
|
63
|
+
Claude Code runs on a token-based subscription model, not pay-per-API-call.
|
|
64
|
+
This estimator helps users understand token consumption before committing to
|
|
65
|
+
a task, and allows setting token budgets to limit usage.
|
|
66
|
+
|
|
67
|
+
Token estimates are based on realistic DaveLoop iteration profiles:
|
|
68
|
+
- Base iteration: ~4000 input tokens (prompt) + ~2000 output tokens (response)
|
|
69
|
+
- Context growth: each iteration adds ~1500 tokens of accumulated context
|
|
70
|
+
- Sub-agent spawn: ~3000 tokens each
|
|
71
|
+
- Convergence run: ~5000 tokens (reads multiple files + analysis)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
BASE_INPUT_TOKENS = 4000 # Base input tokens per DaveLoop iteration
|
|
75
|
+
BASE_OUTPUT_TOKENS = 2000 # Base output tokens per DaveLoop iteration
|
|
76
|
+
CONTEXT_GROWTH_PER_ITER = 1500 # Additional context tokens accumulated per iteration
|
|
77
|
+
TOKENS_PER_SUB_AGENT = 3000 # Tokens per sub-agent spawn
|
|
78
|
+
TOKENS_PER_CONVERGENCE = 5000 # Tokens per convergence run
|
|
79
|
+
|
|
80
|
+
def __init__(self, max_tokens: int = None):
|
|
81
|
+
self.max_tokens = max_tokens
|
|
82
|
+
self.estimated_tokens = 0
|
|
83
|
+
|
|
84
|
+
def _tokens_per_iteration(self, iteration_num: int) -> int:
|
|
85
|
+
"""Estimate tokens for a single DaveLoop iteration, accounting for context growth."""
|
|
86
|
+
input_tokens = self.BASE_INPUT_TOKENS + (iteration_num * self.CONTEXT_GROWTH_PER_ITER)
|
|
87
|
+
return input_tokens + self.BASE_OUTPUT_TOKENS
|
|
88
|
+
|
|
89
|
+
def record_tokens(self, amount: int):
|
|
90
|
+
"""Record estimated token usage."""
|
|
91
|
+
self.estimated_tokens += amount
|
|
92
|
+
|
|
93
|
+
def estimate_batch(self, num_workers: int, avg_iterations: int,
|
|
94
|
+
avg_sub_agents: int) -> int:
|
|
95
|
+
"""Estimate tokens for a batch of parallel workers."""
|
|
96
|
+
worker_tokens = 0
|
|
97
|
+
for w in range(num_workers):
|
|
98
|
+
for i in range(avg_iterations):
|
|
99
|
+
worker_tokens += self._tokens_per_iteration(i)
|
|
100
|
+
sub_agent_tokens = num_workers * avg_sub_agents * self.TOKENS_PER_SUB_AGENT
|
|
101
|
+
convergence_tokens = self.TOKENS_PER_CONVERGENCE
|
|
102
|
+
return worker_tokens + sub_agent_tokens + convergence_tokens
|
|
103
|
+
|
|
104
|
+
def estimate_task(self, plan) -> dict:
|
|
105
|
+
"""Estimate total tokens for a Boris plan.
|
|
106
|
+
|
|
107
|
+
Returns a dict with breakdown: per-milestone estimates, sub-agent tokens,
|
|
108
|
+
convergence tokens, and total.
|
|
109
|
+
"""
|
|
110
|
+
milestones = plan.milestones
|
|
111
|
+
num_milestones = len(milestones)
|
|
112
|
+
avg_iterations = 8 # Typical DaveLoop iterations per milestone
|
|
113
|
+
|
|
114
|
+
milestone_tokens = 0
|
|
115
|
+
for i in range(avg_iterations):
|
|
116
|
+
milestone_tokens += self._tokens_per_iteration(i)
|
|
117
|
+
total_milestone_tokens = milestone_tokens * num_milestones
|
|
118
|
+
|
|
119
|
+
# Estimate convergence runs (one per parallel batch, assume ~ceil(milestones/3) batches)
|
|
120
|
+
estimated_batches = max(1, (num_milestones + 2) // 3)
|
|
121
|
+
convergence_tokens = estimated_batches * self.TOKENS_PER_CONVERGENCE
|
|
122
|
+
|
|
123
|
+
total = total_milestone_tokens + convergence_tokens
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"num_milestones": num_milestones,
|
|
127
|
+
"avg_iterations_per_milestone": avg_iterations,
|
|
128
|
+
"tokens_per_milestone": milestone_tokens,
|
|
129
|
+
"total_milestone_tokens": total_milestone_tokens,
|
|
130
|
+
"convergence_tokens": convergence_tokens,
|
|
131
|
+
"total_tokens": total,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def check_budget(self, estimated_additional: int) -> bool:
|
|
135
|
+
"""Check if estimated additional tokens fit within budget."""
|
|
136
|
+
if self.max_tokens is None:
|
|
137
|
+
return True
|
|
138
|
+
return (self.estimated_tokens + estimated_additional) <= self.max_tokens
|
|
139
|
+
|
|
140
|
+
def summary(self) -> dict:
|
|
141
|
+
"""Return token tracking summary."""
|
|
142
|
+
return {
|
|
143
|
+
"estimated_tokens": self.estimated_tokens,
|
|
144
|
+
"max_tokens": self.max_tokens,
|
|
145
|
+
"remaining": (self.max_tokens - self.estimated_tokens) if self.max_tokens else None,
|
|
146
|
+
}
|
|
147
|
+
|
|
31
148
|
|
|
32
149
|
def setup_logging() -> logging.Logger:
|
|
33
150
|
"""Set up logging to both console and file."""
|
|
@@ -82,6 +199,61 @@ def print_plan_summary(plan: state_module.Plan):
|
|
|
82
199
|
print(flush=True)
|
|
83
200
|
|
|
84
201
|
|
|
202
|
+
def prompt_skip_milestones(plan: state_module.Plan, logger: logging.Logger) -> set:
|
|
203
|
+
"""Prompt the user to optionally skip milestones before execution.
|
|
204
|
+
|
|
205
|
+
Returns a set of milestone IDs that were marked as skipped.
|
|
206
|
+
"""
|
|
207
|
+
valid_ids = {m.id for m in plan.milestones}
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
user_input = input("Enter milestone IDs to skip (comma-separated), or press Enter to run all: ").strip()
|
|
211
|
+
except EOFError:
|
|
212
|
+
# Non-interactive environment, skip nothing
|
|
213
|
+
return set()
|
|
214
|
+
|
|
215
|
+
if not user_input:
|
|
216
|
+
return set()
|
|
217
|
+
|
|
218
|
+
# Parse comma-separated milestone IDs
|
|
219
|
+
requested_ids = {s.strip().upper() for s in user_input.split(",") if s.strip()}
|
|
220
|
+
skip_ids = requested_ids & valid_ids
|
|
221
|
+
invalid_ids = requested_ids - valid_ids
|
|
222
|
+
|
|
223
|
+
if invalid_ids:
|
|
224
|
+
print(f"[Boris] WARNING: Unknown milestone IDs ignored: {', '.join(sorted(invalid_ids))}", flush=True)
|
|
225
|
+
logger.warning("Unknown milestone IDs in skip request: %s", invalid_ids)
|
|
226
|
+
|
|
227
|
+
if not skip_ids:
|
|
228
|
+
return set()
|
|
229
|
+
|
|
230
|
+
# Check for dependency warnings: if a skipped milestone is depended on by a non-skipped one
|
|
231
|
+
skipped_set = skip_ids
|
|
232
|
+
for m in plan.milestones:
|
|
233
|
+
if m.id in skipped_set:
|
|
234
|
+
continue
|
|
235
|
+
depends_on_skipped = [dep for dep in m.depends_on if dep in skipped_set]
|
|
236
|
+
if depends_on_skipped:
|
|
237
|
+
print(
|
|
238
|
+
f"[Boris] WARNING: {m.id} ({m.title}) depends on skipped milestone(s): "
|
|
239
|
+
f"{', '.join(depends_on_skipped)}",
|
|
240
|
+
flush=True,
|
|
241
|
+
)
|
|
242
|
+
logger.warning(
|
|
243
|
+
"Milestone %s depends on skipped milestone(s): %s",
|
|
244
|
+
m.id, depends_on_skipped,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Mark milestones as skipped
|
|
248
|
+
for m in plan.milestones:
|
|
249
|
+
if m.id in skip_ids:
|
|
250
|
+
m.status = "skipped"
|
|
251
|
+
print(f"[Boris] Skipping milestone {m.id}: {m.title}", flush=True)
|
|
252
|
+
logger.info("User skipped milestone %s: %s", m.id, m.title)
|
|
253
|
+
|
|
254
|
+
return skip_ids
|
|
255
|
+
|
|
256
|
+
|
|
85
257
|
def generate_summary(plan: state_module.Plan, project_dir: str, start_time: datetime) -> str:
|
|
86
258
|
"""Generate a summary markdown file and return its path."""
|
|
87
259
|
os.makedirs(PLANS_DIR, exist_ok=True)
|
|
@@ -202,15 +374,23 @@ def parse_args() -> argparse.Namespace:
|
|
|
202
374
|
description="Boris - Autonomous Project Orchestrator. Breaks tasks into milestones and executes via Claude CLI.",
|
|
203
375
|
)
|
|
204
376
|
parser.add_argument("task", nargs="?", help="The task description string")
|
|
377
|
+
parser.add_argument(
|
|
378
|
+
"-f", "--file", help="Read task description from a file"
|
|
379
|
+
)
|
|
205
380
|
parser.add_argument(
|
|
206
381
|
"-d", "--dir", default=os.getcwd(), help="Working directory for the project (default: current dir)"
|
|
207
382
|
)
|
|
208
383
|
parser.add_argument(
|
|
209
384
|
"-r", "--resume", action="store_true", help="Resume from last saved state"
|
|
210
385
|
)
|
|
211
|
-
parser.
|
|
386
|
+
plan_group = parser.add_mutually_exclusive_group()
|
|
387
|
+
plan_group.add_argument(
|
|
212
388
|
"--plan-only", action="store_true", help="Generate plan but don't execute"
|
|
213
389
|
)
|
|
390
|
+
plan_group.add_argument(
|
|
391
|
+
"--exec-only", metavar="PLAN_FILE",
|
|
392
|
+
help="Skip planning and execute an existing plan file directly"
|
|
393
|
+
)
|
|
214
394
|
parser.add_argument("--remote", help="Git remote URL to set up")
|
|
215
395
|
parser.add_argument(
|
|
216
396
|
"--no-git", action="store_true", help="Disable git management"
|
|
@@ -224,6 +404,53 @@ def parse_args() -> argparse.Namespace:
|
|
|
224
404
|
parser.add_argument(
|
|
225
405
|
"--skip-ui", action="store_true", help="Skip UI testing phase after structural milestones"
|
|
226
406
|
)
|
|
407
|
+
parser.add_argument(
|
|
408
|
+
"--incremental", action="store_true",
|
|
409
|
+
help="Execute one milestone at a time, then pause. Use with -r to continue to the next milestone."
|
|
410
|
+
)
|
|
411
|
+
parser.add_argument(
|
|
412
|
+
"--turbo", action="store_true",
|
|
413
|
+
help="Enable parallel DaveLoop execution for independent milestones"
|
|
414
|
+
)
|
|
415
|
+
parser.add_argument(
|
|
416
|
+
"--stop-at", metavar="MILESTONE_ID", dest="stop_at",
|
|
417
|
+
help="Stop execution after completing this milestone ID (e.g. M4). Implies --incremental."
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Swarm mode flags
|
|
421
|
+
parser.add_argument(
|
|
422
|
+
"--swarm", action="store_true",
|
|
423
|
+
help="Enable swarm mode: DaveLoops can spawn sub-agents via Task tool"
|
|
424
|
+
)
|
|
425
|
+
parser.add_argument(
|
|
426
|
+
"--swarm-budget", type=int, default=5,
|
|
427
|
+
help="Max sub-agents per DaveLoop worker in swarm mode (default: 5)"
|
|
428
|
+
)
|
|
429
|
+
parser.add_argument(
|
|
430
|
+
"--swarm-depth", type=int, default=1, choices=[1, 2],
|
|
431
|
+
help="Max sub-agent depth in swarm mode (default: 1, no recursive spawning)"
|
|
432
|
+
)
|
|
433
|
+
parser.add_argument(
|
|
434
|
+
"--preset", choices=["conservative", "balanced", "aggressive", "yolo"],
|
|
435
|
+
help="Apply a swarm configuration preset (implies --swarm)"
|
|
436
|
+
)
|
|
437
|
+
parser.add_argument(
|
|
438
|
+
"--isolation", choices=["none", "worktree"], default="none",
|
|
439
|
+
help="Isolation strategy for parallel workers (default: none)"
|
|
440
|
+
)
|
|
441
|
+
parser.add_argument(
|
|
442
|
+
"--no-converge", action="store_true", dest="no_converge",
|
|
443
|
+
help="Skip convergence phase after each swarm batch"
|
|
444
|
+
)
|
|
445
|
+
parser.add_argument(
|
|
446
|
+
"--max-tokens", type=int, default=None, dest="max_tokens",
|
|
447
|
+
help="Maximum estimated token budget for swarm/turbo mode execution (e.g. 500000)"
|
|
448
|
+
)
|
|
449
|
+
parser.add_argument(
|
|
450
|
+
"--estimate", metavar="TASK", nargs="?", const="__FROM_TASK__", dest="estimate",
|
|
451
|
+
help="Generate plan and print token estimate WITHOUT executing. "
|
|
452
|
+
"Use alone (boris --estimate 'task') or with positional task."
|
|
453
|
+
)
|
|
227
454
|
|
|
228
455
|
return parser.parse_args()
|
|
229
456
|
|
|
@@ -400,11 +627,273 @@ def _run_ui_milestones(st, plan, project_dir, args, logger):
|
|
|
400
627
|
logger.info("UI testing complete: %d/%d", ui_completed, ui_total)
|
|
401
628
|
|
|
402
629
|
|
|
630
|
+
def get_ready_milestones(plan: state_module.Plan) -> list[state_module.Milestone]:
|
|
631
|
+
"""Return all pending milestones whose dependencies are all completed or skipped."""
|
|
632
|
+
done_statuses = {"completed", "skipped"}
|
|
633
|
+
done_ids = {m.id for m in plan.milestones if m.status in done_statuses}
|
|
634
|
+
ready = []
|
|
635
|
+
for m in plan.milestones:
|
|
636
|
+
if m.status != "pending":
|
|
637
|
+
continue
|
|
638
|
+
if all(dep in done_ids for dep in m.depends_on):
|
|
639
|
+
ready.append(m)
|
|
640
|
+
return ready
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _process_milestone_verdict(verdict_result, result, milestone, plan, st, project_dir, args, logger, prompt):
|
|
644
|
+
"""Process a single milestone's verdict (corrections, retries). Used by both sequential and turbo modes."""
|
|
645
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
646
|
+
milestone.status = "completed"
|
|
647
|
+
milestone.completed_at = datetime.now().isoformat()
|
|
648
|
+
state_module.save(st)
|
|
649
|
+
print(f"[Boris] Milestone {milestone.id} COMPLETE", flush=True)
|
|
650
|
+
logger.info("Milestone %s completed", milestone.id)
|
|
651
|
+
|
|
652
|
+
elif verdict_result.verdict == Verdict.OFF_PLAN:
|
|
653
|
+
correction_count = 0
|
|
654
|
+
resolved = False
|
|
655
|
+
|
|
656
|
+
while correction_count < MAX_CORRECTIONS:
|
|
657
|
+
correction_count += 1
|
|
658
|
+
print(
|
|
659
|
+
f" [Boris] Off-plan detected. Correction attempt {correction_count}/{MAX_CORRECTIONS}...",
|
|
660
|
+
flush=True,
|
|
661
|
+
)
|
|
662
|
+
logger.info(
|
|
663
|
+
"Off-plan correction attempt %d for milestone %s",
|
|
664
|
+
correction_count,
|
|
665
|
+
milestone.id,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
correction_prompt = prompts.craft_correction(
|
|
669
|
+
result.output, milestone, plan, verdict_result.reason
|
|
670
|
+
)
|
|
671
|
+
result = engine.run(correction_prompt, project_dir, args.max_iter, milestone=milestone)
|
|
672
|
+
verdict_result = engine.check(result, milestone)
|
|
673
|
+
|
|
674
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
675
|
+
milestone.status = "completed"
|
|
676
|
+
milestone.completed_at = datetime.now().isoformat()
|
|
677
|
+
state_module.save(st)
|
|
678
|
+
print(f"[Boris] Milestone {milestone.id} COMPLETE (after correction)", flush=True)
|
|
679
|
+
logger.info("Milestone %s completed after correction", milestone.id)
|
|
680
|
+
resolved = True
|
|
681
|
+
break
|
|
682
|
+
|
|
683
|
+
if not resolved:
|
|
684
|
+
print(f" [Boris] Milestone {milestone.id} could not be corrected. Skipping.", flush=True)
|
|
685
|
+
logger.warning("Milestone %s skipped after failed corrections", milestone.id)
|
|
686
|
+
milestone.status = "skipped"
|
|
687
|
+
state_module.save(st)
|
|
688
|
+
|
|
689
|
+
elif verdict_result.verdict == Verdict.FAILED:
|
|
690
|
+
retry_count = 0
|
|
691
|
+
resolved = False
|
|
692
|
+
|
|
693
|
+
while retry_count < MAX_RETRIES:
|
|
694
|
+
retry_count += 1
|
|
695
|
+
milestone.retry_count = retry_count
|
|
696
|
+
print(
|
|
697
|
+
f" [Boris] Failed. Retry {retry_count}/{MAX_RETRIES}...",
|
|
698
|
+
flush=True,
|
|
699
|
+
)
|
|
700
|
+
logger.info(
|
|
701
|
+
"Retry %d for milestone %s", retry_count, milestone.id
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
|
|
705
|
+
verdict_result = engine.check(result, milestone)
|
|
706
|
+
|
|
707
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
708
|
+
milestone.status = "completed"
|
|
709
|
+
milestone.completed_at = datetime.now().isoformat()
|
|
710
|
+
state_module.save(st)
|
|
711
|
+
print(f"[Boris] Milestone {milestone.id} COMPLETE (after retry)", flush=True)
|
|
712
|
+
logger.info("Milestone %s completed after retry", milestone.id)
|
|
713
|
+
resolved = True
|
|
714
|
+
break
|
|
715
|
+
|
|
716
|
+
if not resolved:
|
|
717
|
+
print(
|
|
718
|
+
f" [Boris] Milestone {milestone.id} failed after {MAX_RETRIES} retries. Skipping.",
|
|
719
|
+
flush=True,
|
|
720
|
+
)
|
|
721
|
+
logger.warning("Milestone %s skipped after failed retries", milestone.id)
|
|
722
|
+
milestone.status = "skipped"
|
|
723
|
+
state_module.save(st)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def validate_turbo_batch(ready: list, plan: state_module.Plan, logger: logging.Logger) -> list:
|
|
727
|
+
"""Filter out milestones from a turbo batch whose dependencies were skipped.
|
|
728
|
+
|
|
729
|
+
These milestones cannot succeed without their dependency's output.
|
|
730
|
+
Also enforces foundation-first: first batch only runs milestones with no dependencies.
|
|
731
|
+
"""
|
|
732
|
+
skipped_ids = {m.id for m in plan.milestones if m.status == "skipped"}
|
|
733
|
+
valid = []
|
|
734
|
+
for m in ready:
|
|
735
|
+
skipped_deps = [d for d in m.depends_on if d in skipped_ids]
|
|
736
|
+
if skipped_deps:
|
|
737
|
+
logger.warning(
|
|
738
|
+
"Milestone %s depends on skipped milestone(s) %s - deferring from turbo batch",
|
|
739
|
+
m.id, skipped_deps
|
|
740
|
+
)
|
|
741
|
+
print(f"[Boris] WARNING: {m.id} skipped from turbo batch - depends on skipped: {skipped_deps}", flush=True)
|
|
742
|
+
else:
|
|
743
|
+
valid.append(m)
|
|
744
|
+
return valid
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _run_convergence(st, plan, project_dir, batch_milestones, logger):
|
|
748
|
+
"""Run a convergence agent after a parallel batch to resolve conflicts."""
|
|
749
|
+
completed_in_batch = [m for m in batch_milestones if m.status == "completed"]
|
|
750
|
+
if len(completed_in_batch) < 2:
|
|
751
|
+
return # No conflicts possible with 0-1 completed milestones
|
|
752
|
+
|
|
753
|
+
# Collect all files touched by this batch and check for overlaps
|
|
754
|
+
file_owners = {}
|
|
755
|
+
conflicts = []
|
|
756
|
+
for m in completed_in_batch:
|
|
757
|
+
for f in (m.files_to_create or []) + (m.files_to_modify or []):
|
|
758
|
+
if f in file_owners:
|
|
759
|
+
conflicts.append((f, file_owners[f], m.id))
|
|
760
|
+
file_owners[f] = m.id
|
|
761
|
+
|
|
762
|
+
if not conflicts:
|
|
763
|
+
logger.info("Convergence: No file conflicts detected in batch")
|
|
764
|
+
print("[Boris] Convergence: No conflicts detected. Skipping.", flush=True)
|
|
765
|
+
return
|
|
766
|
+
|
|
767
|
+
print(f"[Boris] Convergence: {len(conflicts)} potential conflict(s) detected", flush=True)
|
|
768
|
+
for filepath, owner1, owner2 in conflicts:
|
|
769
|
+
print(f" [Boris] {filepath}: written by {owner1} and {owner2}", flush=True)
|
|
770
|
+
|
|
771
|
+
# Build convergence prompt
|
|
772
|
+
conflict_text = "\n".join(
|
|
773
|
+
f"- {f}: modified by {o1} and {o2}" for f, o1, o2 in conflicts
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
convergence_prompt = f"""# Convergence Task
|
|
777
|
+
|
|
778
|
+
Multiple parallel agents modified overlapping files. Resolve any conflicts.
|
|
779
|
+
|
|
780
|
+
## Conflicts Detected
|
|
781
|
+
{conflict_text}
|
|
782
|
+
|
|
783
|
+
## Milestones Completed in This Batch
|
|
784
|
+
{chr(10).join(f'- {m.id}: {m.title}' for m in completed_in_batch)}
|
|
785
|
+
|
|
786
|
+
## Instructions
|
|
787
|
+
1. Read each conflicted file
|
|
788
|
+
2. Check for type mismatches, duplicate definitions, incompatible interfaces
|
|
789
|
+
3. Reconcile into a consistent state
|
|
790
|
+
4. Run `tsc --noEmit` (TypeScript) or equivalent type checker
|
|
791
|
+
5. Fix any remaining build errors
|
|
792
|
+
6. Do NOT add new features - only resolve conflicts between existing code
|
|
793
|
+
|
|
794
|
+
When all conflicts are resolved and the build is clean, output [DAVELOOP:RESOLVED].
|
|
795
|
+
"""
|
|
796
|
+
|
|
797
|
+
# Run convergence via DaveLoop (without Task tool - no swarm for convergence)
|
|
798
|
+
result = engine.run(convergence_prompt, project_dir, max_iterations=5)
|
|
799
|
+
if result.resolved:
|
|
800
|
+
print("[Boris] Convergence: All conflicts resolved", flush=True)
|
|
801
|
+
logger.info("Convergence phase completed successfully")
|
|
802
|
+
else:
|
|
803
|
+
print("[Boris] WARNING: Convergence could not resolve all conflicts", flush=True)
|
|
804
|
+
logger.warning("Convergence phase did not fully resolve")
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def _print_swarm_dashboard(project_dir: str, batch_num: int, batch_milestones: list):
|
|
808
|
+
"""Print a swarm status dashboard showing all active workers in the current batch (B7)."""
|
|
809
|
+
statuses = engine.read_worker_statuses(project_dir)
|
|
810
|
+
if not statuses:
|
|
811
|
+
return
|
|
812
|
+
|
|
813
|
+
active = sum(1 for s in statuses.values() if s.get("state") in ("starting", "working"))
|
|
814
|
+
total_actions = sum(s.get("actions", 0) for s in statuses.values())
|
|
815
|
+
|
|
816
|
+
print(flush=True)
|
|
817
|
+
print("=== BORIS SWARM STATUS ===", flush=True)
|
|
818
|
+
print(f"Batch {batch_num} | Workers: {active} active / {len(statuses)} total | Actions: {total_actions}", flush=True)
|
|
819
|
+
print(flush=True)
|
|
820
|
+
|
|
821
|
+
for m in batch_milestones:
|
|
822
|
+
status = statuses.get(m.id, {})
|
|
823
|
+
state = status.get("state", "unknown")
|
|
824
|
+
actions = status.get("actions", 0)
|
|
825
|
+
reasoning = status.get("reasoning_blocks", 0)
|
|
826
|
+
interrupts = status.get("interrupts", 0)
|
|
827
|
+
last = status.get("last_action", "")
|
|
828
|
+
|
|
829
|
+
# Progress indicator based on reasoning blocks (rough proxy)
|
|
830
|
+
bar_len = min(reasoning, 10)
|
|
831
|
+
bar = "#" * bar_len + "-" * (10 - bar_len)
|
|
832
|
+
|
|
833
|
+
state_str = state.upper()
|
|
834
|
+
interrupt_str = f" | {interrupts} interrupts" if interrupts > 0 else ""
|
|
835
|
+
print(f"[{m.id}] {m.title[:30]:<30} [{bar}] {state_str} | {actions} actions{interrupt_str}", flush=True)
|
|
836
|
+
if last:
|
|
837
|
+
print(f" Last: {last}", flush=True)
|
|
838
|
+
|
|
839
|
+
# Show file locks if file_lock.py is available
|
|
840
|
+
try:
|
|
841
|
+
from file_lock import FileLockManager
|
|
842
|
+
flm = FileLockManager(project_dir)
|
|
843
|
+
locks = flm.get_locked_files()
|
|
844
|
+
if locks:
|
|
845
|
+
lock_strs = [f"{os.path.basename(f)} ({owner})" for f, owner in locks.items()]
|
|
846
|
+
print(f"\nFile locks: {', '.join(lock_strs)}", flush=True)
|
|
847
|
+
except ImportError:
|
|
848
|
+
pass
|
|
849
|
+
|
|
850
|
+
print("===========================", flush=True)
|
|
851
|
+
print(flush=True)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _apply_preset(args):
|
|
855
|
+
"""Apply a swarm preset to args if specified. Preset implies --swarm."""
|
|
856
|
+
if not args.preset:
|
|
857
|
+
return
|
|
858
|
+
args.swarm = True
|
|
859
|
+
preset = SWARM_PRESETS[args.preset]
|
|
860
|
+
# Only override if user didn't explicitly set these
|
|
861
|
+
if args.swarm_budget == 5: # default value
|
|
862
|
+
args.swarm_budget = preset["swarm_budget"]
|
|
863
|
+
if args.swarm_depth == 1: # default value
|
|
864
|
+
args.swarm_depth = preset["swarm_depth"]
|
|
865
|
+
if args.isolation == "none": # default value
|
|
866
|
+
args.isolation = preset.get("isolation", "none")
|
|
867
|
+
if not args.no_converge:
|
|
868
|
+
args.no_converge = preset["no_converge"]
|
|
869
|
+
|
|
870
|
+
|
|
403
871
|
def main():
|
|
404
872
|
"""Main Boris orchestration loop."""
|
|
405
873
|
args = parse_args()
|
|
874
|
+
|
|
875
|
+
# Apply swarm preset if specified (implies --swarm)
|
|
876
|
+
_apply_preset(args)
|
|
877
|
+
|
|
878
|
+
# --stop-at implies incremental mode
|
|
879
|
+
if args.stop_at:
|
|
880
|
+
args.incremental = True
|
|
881
|
+
|
|
882
|
+
# Handle --estimate: resolve the task description from either the flag or positional arg
|
|
883
|
+
if args.estimate is not None:
|
|
884
|
+
estimate_task = args.estimate if args.estimate != "__FROM_TASK__" else args.task
|
|
885
|
+
if not estimate_task:
|
|
886
|
+
print("[Boris] Error: --estimate requires a task description", flush=True)
|
|
887
|
+
sys.exit(1)
|
|
888
|
+
args.task = estimate_task
|
|
889
|
+
|
|
406
890
|
logger = setup_logging()
|
|
407
891
|
|
|
892
|
+
# Initialize token estimator for swarm/turbo mode
|
|
893
|
+
token_estimator = None
|
|
894
|
+
if getattr(args, 'swarm', False) or getattr(args, 'max_tokens', None) or getattr(args, 'turbo', False):
|
|
895
|
+
token_estimator = TokenEstimator(max_tokens=args.max_tokens)
|
|
896
|
+
|
|
408
897
|
# Create required dirs
|
|
409
898
|
os.makedirs(PLANS_DIR, exist_ok=True)
|
|
410
899
|
os.makedirs(LOGS_DIR, exist_ok=True)
|
|
@@ -427,21 +916,69 @@ def main():
|
|
|
427
916
|
else:
|
|
428
917
|
print(f"[Boris] Resuming from milestone {start_index + 1}...", flush=True)
|
|
429
918
|
logger.info("Resuming from milestone %d", start_index + 1)
|
|
919
|
+
elif args.exec_only:
|
|
920
|
+
# Exec-only mode - load an existing plan file and execute it
|
|
921
|
+
plan_file = os.path.abspath(args.exec_only)
|
|
922
|
+
print_banner()
|
|
923
|
+
print(f"[Boris] Exec-only mode: loading plan from {plan_file}", flush=True)
|
|
924
|
+
logger.info("Exec-only mode: loading plan from %s", plan_file)
|
|
925
|
+
|
|
926
|
+
try:
|
|
927
|
+
plan = prompts.load_plan_from_file(plan_file)
|
|
928
|
+
except (FileNotFoundError, RuntimeError) as e:
|
|
929
|
+
print(f"[Boris] Error loading plan file: {e}", flush=True)
|
|
930
|
+
logger.error("Failed to load plan file: %s", e)
|
|
931
|
+
sys.exit(1)
|
|
932
|
+
|
|
933
|
+
print(f"[Boris] Plan loaded with {len(plan.milestones)} milestones", flush=True)
|
|
934
|
+
logger.info("Plan loaded with %d milestones", len(plan.milestones))
|
|
935
|
+
|
|
936
|
+
print_plan_summary(plan)
|
|
937
|
+
|
|
938
|
+
# Prompt user to skip milestones before execution
|
|
939
|
+
prompt_skip_milestones(plan, logger)
|
|
940
|
+
|
|
941
|
+
# Create initial state from the loaded plan
|
|
942
|
+
st = state_module.State(
|
|
943
|
+
plan=plan,
|
|
944
|
+
current_milestone_index=0,
|
|
945
|
+
project_dir=project_dir,
|
|
946
|
+
git_remote=args.remote,
|
|
947
|
+
no_git=args.no_git,
|
|
948
|
+
turbo=args.turbo,
|
|
949
|
+
)
|
|
950
|
+
state_module.save(st)
|
|
951
|
+
start_index = 0
|
|
430
952
|
else:
|
|
431
|
-
# New task mode
|
|
432
|
-
|
|
433
|
-
|
|
953
|
+
# New task mode - resolve task from args or file
|
|
954
|
+
task = args.task
|
|
955
|
+
if args.file:
|
|
956
|
+
try:
|
|
957
|
+
with open(args.file, "r", encoding="utf-8") as f:
|
|
958
|
+
task = f.read().strip()
|
|
959
|
+
print(f"[Boris] Read task from file: {args.file} ({len(task)} chars)", flush=True)
|
|
960
|
+
except FileNotFoundError:
|
|
961
|
+
print(f"[Boris] Error: File not found: {args.file}", flush=True)
|
|
962
|
+
sys.exit(1)
|
|
963
|
+
except Exception as e:
|
|
964
|
+
print(f"[Boris] Error reading file: {e}", flush=True)
|
|
965
|
+
sys.exit(1)
|
|
966
|
+
|
|
967
|
+
if not task:
|
|
968
|
+
print("[Boris] Error: task is required (pass as argument or use -f FILE)", flush=True)
|
|
434
969
|
sys.exit(1)
|
|
435
970
|
|
|
971
|
+
args.task = task
|
|
972
|
+
|
|
436
973
|
print_banner()
|
|
437
|
-
logger.info("Starting Boris for task: %s", args.task)
|
|
974
|
+
logger.info("Starting Boris for task: %s", args.task[:200])
|
|
438
975
|
|
|
439
976
|
# Create plan (retry once on timeout)
|
|
440
977
|
print("[Boris] Creating plan...", flush=True)
|
|
441
978
|
plan = None
|
|
442
979
|
for attempt in range(2):
|
|
443
980
|
try:
|
|
444
|
-
plan = prompts.create_plan(args.task, project_dir)
|
|
981
|
+
plan = prompts.create_plan(args.task, project_dir, turbo=args.turbo)
|
|
445
982
|
break
|
|
446
983
|
except RuntimeError as e:
|
|
447
984
|
if attempt == 0 and "timed out" in str(e).lower():
|
|
@@ -459,10 +996,32 @@ def main():
|
|
|
459
996
|
|
|
460
997
|
print_plan_summary(plan)
|
|
461
998
|
|
|
999
|
+
# --estimate mode: print token estimate and exit without executing
|
|
1000
|
+
if args.estimate is not None:
|
|
1001
|
+
estimator = TokenEstimator()
|
|
1002
|
+
estimate = estimator.estimate_task(plan)
|
|
1003
|
+
print("=" * 60, flush=True)
|
|
1004
|
+
print(" BORIS - Token Estimate", flush=True)
|
|
1005
|
+
print("=" * 60, flush=True)
|
|
1006
|
+
print(f" Milestones: {estimate['num_milestones']}", flush=True)
|
|
1007
|
+
print(f" Avg iterations each: {estimate['avg_iterations_per_milestone']}", flush=True)
|
|
1008
|
+
print(f" Tokens/milestone: {estimate['tokens_per_milestone']:,}", flush=True)
|
|
1009
|
+
print(f" Milestone tokens: {estimate['total_milestone_tokens']:,}", flush=True)
|
|
1010
|
+
print(f" Convergence tokens: {estimate['convergence_tokens']:,}", flush=True)
|
|
1011
|
+
print(f" ----------------------------------------", flush=True)
|
|
1012
|
+
print(f" TOTAL ESTIMATED: {estimate['total_tokens']:,} tokens", flush=True)
|
|
1013
|
+
print("=" * 60, flush=True)
|
|
1014
|
+
print(flush=True)
|
|
1015
|
+
print("[Boris] Estimate-only mode. No execution performed.", flush=True)
|
|
1016
|
+
return
|
|
1017
|
+
|
|
462
1018
|
if args.plan_only:
|
|
463
1019
|
print("[Boris] Plan-only mode. Exiting.", flush=True)
|
|
464
1020
|
return
|
|
465
1021
|
|
|
1022
|
+
# Prompt user to skip milestones before execution
|
|
1023
|
+
prompt_skip_milestones(plan, logger)
|
|
1024
|
+
|
|
466
1025
|
# Create initial state
|
|
467
1026
|
st = state_module.State(
|
|
468
1027
|
plan=plan,
|
|
@@ -470,6 +1029,7 @@ def main():
|
|
|
470
1029
|
project_dir=project_dir,
|
|
471
1030
|
git_remote=args.remote,
|
|
472
1031
|
no_git=args.no_git,
|
|
1032
|
+
turbo=args.turbo,
|
|
473
1033
|
)
|
|
474
1034
|
state_module.save(st)
|
|
475
1035
|
start_index = 0
|
|
@@ -491,156 +1051,337 @@ def main():
|
|
|
491
1051
|
print(f"[Boris] Setting up remote: {args.remote}", flush=True)
|
|
492
1052
|
git_manager.setup_remote(project_dir, args.remote)
|
|
493
1053
|
|
|
1054
|
+
# Determine turbo mode (from state on resume, from args on new run)
|
|
1055
|
+
turbo = getattr(st, "turbo", False) or getattr(args, "turbo", False)
|
|
1056
|
+
|
|
494
1057
|
# Phase 1: Structural milestones (skip if resuming into UI phase)
|
|
495
1058
|
try:
|
|
496
1059
|
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
|
-
|
|
1060
|
+
if turbo:
|
|
1061
|
+
# === TURBO MODE: Parallel DaveLoop execution ===
|
|
508
1062
|
print(flush=True)
|
|
509
|
-
print(
|
|
510
|
-
logger.info("
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1063
|
+
print("[Boris] TURBO MODE: Parallel DaveLoop execution enabled", flush=True)
|
|
1064
|
+
logger.info("TURBO MODE enabled")
|
|
1065
|
+
|
|
1066
|
+
batch_num = 0
|
|
1067
|
+
while True:
|
|
1068
|
+
ready = get_ready_milestones(plan)
|
|
1069
|
+
if not ready:
|
|
1070
|
+
# Check if there are still pending milestones (deadlock detection)
|
|
1071
|
+
pending = [m for m in plan.milestones if m.status == "pending"]
|
|
1072
|
+
if pending:
|
|
1073
|
+
pending_ids = [m.id for m in pending]
|
|
1074
|
+
print(f"[Boris] TURBO: No milestones ready but {len(pending)} still pending: {pending_ids}", flush=True)
|
|
1075
|
+
print("[Boris] TURBO: Possible dependency deadlock. Skipping remaining.", flush=True)
|
|
1076
|
+
logger.warning("Turbo deadlock: pending=%s", pending_ids)
|
|
1077
|
+
for m in pending:
|
|
1078
|
+
m.status = "skipped"
|
|
1079
|
+
state_module.save(st)
|
|
1080
|
+
break
|
|
1081
|
+
|
|
1082
|
+
# Dependency-aware batch validation: filter milestones with skipped deps
|
|
1083
|
+
ready = validate_turbo_batch(ready, plan, logger)
|
|
1084
|
+
if not ready:
|
|
1085
|
+
logger.warning("All ready milestones filtered out by turbo batch validation")
|
|
1086
|
+
break
|
|
1087
|
+
|
|
1088
|
+
# Foundation-first: first batch only runs milestones with no dependencies
|
|
1089
|
+
if batch_num == 0:
|
|
1090
|
+
foundation = [m for m in ready if not m.depends_on]
|
|
1091
|
+
if foundation:
|
|
1092
|
+
ready = foundation[:1] # Only one foundation milestone at a time
|
|
1093
|
+
|
|
1094
|
+
# Token budget check (swarm/turbo mode)
|
|
1095
|
+
if token_estimator:
|
|
1096
|
+
estimated = token_estimator.estimate_batch(len(ready), args.max_iter, args.swarm_budget if getattr(args, 'swarm', False) else 0)
|
|
1097
|
+
if not token_estimator.check_budget(estimated):
|
|
1098
|
+
print(f"[Boris] Token budget exceeded. Estimated: {token_estimator.estimated_tokens + estimated:,}, Budget: {token_estimator.max_tokens:,} tokens", flush=True)
|
|
1099
|
+
logger.warning("Token budget exceeded, stopping execution")
|
|
1100
|
+
break
|
|
1101
|
+
token_estimator.record_tokens(estimated)
|
|
516
1102
|
|
|
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
|
-
)
|
|
1103
|
+
batch_num += 1
|
|
1104
|
+
batch_ids = [m.id for m in ready]
|
|
1105
|
+
print(flush=True)
|
|
1106
|
+
print(f"[Boris] TURBO: Launching batch {batch_num} of {len(ready)} parallel DaveLoops: {batch_ids}", flush=True)
|
|
1107
|
+
logger.info("TURBO batch %d: %s", batch_num, batch_ids)
|
|
532
1108
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1109
|
+
# Mark all in batch as in_progress
|
|
1110
|
+
for m in ready:
|
|
1111
|
+
m.status = "in_progress"
|
|
536
1112
|
state_module.save(st)
|
|
537
1113
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1114
|
+
# Craft prompts for all milestones in batch
|
|
1115
|
+
tasks = []
|
|
1116
|
+
prompt_map = {} # milestone.id -> prompt, for retries
|
|
1117
|
+
for m in ready:
|
|
1118
|
+
print(f"[Boris] TURBO: Crafting prompt for {m.id}...", flush=True)
|
|
1119
|
+
# Compute sibling milestones (all others in this batch)
|
|
1120
|
+
siblings = [sib for sib in ready if sib.id != m.id]
|
|
1121
|
+
prompt = prompts.craft_prompt(m, plan, project_dir,
|
|
1122
|
+
turbo=True, sibling_milestones=siblings)
|
|
1123
|
+
print(f"[Boris] TURBO: Prompt ready for {m.id} ({len(prompt)} chars)", flush=True)
|
|
1124
|
+
tasks.append((prompt, m))
|
|
1125
|
+
prompt_map[m.id] = prompt
|
|
1126
|
+
|
|
1127
|
+
# Run all DaveLoops in parallel
|
|
1128
|
+
isolation = getattr(args, 'isolation', 'none') if getattr(args, 'swarm', False) else 'none'
|
|
1129
|
+
parallel_results = engine.run_parallel(tasks, project_dir, args.max_iter, isolation=isolation)
|
|
1130
|
+
|
|
1131
|
+
# Print swarm dashboard after parallel run completes (B7)
|
|
1132
|
+
_print_swarm_dashboard(project_dir, batch_num, ready)
|
|
1133
|
+
engine.clear_worker_statuses(project_dir)
|
|
1134
|
+
|
|
1135
|
+
# Process verdicts sequentially (corrections/retries run sequentially)
|
|
1136
|
+
batch_summary = {}
|
|
1137
|
+
for milestone, result in parallel_results:
|
|
1138
|
+
print(f"[Boris] TURBO: Checking verdict for {milestone.id}...", flush=True)
|
|
1139
|
+
verdict_result = engine.check(result, milestone)
|
|
560
1140
|
logger.info(
|
|
561
|
-
"
|
|
562
|
-
correction_count,
|
|
1141
|
+
"TURBO milestone %s verdict: %s - %s",
|
|
563
1142
|
milestone.id,
|
|
1143
|
+
verdict_result.verdict.value,
|
|
1144
|
+
verdict_result.reason,
|
|
564
1145
|
)
|
|
565
1146
|
|
|
566
|
-
|
|
567
|
-
result
|
|
1147
|
+
_process_milestone_verdict(
|
|
1148
|
+
verdict_result, result, milestone, plan, st,
|
|
1149
|
+
project_dir, args, logger, prompt_map[milestone.id]
|
|
568
1150
|
)
|
|
569
|
-
|
|
570
|
-
verdict_result = engine.check(result, milestone)
|
|
1151
|
+
batch_summary[milestone.id] = milestone.status.upper()
|
|
571
1152
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1153
|
+
# Convergence phase: reconcile type conflicts from parallel workers
|
|
1154
|
+
if not getattr(args, 'no_converge', False):
|
|
1155
|
+
completed_in_batch = [m for m, r in parallel_results if m.status == "completed"]
|
|
1156
|
+
if len(completed_in_batch) > 1:
|
|
1157
|
+
print(f"[Boris] TURBO: Running convergence phase for batch {batch_num}...", flush=True)
|
|
1158
|
+
_run_convergence(st, plan, project_dir, completed_in_batch, logger)
|
|
576
1159
|
|
|
577
|
-
|
|
578
|
-
|
|
1160
|
+
# Git commits sequentially after entire batch
|
|
1161
|
+
if not st.no_git:
|
|
1162
|
+
for milestone, result in parallel_results:
|
|
1163
|
+
if milestone.status == "completed":
|
|
1164
|
+
print(f"[Boris] TURBO: Committing {milestone.id} to git...", flush=True)
|
|
579
1165
|
if git_manager.commit_milestone(project_dir, milestone):
|
|
580
|
-
logger.info("Git commit succeeded for milestone %s
|
|
1166
|
+
logger.info("Git commit succeeded for milestone %s", milestone.id)
|
|
581
1167
|
else:
|
|
582
|
-
logger.warning("Git commit FAILED for milestone %s
|
|
1168
|
+
logger.warning("Git commit FAILED for milestone %s", milestone.id)
|
|
583
1169
|
print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
|
|
584
1170
|
|
|
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)
|
|
1171
|
+
# Save state after whole batch
|
|
1172
|
+
state_module.save(st)
|
|
595
1173
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1174
|
+
# Print batch summary
|
|
1175
|
+
summary_parts = [f"{mid}={status}" for mid, status in batch_summary.items()]
|
|
1176
|
+
print(f"[Boris] TURBO: Batch {batch_num} complete. Results: {', '.join(summary_parts)}", flush=True)
|
|
1177
|
+
logger.info("TURBO batch %d complete: %s", batch_num, batch_summary)
|
|
1178
|
+
|
|
1179
|
+
# Incremental / --stop-at: pause after batch in turbo
|
|
1180
|
+
should_pause = False
|
|
1181
|
+
if args.stop_at:
|
|
1182
|
+
# Pause only if the --stop-at milestone was in this batch
|
|
1183
|
+
batch_completed_ids = {mid for mid, status in batch_summary.items()
|
|
1184
|
+
if status in ("COMPLETED", "SKIPPED")}
|
|
1185
|
+
if args.stop_at in batch_completed_ids:
|
|
1186
|
+
should_pause = True
|
|
1187
|
+
elif args.incremental:
|
|
1188
|
+
should_pause = True
|
|
1189
|
+
|
|
1190
|
+
if should_pause:
|
|
1191
|
+
remaining = sum(1 for m in plan.milestones if m.status == "pending")
|
|
1192
|
+
if remaining > 0:
|
|
1193
|
+
print(flush=True)
|
|
1194
|
+
if args.stop_at:
|
|
1195
|
+
print(f"[Boris] --stop-at {args.stop_at}: pausing after batch {batch_num}.", flush=True)
|
|
1196
|
+
else:
|
|
1197
|
+
print(f"[Boris] Incremental mode: pausing after batch {batch_num}.", flush=True)
|
|
1198
|
+
print(f"[Boris] {remaining} milestone(s) remaining.", flush=True)
|
|
1199
|
+
resume_flags = f"-r --turbo -d \"{project_dir}\""
|
|
1200
|
+
if args.stop_at:
|
|
1201
|
+
resume_flags += f" --stop-at {args.stop_at}"
|
|
1202
|
+
elif args.incremental:
|
|
1203
|
+
resume_flags += " --incremental"
|
|
1204
|
+
print(f"[Boris] Resume with: boris {resume_flags}", flush=True)
|
|
1205
|
+
logger.info("Pausing after batch %d. %d remaining.", batch_num, remaining)
|
|
1206
|
+
state_module.save(st)
|
|
1207
|
+
sys.exit(0)
|
|
1208
|
+
else:
|
|
1209
|
+
# === SEQUENTIAL MODE (original behavior) ===
|
|
1210
|
+
for i in range(start_index, len(plan.milestones)):
|
|
1211
|
+
milestone = plan.milestones[i]
|
|
1212
|
+
|
|
1213
|
+
if milestone.status == "completed":
|
|
1214
|
+
logger.info("Skipping already completed milestone %s", milestone.id)
|
|
1215
|
+
continue
|
|
1216
|
+
|
|
1217
|
+
if milestone.status == "skipped":
|
|
1218
|
+
logger.info("Skipping previously skipped milestone %s", milestone.id)
|
|
1219
|
+
continue
|
|
1220
|
+
|
|
1221
|
+
print(flush=True)
|
|
1222
|
+
print(f"=== Milestone {milestone.id}: {milestone.title} ===", flush=True)
|
|
1223
|
+
logger.info("Starting milestone %s: %s", milestone.id, milestone.title)
|
|
1224
|
+
|
|
1225
|
+
# Mark in progress
|
|
1226
|
+
milestone.status = "in_progress"
|
|
1227
|
+
st.current_milestone_index = i
|
|
1228
|
+
state_module.save(st)
|
|
611
1229
|
|
|
612
|
-
|
|
613
|
-
|
|
1230
|
+
# Craft prompt and execute
|
|
1231
|
+
print(f"[Boris] Crafting prompt for {milestone.id}...", flush=True)
|
|
1232
|
+
prompt = prompts.craft_prompt(milestone, plan, project_dir)
|
|
1233
|
+
print(f"[Boris] Prompt ready ({len(prompt)} chars). Spawning DaveLoop...", flush=True)
|
|
1234
|
+
result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
|
|
1235
|
+
|
|
1236
|
+
# Check verdict
|
|
1237
|
+
print(f"[Boris] DaveLoop finished. Checking verdict for {milestone.id}...", flush=True)
|
|
1238
|
+
verdict_result = engine.check(result, milestone)
|
|
1239
|
+
logger.info(
|
|
1240
|
+
"Milestone %s verdict: %s - %s",
|
|
1241
|
+
milestone.id,
|
|
1242
|
+
verdict_result.verdict.value,
|
|
1243
|
+
verdict_result.reason,
|
|
1244
|
+
)
|
|
1245
|
+
|
|
1246
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
1247
|
+
milestone.status = "completed"
|
|
1248
|
+
milestone.completed_at = datetime.now().isoformat()
|
|
1249
|
+
state_module.save(st)
|
|
614
1250
|
|
|
615
|
-
if
|
|
616
|
-
milestone.
|
|
617
|
-
|
|
1251
|
+
if not st.no_git:
|
|
1252
|
+
print(f"[Boris] Committing milestone {milestone.id} to git...", flush=True)
|
|
1253
|
+
if git_manager.commit_milestone(project_dir, milestone):
|
|
1254
|
+
logger.info("Git commit succeeded for milestone %s", milestone.id)
|
|
1255
|
+
else:
|
|
1256
|
+
logger.warning("Git commit FAILED for milestone %s", milestone.id)
|
|
1257
|
+
print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
|
|
1258
|
+
|
|
1259
|
+
print(f"[Boris] Milestone {milestone.id} COMPLETE", flush=True)
|
|
1260
|
+
logger.info("Milestone %s completed", milestone.id)
|
|
1261
|
+
|
|
1262
|
+
elif verdict_result.verdict == Verdict.OFF_PLAN:
|
|
1263
|
+
# Attempt correction
|
|
1264
|
+
correction_count = 0
|
|
1265
|
+
resolved = False
|
|
1266
|
+
|
|
1267
|
+
while correction_count < MAX_CORRECTIONS:
|
|
1268
|
+
correction_count += 1
|
|
1269
|
+
print(
|
|
1270
|
+
f" [Boris] Off-plan detected. Correction attempt {correction_count}/{MAX_CORRECTIONS}...",
|
|
1271
|
+
flush=True,
|
|
1272
|
+
)
|
|
1273
|
+
logger.info(
|
|
1274
|
+
"Off-plan correction attempt %d for milestone %s",
|
|
1275
|
+
correction_count,
|
|
1276
|
+
milestone.id,
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
correction_prompt = prompts.craft_correction(
|
|
1280
|
+
result.output, milestone, plan, verdict_result.reason
|
|
1281
|
+
)
|
|
1282
|
+
result = engine.run(correction_prompt, project_dir, args.max_iter, milestone=milestone)
|
|
1283
|
+
verdict_result = engine.check(result, milestone)
|
|
1284
|
+
|
|
1285
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
1286
|
+
milestone.status = "completed"
|
|
1287
|
+
milestone.completed_at = datetime.now().isoformat()
|
|
1288
|
+
state_module.save(st)
|
|
1289
|
+
|
|
1290
|
+
if not st.no_git:
|
|
1291
|
+
print(f"[Boris] Committing corrected milestone {milestone.id} to git...", flush=True)
|
|
1292
|
+
if git_manager.commit_milestone(project_dir, milestone):
|
|
1293
|
+
logger.info("Git commit succeeded for milestone %s (after correction)", milestone.id)
|
|
1294
|
+
else:
|
|
1295
|
+
logger.warning("Git commit FAILED for milestone %s (after correction)", milestone.id)
|
|
1296
|
+
print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
|
|
1297
|
+
|
|
1298
|
+
print(f"[Boris] Milestone {milestone.id} COMPLETE (after correction)", flush=True)
|
|
1299
|
+
logger.info("Milestone %s completed after correction", milestone.id)
|
|
1300
|
+
resolved = True
|
|
1301
|
+
break
|
|
1302
|
+
|
|
1303
|
+
if not resolved:
|
|
1304
|
+
print(f" [Boris] Milestone {milestone.id} could not be corrected. Skipping.", flush=True)
|
|
1305
|
+
logger.warning("Milestone %s skipped after failed corrections", milestone.id)
|
|
1306
|
+
milestone.status = "skipped"
|
|
618
1307
|
state_module.save(st)
|
|
619
1308
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
print(
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1309
|
+
elif verdict_result.verdict == Verdict.FAILED:
|
|
1310
|
+
# Retry logic
|
|
1311
|
+
retry_count = 0
|
|
1312
|
+
resolved = False
|
|
1313
|
+
|
|
1314
|
+
while retry_count < MAX_RETRIES:
|
|
1315
|
+
retry_count += 1
|
|
1316
|
+
milestone.retry_count = retry_count
|
|
1317
|
+
print(
|
|
1318
|
+
f" [Boris] Failed. Retry {retry_count}/{MAX_RETRIES}...",
|
|
1319
|
+
flush=True,
|
|
1320
|
+
)
|
|
1321
|
+
logger.info(
|
|
1322
|
+
"Retry %d for milestone %s", retry_count, milestone.id
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
|
|
1326
|
+
verdict_result = engine.check(result, milestone)
|
|
1327
|
+
|
|
1328
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
1329
|
+
milestone.status = "completed"
|
|
1330
|
+
milestone.completed_at = datetime.now().isoformat()
|
|
1331
|
+
state_module.save(st)
|
|
1332
|
+
|
|
1333
|
+
if not st.no_git:
|
|
1334
|
+
print(f"[Boris] Committing retried milestone {milestone.id} to git...", flush=True)
|
|
1335
|
+
if git_manager.commit_milestone(project_dir, milestone):
|
|
1336
|
+
logger.info("Git commit succeeded for milestone %s (after retry)", milestone.id)
|
|
1337
|
+
else:
|
|
1338
|
+
logger.warning("Git commit FAILED for milestone %s (after retry)", milestone.id)
|
|
1339
|
+
print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
|
|
1340
|
+
|
|
1341
|
+
print(f"[Boris] Milestone {milestone.id} COMPLETE (after retry)", flush=True)
|
|
1342
|
+
logger.info("Milestone %s completed after retry", milestone.id)
|
|
1343
|
+
resolved = True
|
|
1344
|
+
break
|
|
1345
|
+
|
|
1346
|
+
if not resolved:
|
|
1347
|
+
print(
|
|
1348
|
+
f" [Boris] Milestone {milestone.id} failed after {MAX_RETRIES} retries. Skipping.",
|
|
1349
|
+
flush=True,
|
|
1350
|
+
)
|
|
1351
|
+
logger.warning("Milestone %s skipped after failed retries", milestone.id)
|
|
1352
|
+
milestone.status = "skipped"
|
|
1353
|
+
state_module.save(st)
|
|
632
1354
|
|
|
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)
|
|
1355
|
+
# Save state after each milestone outcome
|
|
1356
|
+
state_module.save(st)
|
|
641
1357
|
|
|
642
|
-
|
|
643
|
-
|
|
1358
|
+
# Incremental mode: pause after each milestone
|
|
1359
|
+
# --stop-at: only pause when the target milestone completes
|
|
1360
|
+
should_pause = False
|
|
1361
|
+
if args.stop_at and milestone.status in ("completed", "skipped"):
|
|
1362
|
+
if milestone.id == args.stop_at:
|
|
1363
|
+
should_pause = True
|
|
1364
|
+
elif args.incremental and milestone.status in ("completed", "skipped"):
|
|
1365
|
+
should_pause = True
|
|
1366
|
+
|
|
1367
|
+
if should_pause:
|
|
1368
|
+
remaining = sum(1 for m in plan.milestones if m.status == "pending")
|
|
1369
|
+
if remaining > 0:
|
|
1370
|
+
print(flush=True)
|
|
1371
|
+
if args.stop_at:
|
|
1372
|
+
print(f"[Boris] --stop-at {args.stop_at}: pausing after {milestone.id}.", flush=True)
|
|
1373
|
+
else:
|
|
1374
|
+
print(f"[Boris] Incremental mode: pausing after {milestone.id}.", flush=True)
|
|
1375
|
+
print(f"[Boris] {remaining} milestone(s) remaining.", flush=True)
|
|
1376
|
+
resume_flags = f"-r -d \"{project_dir}\""
|
|
1377
|
+
if args.stop_at:
|
|
1378
|
+
resume_flags += f" --stop-at {args.stop_at}"
|
|
1379
|
+
elif args.incremental:
|
|
1380
|
+
resume_flags += " --incremental"
|
|
1381
|
+
print(f"[Boris] Resume with: boris {resume_flags}", flush=True)
|
|
1382
|
+
logger.info("Pausing after %s. %d remaining.", milestone.id, remaining)
|
|
1383
|
+
state_module.save(st)
|
|
1384
|
+
sys.exit(0)
|
|
644
1385
|
|
|
645
1386
|
# Phase 2: UI Testing & Polish
|
|
646
1387
|
_run_ui_phase(st, plan, project_dir, args, logger)
|