borisxdave 0.2.0__tar.gz
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.
- borisxdave-0.2.0/PKG-INFO +5 -0
- borisxdave-0.2.0/boris.py +672 -0
- borisxdave-0.2.0/borisxdave.egg-info/PKG-INFO +5 -0
- borisxdave-0.2.0/borisxdave.egg-info/SOURCES.txt +13 -0
- borisxdave-0.2.0/borisxdave.egg-info/dependency_links.txt +1 -0
- borisxdave-0.2.0/borisxdave.egg-info/entry_points.txt +2 -0
- borisxdave-0.2.0/borisxdave.egg-info/top_level.txt +7 -0
- borisxdave-0.2.0/config.py +12 -0
- borisxdave-0.2.0/engine.py +684 -0
- borisxdave-0.2.0/git_manager.py +248 -0
- borisxdave-0.2.0/planner.py +161 -0
- borisxdave-0.2.0/prompts.py +687 -0
- borisxdave-0.2.0/setup.cfg +4 -0
- borisxdave-0.2.0/setup.py +14 -0
- borisxdave-0.2.0/state.py +103 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Boris - Autonomous Project Orchestrator. Breaks tasks into milestones, executes via Claude CLI."""
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
# Force unbuffered stdout so all output shows immediately on Windows
|
|
10
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
11
|
+
try:
|
|
12
|
+
sys.stdout.reconfigure(line_buffering=True)
|
|
13
|
+
except Exception:
|
|
14
|
+
pass
|
|
15
|
+
os.environ["PYTHONUNBUFFERED"] = "1"
|
|
16
|
+
|
|
17
|
+
import engine
|
|
18
|
+
import git_manager
|
|
19
|
+
import prompts
|
|
20
|
+
import state as state_module
|
|
21
|
+
from engine import Verdict
|
|
22
|
+
|
|
23
|
+
# Config constants (inlined from config.py)
|
|
24
|
+
BORIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
25
|
+
PLANS_DIR = os.path.join(BORIS_DIR, "plans")
|
|
26
|
+
LOGS_DIR = os.path.join(BORIS_DIR, "logs")
|
|
27
|
+
DEFAULT_MAX_ITERATIONS = 15
|
|
28
|
+
MAX_CORRECTIONS = 2
|
|
29
|
+
MAX_RETRIES = 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def setup_logging() -> logging.Logger:
|
|
33
|
+
"""Set up logging to both console and file."""
|
|
34
|
+
os.makedirs(LOGS_DIR, exist_ok=True)
|
|
35
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
36
|
+
log_file = os.path.join(LOGS_DIR, f"boris_{timestamp}.log")
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("boris")
|
|
39
|
+
logger.setLevel(logging.DEBUG)
|
|
40
|
+
|
|
41
|
+
# File handler - detailed
|
|
42
|
+
fh = logging.FileHandler(log_file, encoding="utf-8")
|
|
43
|
+
fh.setLevel(logging.DEBUG)
|
|
44
|
+
fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s"))
|
|
45
|
+
|
|
46
|
+
# Console handler - info level
|
|
47
|
+
# Use sys.stdout but wrap with utf-8 on Windows to handle emoji/unicode in log messages
|
|
48
|
+
console_stream = sys.stdout
|
|
49
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
50
|
+
try:
|
|
51
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
ch = logging.StreamHandler(console_stream)
|
|
55
|
+
ch.setLevel(logging.INFO)
|
|
56
|
+
ch.setFormatter(logging.Formatter("[Boris] %(message)s"))
|
|
57
|
+
|
|
58
|
+
logger.addHandler(fh)
|
|
59
|
+
logger.addHandler(ch)
|
|
60
|
+
|
|
61
|
+
return logger
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def print_banner():
|
|
65
|
+
"""Print Boris startup banner."""
|
|
66
|
+
print("=" * 60, flush=True)
|
|
67
|
+
print(" BORIS - DaveLoop Orchestrator", flush=True)
|
|
68
|
+
print(" Breaking tasks into milestones, delegating to DaveLoop", flush=True)
|
|
69
|
+
print("=" * 60, flush=True)
|
|
70
|
+
print(flush=True)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def print_plan_summary(plan: state_module.Plan):
|
|
74
|
+
"""Print a summary of the plan."""
|
|
75
|
+
print(f"\nPlan: {plan.task}", flush=True)
|
|
76
|
+
print(f"Milestones: {len(plan.milestones)}", flush=True)
|
|
77
|
+
print("-" * 40, flush=True)
|
|
78
|
+
for m in plan.milestones:
|
|
79
|
+
deps = f" (depends on: {', '.join(m.depends_on)})" if m.depends_on else ""
|
|
80
|
+
print(f" {m.id}: {m.title}{deps}", flush=True)
|
|
81
|
+
print("-" * 40, flush=True)
|
|
82
|
+
print(flush=True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def generate_summary(plan: state_module.Plan, project_dir: str, start_time: datetime) -> str:
|
|
86
|
+
"""Generate a summary markdown file and return its path."""
|
|
87
|
+
os.makedirs(PLANS_DIR, exist_ok=True)
|
|
88
|
+
end_time = datetime.now()
|
|
89
|
+
duration = end_time - start_time
|
|
90
|
+
|
|
91
|
+
completed = [m for m in plan.milestones if m.status == "completed"]
|
|
92
|
+
skipped = [m for m in plan.milestones if m.status == "skipped"]
|
|
93
|
+
failed = [m for m in plan.milestones if m.status not in ("completed", "skipped")]
|
|
94
|
+
total = len(plan.milestones)
|
|
95
|
+
|
|
96
|
+
hours, remainder = divmod(int(duration.total_seconds()), 3600)
|
|
97
|
+
minutes, seconds = divmod(remainder, 60)
|
|
98
|
+
duration_str = f"{hours}h {minutes}m {seconds}s" if hours else f"{minutes}m {seconds}s"
|
|
99
|
+
|
|
100
|
+
lines = [
|
|
101
|
+
"# Boris Orchestration Summary",
|
|
102
|
+
"",
|
|
103
|
+
f"**Task:** {plan.task}",
|
|
104
|
+
f"**Project:** {project_dir}",
|
|
105
|
+
f"**Started:** {start_time.strftime('%Y-%m-%d %H:%M:%S')}",
|
|
106
|
+
f"**Finished:** {end_time.strftime('%Y-%m-%d %H:%M:%S')}",
|
|
107
|
+
f"**Duration:** {duration_str}",
|
|
108
|
+
"",
|
|
109
|
+
"---",
|
|
110
|
+
"",
|
|
111
|
+
"## Results",
|
|
112
|
+
"",
|
|
113
|
+
f"| Metric | Count |",
|
|
114
|
+
f"|--------|-------|",
|
|
115
|
+
f"| Total milestones | {total} |",
|
|
116
|
+
f"| Completed | {len(completed)} |",
|
|
117
|
+
f"| Skipped | {len(skipped)} |",
|
|
118
|
+
f"| Remaining | {len(failed)} |",
|
|
119
|
+
"",
|
|
120
|
+
f"**Overall: {'ALL MILESTONES COMPLETED' if len(completed) == total else 'PARTIAL COMPLETION'}**",
|
|
121
|
+
"",
|
|
122
|
+
"---",
|
|
123
|
+
"",
|
|
124
|
+
"## Milestone Breakdown",
|
|
125
|
+
"",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
status_icons = {"completed": "+", "skipped": "!", "pending": " ", "in_progress": ">"}
|
|
129
|
+
|
|
130
|
+
for m in plan.milestones:
|
|
131
|
+
icon = status_icons.get(m.status, "?")
|
|
132
|
+
lines.append(f"### [{icon}] {m.id}: {m.title}")
|
|
133
|
+
lines.append(f"**Status:** {m.status.upper()}")
|
|
134
|
+
if m.completed_at:
|
|
135
|
+
lines.append(f"**Completed at:** {m.completed_at}")
|
|
136
|
+
if m.files_to_create:
|
|
137
|
+
lines.append(f"**Files created:** {', '.join(m.files_to_create)}")
|
|
138
|
+
if m.files_to_modify:
|
|
139
|
+
lines.append(f"**Files modified:** {', '.join(m.files_to_modify)}")
|
|
140
|
+
if m.status == "skipped":
|
|
141
|
+
lines.append(f"**Reason:** Skipped after exhausting retries/corrections")
|
|
142
|
+
lines.append("")
|
|
143
|
+
|
|
144
|
+
if skipped:
|
|
145
|
+
lines.append("---")
|
|
146
|
+
lines.append("")
|
|
147
|
+
lines.append("## Skipped Milestones")
|
|
148
|
+
lines.append("")
|
|
149
|
+
for m in skipped:
|
|
150
|
+
deps = f" (depends on: {', '.join(m.depends_on)})" if m.depends_on else ""
|
|
151
|
+
lines.append(f"- **{m.id}: {m.title}**{deps} - skipped after failed retries/corrections")
|
|
152
|
+
lines.append("")
|
|
153
|
+
|
|
154
|
+
lines.append("---")
|
|
155
|
+
lines.append("")
|
|
156
|
+
lines.append("*Generated by Boris - Autonomous Project Orchestrator*")
|
|
157
|
+
|
|
158
|
+
timestamp = end_time.strftime("%Y%m%d_%H%M%S")
|
|
159
|
+
filepath = os.path.join(PLANS_DIR, f"summary_{timestamp}.md")
|
|
160
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
161
|
+
f.write("\n".join(lines))
|
|
162
|
+
|
|
163
|
+
return filepath
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def print_final_summary(plan: state_module.Plan, project_dir: str, start_time: datetime):
|
|
167
|
+
"""Print final summary to console and save summary markdown."""
|
|
168
|
+
completed = sum(1 for m in plan.milestones if m.status == "completed")
|
|
169
|
+
skipped = sum(1 for m in plan.milestones if m.status == "skipped")
|
|
170
|
+
total = len(plan.milestones)
|
|
171
|
+
|
|
172
|
+
# Generate summary file
|
|
173
|
+
summary_path = generate_summary(plan, project_dir, start_time)
|
|
174
|
+
|
|
175
|
+
print(flush=True)
|
|
176
|
+
print("=" * 60, flush=True)
|
|
177
|
+
print(" BORIS - Orchestration Summary", flush=True)
|
|
178
|
+
print("=" * 60, flush=True)
|
|
179
|
+
print(f" Completed: {completed}/{total}", flush=True)
|
|
180
|
+
if skipped:
|
|
181
|
+
print(f" Skipped: {skipped}/{total}", flush=True)
|
|
182
|
+
print(flush=True)
|
|
183
|
+
for m in plan.milestones:
|
|
184
|
+
status_icon = {"completed": "+", "skipped": "-", "pending": " ", "in_progress": ">"}.get(
|
|
185
|
+
m.status, "?"
|
|
186
|
+
)
|
|
187
|
+
print(f" [{status_icon}] {m.id}: {m.title} ({m.status})", flush=True)
|
|
188
|
+
print(flush=True)
|
|
189
|
+
print(f" Summary saved to: {summary_path}", flush=True)
|
|
190
|
+
print(flush=True)
|
|
191
|
+
if completed == total:
|
|
192
|
+
print("Boris orchestration complete! All milestones delivered.", flush=True)
|
|
193
|
+
else:
|
|
194
|
+
print(f"Boris orchestration finished. {skipped} milestone(s) skipped.", flush=True)
|
|
195
|
+
print("=" * 60, flush=True)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def parse_args() -> argparse.Namespace:
|
|
199
|
+
"""Parse CLI arguments."""
|
|
200
|
+
parser = argparse.ArgumentParser(
|
|
201
|
+
prog="boris",
|
|
202
|
+
description="Boris - Autonomous Project Orchestrator. Breaks tasks into milestones and executes via Claude CLI.",
|
|
203
|
+
)
|
|
204
|
+
parser.add_argument("task", nargs="?", help="The task description string")
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"-d", "--dir", default=os.getcwd(), help="Working directory for the project (default: current dir)"
|
|
207
|
+
)
|
|
208
|
+
parser.add_argument(
|
|
209
|
+
"-r", "--resume", action="store_true", help="Resume from last saved state"
|
|
210
|
+
)
|
|
211
|
+
parser.add_argument(
|
|
212
|
+
"--plan-only", action="store_true", help="Generate plan but don't execute"
|
|
213
|
+
)
|
|
214
|
+
parser.add_argument("--remote", help="Git remote URL to set up")
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"--no-git", action="store_true", help="Disable git management"
|
|
217
|
+
)
|
|
218
|
+
parser.add_argument(
|
|
219
|
+
"--max-iter",
|
|
220
|
+
type=int,
|
|
221
|
+
default=DEFAULT_MAX_ITERATIONS,
|
|
222
|
+
help=f"Max DaveLoop iterations per milestone (default: {DEFAULT_MAX_ITERATIONS})",
|
|
223
|
+
)
|
|
224
|
+
parser.add_argument(
|
|
225
|
+
"--skip-ui", action="store_true", help="Skip UI testing phase after structural milestones"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return parser.parse_args()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _run_ui_phase(st, plan, project_dir, args, logger):
|
|
232
|
+
"""Phase 2: UI Testing & Polish. Boris ships DaveLoop into UI tester mode."""
|
|
233
|
+
skip_ui = getattr(args, "skip_ui", False)
|
|
234
|
+
if skip_ui:
|
|
235
|
+
print("[Boris] --skip-ui flag set. Skipping UI testing phase.", flush=True)
|
|
236
|
+
logger.info("UI testing phase skipped by --skip-ui flag")
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
structural_completed = sum(1 for m in plan.milestones if m.status == "completed")
|
|
240
|
+
structural_total = len(plan.milestones)
|
|
241
|
+
|
|
242
|
+
if structural_completed < structural_total:
|
|
243
|
+
logger.info("Not all structural milestones complete (%d/%d), skipping UI phase",
|
|
244
|
+
structural_completed, structural_total)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Transition to UI phase
|
|
248
|
+
st.phase = "ui_testing"
|
|
249
|
+
state_module.save(st)
|
|
250
|
+
|
|
251
|
+
print(flush=True)
|
|
252
|
+
print("=" * 60, flush=True)
|
|
253
|
+
print(" BORIS - Phase 2: UI Testing & Polish", flush=True)
|
|
254
|
+
print("=" * 60, flush=True)
|
|
255
|
+
print(flush=True)
|
|
256
|
+
print("[Boris] All structural milestones complete. Entering UI Testing & Polish phase...", flush=True)
|
|
257
|
+
logger.info("Entering UI Testing & Polish phase")
|
|
258
|
+
|
|
259
|
+
# Create UI plan if we don't have one yet.
|
|
260
|
+
# Claude figures out the project type and test tool from the task + file listing.
|
|
261
|
+
if st.ui_plan is None:
|
|
262
|
+
print("[Boris] Creating UI testing plan...", flush=True)
|
|
263
|
+
ui_plan = prompts.create_ui_plan(plan.task, project_dir)
|
|
264
|
+
st.ui_plan = ui_plan
|
|
265
|
+
state_module.save(st)
|
|
266
|
+
print(f"[Boris] UI plan created with {len(ui_plan.milestones)} milestones", flush=True)
|
|
267
|
+
logger.info("UI plan created with %d milestones", len(ui_plan.milestones))
|
|
268
|
+
|
|
269
|
+
_run_ui_milestones(st, plan, project_dir, args, logger)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _run_ui_milestones(st, plan, project_dir, args, logger):
|
|
273
|
+
"""Execute the UI milestone loop. Same pattern as structural but ships DaveLoop in UI test mode."""
|
|
274
|
+
ui_plan = st.ui_plan
|
|
275
|
+
if ui_plan is None:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
for i, ui_milestone in enumerate(ui_plan.milestones):
|
|
279
|
+
if ui_milestone.status == "completed":
|
|
280
|
+
logger.info("Skipping already completed UI milestone %s", ui_milestone.id)
|
|
281
|
+
continue
|
|
282
|
+
if ui_milestone.status == "skipped":
|
|
283
|
+
logger.info("Skipping previously skipped UI milestone %s", ui_milestone.id)
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
print(flush=True)
|
|
287
|
+
print(f"=== UI Milestone {ui_milestone.id}: {ui_milestone.title} ===", flush=True)
|
|
288
|
+
logger.info("Starting UI milestone %s: %s", ui_milestone.id, ui_milestone.title)
|
|
289
|
+
|
|
290
|
+
ui_milestone.status = "in_progress"
|
|
291
|
+
state_module.save(st)
|
|
292
|
+
|
|
293
|
+
# Craft prompt and ship DaveLoop
|
|
294
|
+
print(f"[Boris] Crafting UI test prompt for {ui_milestone.id}...", flush=True)
|
|
295
|
+
prompt = prompts.craft_ui_prompt(ui_milestone, ui_plan, plan, project_dir)
|
|
296
|
+
print(f"[Boris] Prompt ready ({len(prompt)} chars). Spawning DaveLoop in UI Tester Mode...", flush=True)
|
|
297
|
+
result = engine.run_ui_test(prompt, project_dir, args.max_iter, ui_milestone=ui_milestone)
|
|
298
|
+
|
|
299
|
+
# Check verdict
|
|
300
|
+
print(f"[Boris] DaveLoop finished. Checking UI verdict for {ui_milestone.id}...", flush=True)
|
|
301
|
+
verdict_result = engine.check_ui(result, ui_milestone, ui_plan.test_tool)
|
|
302
|
+
logger.info(
|
|
303
|
+
"UI milestone %s verdict: %s - %s",
|
|
304
|
+
ui_milestone.id, verdict_result.verdict.value, verdict_result.reason,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
308
|
+
ui_milestone.status = "completed"
|
|
309
|
+
ui_milestone.completed_at = datetime.now().isoformat()
|
|
310
|
+
state_module.save(st)
|
|
311
|
+
|
|
312
|
+
if not st.no_git:
|
|
313
|
+
# Lightweight commit for UI fixes
|
|
314
|
+
ui_commit_ok = git_manager.commit_milestone(project_dir, state_module.Milestone(
|
|
315
|
+
id=ui_milestone.id, title=ui_milestone.title, description="",
|
|
316
|
+
depends_on=[], acceptance_criteria=[], files_to_create=[], files_to_modify=[],
|
|
317
|
+
status="completed",
|
|
318
|
+
))
|
|
319
|
+
if ui_commit_ok:
|
|
320
|
+
logger.info("Git commit succeeded for UI milestone %s", ui_milestone.id)
|
|
321
|
+
else:
|
|
322
|
+
logger.warning("Git commit FAILED for UI milestone %s", ui_milestone.id)
|
|
323
|
+
print(f" [Boris] WARNING: Git commit failed for {ui_milestone.id}", flush=True)
|
|
324
|
+
|
|
325
|
+
print(f"[Boris] UI Milestone {ui_milestone.id} COMPLETE", flush=True)
|
|
326
|
+
if ui_milestone.issues_found:
|
|
327
|
+
print(f" [Boris] Issues found: {len(ui_milestone.issues_found)}", flush=True)
|
|
328
|
+
if ui_milestone.issues_fixed:
|
|
329
|
+
print(f" [Boris] Issues fixed: {len(ui_milestone.issues_fixed)}", flush=True)
|
|
330
|
+
logger.info("UI milestone %s completed", ui_milestone.id)
|
|
331
|
+
|
|
332
|
+
elif verdict_result.verdict == Verdict.OFF_PLAN:
|
|
333
|
+
correction_count = 0
|
|
334
|
+
resolved = False
|
|
335
|
+
|
|
336
|
+
while correction_count < MAX_CORRECTIONS:
|
|
337
|
+
correction_count += 1
|
|
338
|
+
print(
|
|
339
|
+
f" [Boris] Off-plan detected. UI correction {correction_count}/{MAX_CORRECTIONS}...",
|
|
340
|
+
flush=True,
|
|
341
|
+
)
|
|
342
|
+
logger.info("Off-plan UI correction %d for %s", correction_count, ui_milestone.id)
|
|
343
|
+
|
|
344
|
+
correction_prompt = prompts.craft_ui_correction(
|
|
345
|
+
result.output, ui_milestone, ui_plan, verdict_result.reason
|
|
346
|
+
)
|
|
347
|
+
result = engine.run_ui_test(correction_prompt, project_dir, args.max_iter, ui_milestone=ui_milestone)
|
|
348
|
+
verdict_result = engine.check_ui(result, ui_milestone, ui_plan.test_tool)
|
|
349
|
+
|
|
350
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
351
|
+
ui_milestone.status = "completed"
|
|
352
|
+
ui_milestone.completed_at = datetime.now().isoformat()
|
|
353
|
+
state_module.save(st)
|
|
354
|
+
print(f"[Boris] UI Milestone {ui_milestone.id} COMPLETE (after correction)", flush=True)
|
|
355
|
+
logger.info("UI milestone %s completed after correction", ui_milestone.id)
|
|
356
|
+
resolved = True
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
if not resolved:
|
|
360
|
+
print(f" [Boris] UI Milestone {ui_milestone.id} could not be corrected. Skipping.", flush=True)
|
|
361
|
+
logger.warning("UI milestone %s skipped after failed corrections", ui_milestone.id)
|
|
362
|
+
ui_milestone.status = "skipped"
|
|
363
|
+
state_module.save(st)
|
|
364
|
+
|
|
365
|
+
elif verdict_result.verdict == Verdict.FAILED:
|
|
366
|
+
retry_count = 0
|
|
367
|
+
resolved = False
|
|
368
|
+
|
|
369
|
+
while retry_count < MAX_RETRIES:
|
|
370
|
+
retry_count += 1
|
|
371
|
+
ui_milestone.retry_count = retry_count
|
|
372
|
+
print(f" [Boris] Failed. UI retry {retry_count}/{MAX_RETRIES}...", flush=True)
|
|
373
|
+
logger.info("UI retry %d for %s", retry_count, ui_milestone.id)
|
|
374
|
+
|
|
375
|
+
result = engine.run_ui_test(prompt, project_dir, args.max_iter, ui_milestone=ui_milestone)
|
|
376
|
+
verdict_result = engine.check_ui(result, ui_milestone, ui_plan.test_tool)
|
|
377
|
+
|
|
378
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
379
|
+
ui_milestone.status = "completed"
|
|
380
|
+
ui_milestone.completed_at = datetime.now().isoformat()
|
|
381
|
+
state_module.save(st)
|
|
382
|
+
print(f"[Boris] UI Milestone {ui_milestone.id} COMPLETE (after retry)", flush=True)
|
|
383
|
+
logger.info("UI milestone %s completed after retry", ui_milestone.id)
|
|
384
|
+
resolved = True
|
|
385
|
+
break
|
|
386
|
+
|
|
387
|
+
if not resolved:
|
|
388
|
+
print(f" [Boris] UI Milestone {ui_milestone.id} failed after retries. Skipping.", flush=True)
|
|
389
|
+
logger.warning("UI milestone %s skipped after failed retries", ui_milestone.id)
|
|
390
|
+
ui_milestone.status = "skipped"
|
|
391
|
+
state_module.save(st)
|
|
392
|
+
|
|
393
|
+
state_module.save(st)
|
|
394
|
+
|
|
395
|
+
# UI phase summary
|
|
396
|
+
ui_completed = sum(1 for m in ui_plan.milestones if m.status == "completed")
|
|
397
|
+
ui_total = len(ui_plan.milestones)
|
|
398
|
+
print(flush=True)
|
|
399
|
+
print(f"[Boris] UI Testing complete: {ui_completed}/{ui_total} milestones passed", flush=True)
|
|
400
|
+
logger.info("UI testing complete: %d/%d", ui_completed, ui_total)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def main():
|
|
404
|
+
"""Main Boris orchestration loop."""
|
|
405
|
+
args = parse_args()
|
|
406
|
+
logger = setup_logging()
|
|
407
|
+
|
|
408
|
+
# Create required dirs
|
|
409
|
+
os.makedirs(PLANS_DIR, exist_ok=True)
|
|
410
|
+
os.makedirs(LOGS_DIR, exist_ok=True)
|
|
411
|
+
|
|
412
|
+
project_dir = os.path.abspath(args.dir)
|
|
413
|
+
|
|
414
|
+
if args.resume:
|
|
415
|
+
# Resume mode
|
|
416
|
+
st = state_module.load(project_dir)
|
|
417
|
+
if st is None:
|
|
418
|
+
print("[Boris] Error: No saved state found. Cannot resume.", flush=True)
|
|
419
|
+
sys.exit(1)
|
|
420
|
+
plan = st.plan
|
|
421
|
+
start_index = st.current_milestone_index
|
|
422
|
+
|
|
423
|
+
# If already in UI testing phase, skip straight to UI loop
|
|
424
|
+
if st.phase == "ui_testing":
|
|
425
|
+
print(f"[Boris] Resuming UI Testing phase...", flush=True)
|
|
426
|
+
logger.info("Resuming UI testing phase")
|
|
427
|
+
else:
|
|
428
|
+
print(f"[Boris] Resuming from milestone {start_index + 1}...", flush=True)
|
|
429
|
+
logger.info("Resuming from milestone %d", start_index + 1)
|
|
430
|
+
else:
|
|
431
|
+
# New task mode
|
|
432
|
+
if not args.task:
|
|
433
|
+
print("[Boris] Error: task is required (unless using --resume)", flush=True)
|
|
434
|
+
sys.exit(1)
|
|
435
|
+
|
|
436
|
+
print_banner()
|
|
437
|
+
logger.info("Starting Boris for task: %s", args.task)
|
|
438
|
+
|
|
439
|
+
# Create plan (retry once on timeout)
|
|
440
|
+
print("[Boris] Creating plan...", flush=True)
|
|
441
|
+
plan = None
|
|
442
|
+
for attempt in range(2):
|
|
443
|
+
try:
|
|
444
|
+
plan = prompts.create_plan(args.task, project_dir)
|
|
445
|
+
break
|
|
446
|
+
except RuntimeError as e:
|
|
447
|
+
if attempt == 0 and "timed out" in str(e).lower():
|
|
448
|
+
print(f" [Boris] Plan generation timed out. Retrying...", flush=True)
|
|
449
|
+
logger.warning("Plan timed out, retrying (attempt %d)", attempt + 2)
|
|
450
|
+
else:
|
|
451
|
+
print(f"[Boris] Error creating plan: {e}", flush=True)
|
|
452
|
+
logger.error("Plan creation failed: %s", e)
|
|
453
|
+
sys.exit(1)
|
|
454
|
+
if plan is None:
|
|
455
|
+
print("[Boris] Error: Failed to create plan after retries.", flush=True)
|
|
456
|
+
sys.exit(1)
|
|
457
|
+
print(f"[Boris] Plan created with {len(plan.milestones)} milestones", flush=True)
|
|
458
|
+
logger.info("Plan created with %d milestones", len(plan.milestones))
|
|
459
|
+
|
|
460
|
+
print_plan_summary(plan)
|
|
461
|
+
|
|
462
|
+
if args.plan_only:
|
|
463
|
+
print("[Boris] Plan-only mode. Exiting.", flush=True)
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
# Create initial state
|
|
467
|
+
st = state_module.State(
|
|
468
|
+
plan=plan,
|
|
469
|
+
current_milestone_index=0,
|
|
470
|
+
project_dir=project_dir,
|
|
471
|
+
git_remote=args.remote,
|
|
472
|
+
no_git=args.no_git,
|
|
473
|
+
)
|
|
474
|
+
state_module.save(st)
|
|
475
|
+
start_index = 0
|
|
476
|
+
|
|
477
|
+
# Track start time for summary
|
|
478
|
+
start_time = datetime.now()
|
|
479
|
+
|
|
480
|
+
# Git setup
|
|
481
|
+
if not st.no_git:
|
|
482
|
+
print("[Boris] Initializing git repo...", flush=True)
|
|
483
|
+
git_manager.init_repo(project_dir)
|
|
484
|
+
print("[Boris] Verifying git config (user.name/email)...", flush=True)
|
|
485
|
+
if git_manager.ensure_git_config(project_dir):
|
|
486
|
+
logger.info("Git config verified")
|
|
487
|
+
else:
|
|
488
|
+
logger.warning("Git config verification had issues - commits may fail")
|
|
489
|
+
print(" [Boris] WARNING: Git config issues detected. Commits may fail.", flush=True)
|
|
490
|
+
if args.remote:
|
|
491
|
+
print(f"[Boris] Setting up remote: {args.remote}", flush=True)
|
|
492
|
+
git_manager.setup_remote(project_dir, args.remote)
|
|
493
|
+
|
|
494
|
+
# Phase 1: Structural milestones (skip if resuming into UI phase)
|
|
495
|
+
try:
|
|
496
|
+
if st.phase != "ui_testing":
|
|
497
|
+
for i in range(start_index, len(plan.milestones)):
|
|
498
|
+
milestone = plan.milestones[i]
|
|
499
|
+
|
|
500
|
+
if milestone.status == "completed":
|
|
501
|
+
logger.info("Skipping already completed milestone %s", milestone.id)
|
|
502
|
+
continue
|
|
503
|
+
|
|
504
|
+
if milestone.status == "skipped":
|
|
505
|
+
logger.info("Skipping previously skipped milestone %s", milestone.id)
|
|
506
|
+
continue
|
|
507
|
+
|
|
508
|
+
print(flush=True)
|
|
509
|
+
print(f"=== Milestone {milestone.id}: {milestone.title} ===", flush=True)
|
|
510
|
+
logger.info("Starting milestone %s: %s", milestone.id, milestone.title)
|
|
511
|
+
|
|
512
|
+
# Mark in progress
|
|
513
|
+
milestone.status = "in_progress"
|
|
514
|
+
st.current_milestone_index = i
|
|
515
|
+
state_module.save(st)
|
|
516
|
+
|
|
517
|
+
# Craft prompt and execute
|
|
518
|
+
print(f"[Boris] Crafting prompt for {milestone.id}...", flush=True)
|
|
519
|
+
prompt = prompts.craft_prompt(milestone, plan, project_dir)
|
|
520
|
+
print(f"[Boris] Prompt ready ({len(prompt)} chars). Spawning DaveLoop...", flush=True)
|
|
521
|
+
result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
|
|
522
|
+
|
|
523
|
+
# Check verdict
|
|
524
|
+
print(f"[Boris] DaveLoop finished. Checking verdict for {milestone.id}...", flush=True)
|
|
525
|
+
verdict_result = engine.check(result, milestone)
|
|
526
|
+
logger.info(
|
|
527
|
+
"Milestone %s verdict: %s - %s",
|
|
528
|
+
milestone.id,
|
|
529
|
+
verdict_result.verdict.value,
|
|
530
|
+
verdict_result.reason,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
534
|
+
milestone.status = "completed"
|
|
535
|
+
milestone.completed_at = datetime.now().isoformat()
|
|
536
|
+
state_module.save(st)
|
|
537
|
+
|
|
538
|
+
if not st.no_git:
|
|
539
|
+
print(f"[Boris] Committing milestone {milestone.id} to git...", flush=True)
|
|
540
|
+
if git_manager.commit_milestone(project_dir, milestone):
|
|
541
|
+
logger.info("Git commit succeeded for milestone %s", milestone.id)
|
|
542
|
+
else:
|
|
543
|
+
logger.warning("Git commit FAILED for milestone %s", milestone.id)
|
|
544
|
+
print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
|
|
545
|
+
|
|
546
|
+
print(f"[Boris] Milestone {milestone.id} COMPLETE", flush=True)
|
|
547
|
+
logger.info("Milestone %s completed", milestone.id)
|
|
548
|
+
|
|
549
|
+
elif verdict_result.verdict == Verdict.OFF_PLAN:
|
|
550
|
+
# Attempt correction
|
|
551
|
+
correction_count = 0
|
|
552
|
+
resolved = False
|
|
553
|
+
|
|
554
|
+
while correction_count < MAX_CORRECTIONS:
|
|
555
|
+
correction_count += 1
|
|
556
|
+
print(
|
|
557
|
+
f" [Boris] Off-plan detected. Correction attempt {correction_count}/{MAX_CORRECTIONS}...",
|
|
558
|
+
flush=True,
|
|
559
|
+
)
|
|
560
|
+
logger.info(
|
|
561
|
+
"Off-plan correction attempt %d for milestone %s",
|
|
562
|
+
correction_count,
|
|
563
|
+
milestone.id,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
correction_prompt = prompts.craft_correction(
|
|
567
|
+
result.output, milestone, plan, verdict_result.reason
|
|
568
|
+
)
|
|
569
|
+
result = engine.run(correction_prompt, project_dir, args.max_iter, milestone=milestone)
|
|
570
|
+
verdict_result = engine.check(result, milestone)
|
|
571
|
+
|
|
572
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
573
|
+
milestone.status = "completed"
|
|
574
|
+
milestone.completed_at = datetime.now().isoformat()
|
|
575
|
+
state_module.save(st)
|
|
576
|
+
|
|
577
|
+
if not st.no_git:
|
|
578
|
+
print(f"[Boris] Committing corrected milestone {milestone.id} to git...", flush=True)
|
|
579
|
+
if git_manager.commit_milestone(project_dir, milestone):
|
|
580
|
+
logger.info("Git commit succeeded for milestone %s (after correction)", milestone.id)
|
|
581
|
+
else:
|
|
582
|
+
logger.warning("Git commit FAILED for milestone %s (after correction)", milestone.id)
|
|
583
|
+
print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
|
|
584
|
+
|
|
585
|
+
print(f"[Boris] Milestone {milestone.id} COMPLETE (after correction)", flush=True)
|
|
586
|
+
logger.info("Milestone %s completed after correction", milestone.id)
|
|
587
|
+
resolved = True
|
|
588
|
+
break
|
|
589
|
+
|
|
590
|
+
if not resolved:
|
|
591
|
+
print(f" [Boris] Milestone {milestone.id} could not be corrected. Skipping.", flush=True)
|
|
592
|
+
logger.warning("Milestone %s skipped after failed corrections", milestone.id)
|
|
593
|
+
milestone.status = "skipped"
|
|
594
|
+
state_module.save(st)
|
|
595
|
+
|
|
596
|
+
elif verdict_result.verdict == Verdict.FAILED:
|
|
597
|
+
# Retry logic
|
|
598
|
+
retry_count = 0
|
|
599
|
+
resolved = False
|
|
600
|
+
|
|
601
|
+
while retry_count < MAX_RETRIES:
|
|
602
|
+
retry_count += 1
|
|
603
|
+
milestone.retry_count = retry_count
|
|
604
|
+
print(
|
|
605
|
+
f" [Boris] Failed. Retry {retry_count}/{MAX_RETRIES}...",
|
|
606
|
+
flush=True,
|
|
607
|
+
)
|
|
608
|
+
logger.info(
|
|
609
|
+
"Retry %d for milestone %s", retry_count, milestone.id
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
|
|
613
|
+
verdict_result = engine.check(result, milestone)
|
|
614
|
+
|
|
615
|
+
if verdict_result.verdict == Verdict.RESOLVED:
|
|
616
|
+
milestone.status = "completed"
|
|
617
|
+
milestone.completed_at = datetime.now().isoformat()
|
|
618
|
+
state_module.save(st)
|
|
619
|
+
|
|
620
|
+
if not st.no_git:
|
|
621
|
+
print(f"[Boris] Committing retried milestone {milestone.id} to git...", flush=True)
|
|
622
|
+
if git_manager.commit_milestone(project_dir, milestone):
|
|
623
|
+
logger.info("Git commit succeeded for milestone %s (after retry)", milestone.id)
|
|
624
|
+
else:
|
|
625
|
+
logger.warning("Git commit FAILED for milestone %s (after retry)", milestone.id)
|
|
626
|
+
print(f" [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)
|
|
627
|
+
|
|
628
|
+
print(f"[Boris] Milestone {milestone.id} COMPLETE (after retry)", flush=True)
|
|
629
|
+
logger.info("Milestone %s completed after retry", milestone.id)
|
|
630
|
+
resolved = True
|
|
631
|
+
break
|
|
632
|
+
|
|
633
|
+
if not resolved:
|
|
634
|
+
print(
|
|
635
|
+
f" [Boris] Milestone {milestone.id} failed after {MAX_RETRIES} retries. Skipping.",
|
|
636
|
+
flush=True,
|
|
637
|
+
)
|
|
638
|
+
logger.warning("Milestone %s skipped after failed retries", milestone.id)
|
|
639
|
+
milestone.status = "skipped"
|
|
640
|
+
state_module.save(st)
|
|
641
|
+
|
|
642
|
+
# Save state after each milestone outcome
|
|
643
|
+
state_module.save(st)
|
|
644
|
+
|
|
645
|
+
# Phase 2: UI Testing & Polish
|
|
646
|
+
_run_ui_phase(st, plan, project_dir, args, logger)
|
|
647
|
+
|
|
648
|
+
except KeyboardInterrupt:
|
|
649
|
+
print(flush=True)
|
|
650
|
+
print(f"[Boris] Interrupted. State saved. Resume with: boris -r -d \"{project_dir}\"", flush=True)
|
|
651
|
+
logger.info("Interrupted by user. State saved.")
|
|
652
|
+
state_module.save(st)
|
|
653
|
+
sys.exit(130)
|
|
654
|
+
|
|
655
|
+
# Final push
|
|
656
|
+
if not st.no_git:
|
|
657
|
+
print("[Boris] Pushing final changes to remote...", flush=True)
|
|
658
|
+
git_manager.final_push(project_dir)
|
|
659
|
+
|
|
660
|
+
# Summary and proper exit
|
|
661
|
+
print_final_summary(plan, project_dir, start_time)
|
|
662
|
+
|
|
663
|
+
completed = sum(1 for m in plan.milestones if m.status == "completed")
|
|
664
|
+
total = len(plan.milestones)
|
|
665
|
+
if completed == total:
|
|
666
|
+
sys.exit(0)
|
|
667
|
+
else:
|
|
668
|
+
sys.exit(1)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
if __name__ == "__main__":
|
|
672
|
+
main()
|