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.
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.1
2
+ Name: borisxdave
3
+ Version: 0.2.0
4
+ Summary: Boris - Autonomous Project Orchestrator
5
+ Requires-Python: >=3.8
@@ -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()
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.1
2
+ Name: borisxdave
3
+ Version: 0.2.0
4
+ Summary: Boris - Autonomous Project Orchestrator
5
+ Requires-Python: >=3.8