#!/usr/bin/env python3
"""Boris - Autonomous Project Orchestrator. Breaks tasks into milestones, executes via Claude CLI."""
import argparse
import logging
import os
import sys
from datetime import datetime

# Force unbuffered stdout so all output shows immediately on Windows
if hasattr(sys.stdout, "reconfigure"):
    try:
        sys.stdout.reconfigure(line_buffering=True)
    except Exception:
        pass
os.environ["PYTHONUNBUFFERED"] = "1"

import engine
import git_manager
import prompts
import state as state_module
from engine import Verdict

# Config constants (inlined from config.py)
BORIS_DIR = os.path.dirname(os.path.abspath(__file__))
PLANS_DIR = os.path.join(BORIS_DIR, "plans")
LOGS_DIR = os.path.join(BORIS_DIR, "logs")
DEFAULT_MAX_ITERATIONS = 15
MAX_CORRECTIONS = 2
MAX_RETRIES = 1

# Swarm mode configuration presets
SWARM_PRESETS = {
    "conservative": {
        "swarm_budget": 3,
        "swarm_depth": 1,
        "isolation": "worktree",
        "no_converge": False,
    },
    "balanced": {
        "swarm_budget": 5,
        "swarm_depth": 1,
        "isolation": "worktree",
        "no_converge": False,
    },
    "aggressive": {
        "swarm_budget": 10,
        "swarm_depth": 2,
        "isolation": "worktree",
        "no_converge": False,
    },
    "yolo": {  # Closest to the original emergent behavior
        "swarm_budget": 999,
        "swarm_depth": 2,
        "isolation": "none",
        "no_converge": True,
    },
}


class TokenEstimator:
    """Estimates token usage for Boris orchestration.

    Claude Code runs on a token-based subscription model, not pay-per-API-call.
    This estimator helps users understand token consumption before committing to
    a task, and allows setting token budgets to limit usage.

    Token estimates are based on realistic DaveLoop iteration profiles:
    - Base iteration: ~4000 input tokens (prompt) + ~2000 output tokens (response)
    - Context growth: each iteration adds ~1500 tokens of accumulated context
    - Sub-agent spawn: ~3000 tokens each
    - Convergence run: ~5000 tokens (reads multiple files + analysis)
    """

    BASE_INPUT_TOKENS = 4000       # Base input tokens per DaveLoop iteration
    BASE_OUTPUT_TOKENS = 2000      # Base output tokens per DaveLoop iteration
    CONTEXT_GROWTH_PER_ITER = 1500 # Additional context tokens accumulated per iteration
    TOKENS_PER_SUB_AGENT = 3000   # Tokens per sub-agent spawn
    TOKENS_PER_CONVERGENCE = 5000 # Tokens per convergence run

    def __init__(self, max_tokens: int = None):
        self.max_tokens = max_tokens
        self.estimated_tokens = 0

    def _tokens_per_iteration(self, iteration_num: int) -> int:
        """Estimate tokens for a single DaveLoop iteration, accounting for context growth."""
        input_tokens = self.BASE_INPUT_TOKENS + (iteration_num * self.CONTEXT_GROWTH_PER_ITER)
        return input_tokens + self.BASE_OUTPUT_TOKENS

    def record_tokens(self, amount: int):
        """Record estimated token usage."""
        self.estimated_tokens += amount

    def estimate_batch(self, num_workers: int, avg_iterations: int,
                       avg_sub_agents: int) -> int:
        """Estimate tokens for a batch of parallel workers."""
        worker_tokens = 0
        for w in range(num_workers):
            for i in range(avg_iterations):
                worker_tokens += self._tokens_per_iteration(i)
        sub_agent_tokens = num_workers * avg_sub_agents * self.TOKENS_PER_SUB_AGENT
        convergence_tokens = self.TOKENS_PER_CONVERGENCE
        return worker_tokens + sub_agent_tokens + convergence_tokens

    def estimate_task(self, plan) -> dict:
        """Estimate total tokens for a Boris plan.

        Returns a dict with breakdown: per-milestone estimates, sub-agent tokens,
        convergence tokens, and total.
        """
        milestones = plan.milestones
        num_milestones = len(milestones)
        avg_iterations = 8  # Typical DaveLoop iterations per milestone

        milestone_tokens = 0
        for i in range(avg_iterations):
            milestone_tokens += self._tokens_per_iteration(i)
        total_milestone_tokens = milestone_tokens * num_milestones

        # Estimate convergence runs (one per parallel batch, assume ~ceil(milestones/3) batches)
        estimated_batches = max(1, (num_milestones + 2) // 3)
        convergence_tokens = estimated_batches * self.TOKENS_PER_CONVERGENCE

        total = total_milestone_tokens + convergence_tokens

        return {
            "num_milestones": num_milestones,
            "avg_iterations_per_milestone": avg_iterations,
            "tokens_per_milestone": milestone_tokens,
            "total_milestone_tokens": total_milestone_tokens,
            "convergence_tokens": convergence_tokens,
            "total_tokens": total,
        }

    def check_budget(self, estimated_additional: int) -> bool:
        """Check if estimated additional tokens fit within budget."""
        if self.max_tokens is None:
            return True
        return (self.estimated_tokens + estimated_additional) <= self.max_tokens

    def summary(self) -> dict:
        """Return token tracking summary."""
        return {
            "estimated_tokens": self.estimated_tokens,
            "max_tokens": self.max_tokens,
            "remaining": (self.max_tokens - self.estimated_tokens) if self.max_tokens else None,
        }


def setup_logging() -> logging.Logger:
    """Set up logging to both console and file."""
    os.makedirs(LOGS_DIR, exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    log_file = os.path.join(LOGS_DIR, f"boris_{timestamp}.log")

    logger = logging.getLogger("boris")
    logger.setLevel(logging.DEBUG)

    # File handler - detailed
    fh = logging.FileHandler(log_file, encoding="utf-8")
    fh.setLevel(logging.DEBUG)
    fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s"))

    # Console handler - info level
    # Use sys.stdout but wrap with utf-8 on Windows to handle emoji/unicode in log messages
    console_stream = sys.stdout
    if hasattr(sys.stdout, "reconfigure"):
        try:
            sys.stdout.reconfigure(encoding="utf-8", errors="replace")
        except Exception:
            pass
    ch = logging.StreamHandler(console_stream)
    ch.setLevel(logging.INFO)
    ch.setFormatter(logging.Formatter("[Boris] %(message)s"))

    logger.addHandler(fh)
    logger.addHandler(ch)

    return logger


def print_banner():
    """Print Boris startup banner."""
    print("=" * 60, flush=True)
    print("  BORIS - DaveLoop Orchestrator", flush=True)
    print("  Breaking tasks into milestones, delegating to DaveLoop", flush=True)
    print("=" * 60, flush=True)
    print(flush=True)


def print_plan_summary(plan: state_module.Plan):
    """Print a summary of the plan."""
    print(f"\nPlan: {plan.task}", flush=True)
    print(f"Milestones: {len(plan.milestones)}", flush=True)
    print("-" * 40, flush=True)
    for m in plan.milestones:
        deps = f" (depends on: {', '.join(m.depends_on)})" if m.depends_on else ""
        print(f"  {m.id}: {m.title}{deps}", flush=True)
    print("-" * 40, flush=True)
    print(flush=True)


def prompt_skip_milestones(plan: state_module.Plan, logger: logging.Logger) -> set:
    """Prompt the user to optionally skip milestones before execution.

    Returns a set of milestone IDs that were marked as skipped.
    """
    valid_ids = {m.id for m in plan.milestones}

    try:
        user_input = input("Enter milestone IDs to skip (comma-separated), or press Enter to run all: ").strip()
    except EOFError:
        # Non-interactive environment, skip nothing
        return set()

    if not user_input:
        return set()

    # Parse comma-separated milestone IDs
    requested_ids = {s.strip().upper() for s in user_input.split(",") if s.strip()}
    skip_ids = requested_ids & valid_ids
    invalid_ids = requested_ids - valid_ids

    if invalid_ids:
        print(f"[Boris] WARNING: Unknown milestone IDs ignored: {', '.join(sorted(invalid_ids))}", flush=True)
        logger.warning("Unknown milestone IDs in skip request: %s", invalid_ids)

    if not skip_ids:
        return set()

    # Check for dependency warnings: if a skipped milestone is depended on by a non-skipped one
    skipped_set = skip_ids
    for m in plan.milestones:
        if m.id in skipped_set:
            continue
        depends_on_skipped = [dep for dep in m.depends_on if dep in skipped_set]
        if depends_on_skipped:
            print(
                f"[Boris] WARNING: {m.id} ({m.title}) depends on skipped milestone(s): "
                f"{', '.join(depends_on_skipped)}",
                flush=True,
            )
            logger.warning(
                "Milestone %s depends on skipped milestone(s): %s",
                m.id, depends_on_skipped,
            )

    # Mark milestones as skipped
    for m in plan.milestones:
        if m.id in skip_ids:
            m.status = "skipped"
            print(f"[Boris] Skipping milestone {m.id}: {m.title}", flush=True)
            logger.info("User skipped milestone %s: %s", m.id, m.title)

    return skip_ids


def generate_summary(plan: state_module.Plan, project_dir: str, start_time: datetime) -> str:
    """Generate a summary markdown file and return its path."""
    os.makedirs(PLANS_DIR, exist_ok=True)
    end_time = datetime.now()
    duration = end_time - start_time

    completed = [m for m in plan.milestones if m.status == "completed"]
    skipped = [m for m in plan.milestones if m.status == "skipped"]
    failed = [m for m in plan.milestones if m.status not in ("completed", "skipped")]
    total = len(plan.milestones)

    hours, remainder = divmod(int(duration.total_seconds()), 3600)
    minutes, seconds = divmod(remainder, 60)
    duration_str = f"{hours}h {minutes}m {seconds}s" if hours else f"{minutes}m {seconds}s"

    lines = [
        "# Boris Orchestration Summary",
        "",
        f"**Task:** {plan.task}",
        f"**Project:** {project_dir}",
        f"**Started:** {start_time.strftime('%Y-%m-%d %H:%M:%S')}",
        f"**Finished:** {end_time.strftime('%Y-%m-%d %H:%M:%S')}",
        f"**Duration:** {duration_str}",
        "",
        "---",
        "",
        "## Results",
        "",
        f"| Metric | Count |",
        f"|--------|-------|",
        f"| Total milestones | {total} |",
        f"| Completed | {len(completed)} |",
        f"| Skipped | {len(skipped)} |",
        f"| Remaining | {len(failed)} |",
        "",
        f"**Overall: {'ALL MILESTONES COMPLETED' if len(completed) == total else 'PARTIAL COMPLETION'}**",
        "",
        "---",
        "",
        "## Milestone Breakdown",
        "",
    ]

    status_icons = {"completed": "+", "skipped": "!", "pending": " ", "in_progress": ">"}

    for m in plan.milestones:
        icon = status_icons.get(m.status, "?")
        lines.append(f"### [{icon}] {m.id}: {m.title}")
        lines.append(f"**Status:** {m.status.upper()}")
        if m.completed_at:
            lines.append(f"**Completed at:** {m.completed_at}")
        if m.files_to_create:
            lines.append(f"**Files created:** {', '.join(m.files_to_create)}")
        if m.files_to_modify:
            lines.append(f"**Files modified:** {', '.join(m.files_to_modify)}")
        if m.status == "skipped":
            lines.append(f"**Reason:** Skipped after exhausting retries/corrections")
        lines.append("")

    if skipped:
        lines.append("---")
        lines.append("")
        lines.append("## Skipped Milestones")
        lines.append("")
        for m in skipped:
            deps = f" (depends on: {', '.join(m.depends_on)})" if m.depends_on else ""
            lines.append(f"- **{m.id}: {m.title}**{deps} - skipped after failed retries/corrections")
        lines.append("")

    lines.append("---")
    lines.append("")
    lines.append("*Generated by Boris - Autonomous Project Orchestrator*")

    timestamp = end_time.strftime("%Y%m%d_%H%M%S")
    filepath = os.path.join(PLANS_DIR, f"summary_{timestamp}.md")
    with open(filepath, "w", encoding="utf-8") as f:
        f.write("\n".join(lines))

    return filepath


def print_final_summary(plan: state_module.Plan, project_dir: str, start_time: datetime):
    """Print final summary to console and save summary markdown."""
    completed = sum(1 for m in plan.milestones if m.status == "completed")
    skipped = sum(1 for m in plan.milestones if m.status == "skipped")
    total = len(plan.milestones)

    # Generate summary file
    summary_path = generate_summary(plan, project_dir, start_time)

    print(flush=True)
    print("=" * 60, flush=True)
    print("  BORIS - Orchestration Summary", flush=True)
    print("=" * 60, flush=True)
    print(f"  Completed: {completed}/{total}", flush=True)
    if skipped:
        print(f"  Skipped:   {skipped}/{total}", flush=True)
    print(flush=True)
    for m in plan.milestones:
        status_icon = {"completed": "+", "skipped": "-", "pending": " ", "in_progress": ">"}.get(
            m.status, "?"
        )
        print(f"  [{status_icon}] {m.id}: {m.title} ({m.status})", flush=True)
    print(flush=True)
    print(f"  Summary saved to: {summary_path}", flush=True)
    print(flush=True)
    if completed == total:
        print("Boris orchestration complete! All milestones delivered.", flush=True)
    else:
        print(f"Boris orchestration finished. {skipped} milestone(s) skipped.", flush=True)
    print("=" * 60, flush=True)


def parse_args() -> argparse.Namespace:
    """Parse CLI arguments."""
    parser = argparse.ArgumentParser(
        prog="boris",
        description="Boris - Autonomous Project Orchestrator. Breaks tasks into milestones and executes via Claude CLI.",
    )
    parser.add_argument("task", nargs="?", help="The task description string")
    parser.add_argument(
        "-f", "--file", help="Read task description from a file"
    )
    parser.add_argument(
        "-d", "--dir", default=os.getcwd(), help="Working directory for the project (default: current dir)"
    )
    parser.add_argument(
        "-r", "--resume", action="store_true", help="Resume from last saved state"
    )
    plan_group = parser.add_mutually_exclusive_group()
    plan_group.add_argument(
        "--plan-only", action="store_true", help="Generate plan but don't execute"
    )
    plan_group.add_argument(
        "--exec-only", metavar="PLAN_FILE",
        help="Skip planning and execute an existing plan file directly"
    )
    parser.add_argument("--remote", help="Git remote URL to set up")
    parser.add_argument(
        "--no-git", action="store_true", help="Disable git management"
    )
    parser.add_argument(
        "--max-iter",
        type=int,
        default=DEFAULT_MAX_ITERATIONS,
        help=f"Max DaveLoop iterations per milestone (default: {DEFAULT_MAX_ITERATIONS})",
    )
    parser.add_argument(
        "--skip-ui", action="store_true", help="Skip UI testing phase after structural milestones"
    )
    parser.add_argument(
        "--incremental", action="store_true",
        help="Execute one milestone at a time, then pause. Use with -r to continue to the next milestone."
    )
    parser.add_argument(
        "--turbo", action="store_true",
        help="Enable parallel DaveLoop execution for independent milestones"
    )
    parser.add_argument(
        "--stop-at", metavar="MILESTONE_ID", dest="stop_at",
        help="Stop execution after completing this milestone ID (e.g. M4). Implies --incremental."
    )

    # Swarm mode flags
    parser.add_argument(
        "--swarm", action="store_true",
        help="Enable swarm mode: DaveLoops can spawn sub-agents via Task tool"
    )
    parser.add_argument(
        "--swarm-budget", type=int, default=5,
        help="Max sub-agents per DaveLoop worker in swarm mode (default: 5)"
    )
    parser.add_argument(
        "--swarm-depth", type=int, default=1, choices=[1, 2],
        help="Max sub-agent depth in swarm mode (default: 1, no recursive spawning)"
    )
    parser.add_argument(
        "--preset", choices=["conservative", "balanced", "aggressive", "yolo"],
        help="Apply a swarm configuration preset (implies --swarm)"
    )
    parser.add_argument(
        "--isolation", choices=["none", "worktree"], default="none",
        help="Isolation strategy for parallel workers (default: none)"
    )
    parser.add_argument(
        "--no-converge", action="store_true", dest="no_converge",
        help="Skip convergence phase after each swarm batch"
    )
    parser.add_argument(
        "--max-tokens", type=int, default=None, dest="max_tokens",
        help="Maximum estimated token budget for swarm/turbo mode execution (e.g. 500000)"
    )
    parser.add_argument(
        "--estimate", metavar="TASK", nargs="?", const="__FROM_TASK__", dest="estimate",
        help="Generate plan and print token estimate WITHOUT executing. "
             "Use alone (boris --estimate 'task') or with positional task."
    )

    return parser.parse_args()


def _run_ui_phase(st, plan, project_dir, args, logger):
    """Phase 2: UI Testing & Polish. Boris ships DaveLoop into UI tester mode."""
    skip_ui = getattr(args, "skip_ui", False)
    if skip_ui:
        print("[Boris] --skip-ui flag set. Skipping UI testing phase.", flush=True)
        logger.info("UI testing phase skipped by --skip-ui flag")
        return

    structural_completed = sum(1 for m in plan.milestones if m.status == "completed")
    structural_total = len(plan.milestones)

    if structural_completed < structural_total:
        logger.info("Not all structural milestones complete (%d/%d), skipping UI phase",
                     structural_completed, structural_total)
        return

    # Transition to UI phase
    st.phase = "ui_testing"
    state_module.save(st)

    print(flush=True)
    print("=" * 60, flush=True)
    print("  BORIS - Phase 2: UI Testing & Polish", flush=True)
    print("=" * 60, flush=True)
    print(flush=True)
    print("[Boris] All structural milestones complete. Entering UI Testing & Polish phase...", flush=True)
    logger.info("Entering UI Testing & Polish phase")

    # Create UI plan if we don't have one yet.
    # Claude figures out the project type and test tool from the task + file listing.
    if st.ui_plan is None:
        print("[Boris] Creating UI testing plan...", flush=True)
        ui_plan = prompts.create_ui_plan(plan.task, project_dir)
        st.ui_plan = ui_plan
        state_module.save(st)
        print(f"[Boris] UI plan created with {len(ui_plan.milestones)} milestones", flush=True)
        logger.info("UI plan created with %d milestones", len(ui_plan.milestones))

    _run_ui_milestones(st, plan, project_dir, args, logger)


def _run_ui_milestones(st, plan, project_dir, args, logger):
    """Execute the UI milestone loop. Same pattern as structural but ships DaveLoop in UI test mode."""
    ui_plan = st.ui_plan
    if ui_plan is None:
        return

    for i, ui_milestone in enumerate(ui_plan.milestones):
        if ui_milestone.status == "completed":
            logger.info("Skipping already completed UI milestone %s", ui_milestone.id)
            continue
        if ui_milestone.status == "skipped":
            logger.info("Skipping previously skipped UI milestone %s", ui_milestone.id)
            continue

        print(flush=True)
        print(f"=== UI Milestone {ui_milestone.id}: {ui_milestone.title} ===", flush=True)
        logger.info("Starting UI milestone %s: %s", ui_milestone.id, ui_milestone.title)

        ui_milestone.status = "in_progress"
        state_module.save(st)

        # Craft prompt and ship DaveLoop
        print(f"[Boris] Crafting UI test prompt for {ui_milestone.id}...", flush=True)
        prompt = prompts.craft_ui_prompt(ui_milestone, ui_plan, plan, project_dir)
        print(f"[Boris] Prompt ready ({len(prompt)} chars). Spawning DaveLoop in UI Tester Mode...", flush=True)
        result = engine.run_ui_test(prompt, project_dir, args.max_iter, ui_milestone=ui_milestone)

        # Check verdict
        print(f"[Boris] DaveLoop finished. Checking UI verdict for {ui_milestone.id}...", flush=True)
        verdict_result = engine.check_ui(result, ui_milestone, ui_plan.test_tool)
        logger.info(
            "UI milestone %s verdict: %s - %s",
            ui_milestone.id, verdict_result.verdict.value, verdict_result.reason,
        )

        if verdict_result.verdict == Verdict.RESOLVED:
            ui_milestone.status = "completed"
            ui_milestone.completed_at = datetime.now().isoformat()
            state_module.save(st)

            if not st.no_git:
                # Lightweight commit for UI fixes
                ui_commit_ok = git_manager.commit_milestone(project_dir, state_module.Milestone(
                    id=ui_milestone.id, title=ui_milestone.title, description="",
                    depends_on=[], acceptance_criteria=[], files_to_create=[], files_to_modify=[],
                    status="completed",
                ))
                if ui_commit_ok:
                    logger.info("Git commit succeeded for UI milestone %s", ui_milestone.id)
                else:
                    logger.warning("Git commit FAILED for UI milestone %s", ui_milestone.id)
                    print(f"  [Boris] WARNING: Git commit failed for {ui_milestone.id}", flush=True)

            print(f"[Boris] UI Milestone {ui_milestone.id} COMPLETE", flush=True)
            if ui_milestone.issues_found:
                print(f"  [Boris] Issues found: {len(ui_milestone.issues_found)}", flush=True)
            if ui_milestone.issues_fixed:
                print(f"  [Boris] Issues fixed: {len(ui_milestone.issues_fixed)}", flush=True)
            logger.info("UI milestone %s completed", ui_milestone.id)

        elif verdict_result.verdict == Verdict.OFF_PLAN:
            correction_count = 0
            resolved = False

            while correction_count < MAX_CORRECTIONS:
                correction_count += 1
                print(
                    f"  [Boris] Off-plan detected. UI correction {correction_count}/{MAX_CORRECTIONS}...",
                    flush=True,
                )
                logger.info("Off-plan UI correction %d for %s", correction_count, ui_milestone.id)

                correction_prompt = prompts.craft_ui_correction(
                    result.output, ui_milestone, ui_plan, verdict_result.reason
                )
                result = engine.run_ui_test(correction_prompt, project_dir, args.max_iter, ui_milestone=ui_milestone)
                verdict_result = engine.check_ui(result, ui_milestone, ui_plan.test_tool)

                if verdict_result.verdict == Verdict.RESOLVED:
                    ui_milestone.status = "completed"
                    ui_milestone.completed_at = datetime.now().isoformat()
                    state_module.save(st)
                    print(f"[Boris] UI Milestone {ui_milestone.id} COMPLETE (after correction)", flush=True)
                    logger.info("UI milestone %s completed after correction", ui_milestone.id)
                    resolved = True
                    break

            if not resolved:
                print(f"  [Boris] UI Milestone {ui_milestone.id} could not be corrected. Skipping.", flush=True)
                logger.warning("UI milestone %s skipped after failed corrections", ui_milestone.id)
                ui_milestone.status = "skipped"
                state_module.save(st)

        elif verdict_result.verdict == Verdict.FAILED:
            retry_count = 0
            resolved = False

            while retry_count < MAX_RETRIES:
                retry_count += 1
                ui_milestone.retry_count = retry_count
                print(f"  [Boris] Failed. UI retry {retry_count}/{MAX_RETRIES}...", flush=True)
                logger.info("UI retry %d for %s", retry_count, ui_milestone.id)

                result = engine.run_ui_test(prompt, project_dir, args.max_iter, ui_milestone=ui_milestone)
                verdict_result = engine.check_ui(result, ui_milestone, ui_plan.test_tool)

                if verdict_result.verdict == Verdict.RESOLVED:
                    ui_milestone.status = "completed"
                    ui_milestone.completed_at = datetime.now().isoformat()
                    state_module.save(st)
                    print(f"[Boris] UI Milestone {ui_milestone.id} COMPLETE (after retry)", flush=True)
                    logger.info("UI milestone %s completed after retry", ui_milestone.id)
                    resolved = True
                    break

            if not resolved:
                print(f"  [Boris] UI Milestone {ui_milestone.id} failed after retries. Skipping.", flush=True)
                logger.warning("UI milestone %s skipped after failed retries", ui_milestone.id)
                ui_milestone.status = "skipped"
                state_module.save(st)

        state_module.save(st)

    # UI phase summary
    ui_completed = sum(1 for m in ui_plan.milestones if m.status == "completed")
    ui_total = len(ui_plan.milestones)
    print(flush=True)
    print(f"[Boris] UI Testing complete: {ui_completed}/{ui_total} milestones passed", flush=True)
    logger.info("UI testing complete: %d/%d", ui_completed, ui_total)


def get_ready_milestones(plan: state_module.Plan) -> list[state_module.Milestone]:
    """Return all pending milestones whose dependencies are all completed or skipped."""
    done_statuses = {"completed", "skipped"}
    done_ids = {m.id for m in plan.milestones if m.status in done_statuses}
    ready = []
    for m in plan.milestones:
        if m.status != "pending":
            continue
        if all(dep in done_ids for dep in m.depends_on):
            ready.append(m)
    return ready


def _process_milestone_verdict(verdict_result, result, milestone, plan, st, project_dir, args, logger, prompt):
    """Process a single milestone's verdict (corrections, retries). Used by both sequential and turbo modes."""
    if verdict_result.verdict == Verdict.RESOLVED:
        milestone.status = "completed"
        milestone.completed_at = datetime.now().isoformat()
        state_module.save(st)
        print(f"[Boris] Milestone {milestone.id} COMPLETE", flush=True)
        logger.info("Milestone %s completed", milestone.id)

    elif verdict_result.verdict == Verdict.OFF_PLAN:
        correction_count = 0
        resolved = False

        while correction_count < MAX_CORRECTIONS:
            correction_count += 1
            print(
                f"  [Boris] Off-plan detected. Correction attempt {correction_count}/{MAX_CORRECTIONS}...",
                flush=True,
            )
            logger.info(
                "Off-plan correction attempt %d for milestone %s",
                correction_count,
                milestone.id,
            )

            correction_prompt = prompts.craft_correction(
                result.output, milestone, plan, verdict_result.reason
            )
            result = engine.run(correction_prompt, project_dir, args.max_iter, milestone=milestone)
            verdict_result = engine.check(result, milestone)

            if verdict_result.verdict == Verdict.RESOLVED:
                milestone.status = "completed"
                milestone.completed_at = datetime.now().isoformat()
                state_module.save(st)
                print(f"[Boris] Milestone {milestone.id} COMPLETE (after correction)", flush=True)
                logger.info("Milestone %s completed after correction", milestone.id)
                resolved = True
                break

        if not resolved:
            print(f"  [Boris] Milestone {milestone.id} could not be corrected. Skipping.", flush=True)
            logger.warning("Milestone %s skipped after failed corrections", milestone.id)
            milestone.status = "skipped"
            state_module.save(st)

    elif verdict_result.verdict == Verdict.FAILED:
        retry_count = 0
        resolved = False

        while retry_count < MAX_RETRIES:
            retry_count += 1
            milestone.retry_count = retry_count
            print(
                f"  [Boris] Failed. Retry {retry_count}/{MAX_RETRIES}...",
                flush=True,
            )
            logger.info(
                "Retry %d for milestone %s", retry_count, milestone.id
            )

            result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
            verdict_result = engine.check(result, milestone)

            if verdict_result.verdict == Verdict.RESOLVED:
                milestone.status = "completed"
                milestone.completed_at = datetime.now().isoformat()
                state_module.save(st)
                print(f"[Boris] Milestone {milestone.id} COMPLETE (after retry)", flush=True)
                logger.info("Milestone %s completed after retry", milestone.id)
                resolved = True
                break

        if not resolved:
            print(
                f"  [Boris] Milestone {milestone.id} failed after {MAX_RETRIES} retries. Skipping.",
                flush=True,
            )
            logger.warning("Milestone %s skipped after failed retries", milestone.id)
            milestone.status = "skipped"
            state_module.save(st)


def validate_turbo_batch(ready: list, plan: state_module.Plan, logger: logging.Logger) -> list:
    """Filter out milestones from a turbo batch whose dependencies were skipped.

    These milestones cannot succeed without their dependency's output.
    Also enforces foundation-first: first batch only runs milestones with no dependencies.
    """
    skipped_ids = {m.id for m in plan.milestones if m.status == "skipped"}
    valid = []
    for m in ready:
        skipped_deps = [d for d in m.depends_on if d in skipped_ids]
        if skipped_deps:
            logger.warning(
                "Milestone %s depends on skipped milestone(s) %s - deferring from turbo batch",
                m.id, skipped_deps
            )
            print(f"[Boris] WARNING: {m.id} skipped from turbo batch - depends on skipped: {skipped_deps}", flush=True)
        else:
            valid.append(m)
    return valid


def _run_convergence(st, plan, project_dir, batch_milestones, logger):
    """Run a convergence agent after a parallel batch to resolve conflicts."""
    completed_in_batch = [m for m in batch_milestones if m.status == "completed"]
    if len(completed_in_batch) < 2:
        return  # No conflicts possible with 0-1 completed milestones

    # Collect all files touched by this batch and check for overlaps
    file_owners = {}
    conflicts = []
    for m in completed_in_batch:
        for f in (m.files_to_create or []) + (m.files_to_modify or []):
            if f in file_owners:
                conflicts.append((f, file_owners[f], m.id))
            file_owners[f] = m.id

    if not conflicts:
        logger.info("Convergence: No file conflicts detected in batch")
        print("[Boris] Convergence: No conflicts detected. Skipping.", flush=True)
        return

    print(f"[Boris] Convergence: {len(conflicts)} potential conflict(s) detected", flush=True)
    for filepath, owner1, owner2 in conflicts:
        print(f"  [Boris]   {filepath}: written by {owner1} and {owner2}", flush=True)

    # Build convergence prompt
    conflict_text = "\n".join(
        f"- {f}: modified by {o1} and {o2}" for f, o1, o2 in conflicts
    )

    convergence_prompt = f"""# Convergence Task

Multiple parallel agents modified overlapping files. Resolve any conflicts.

## Conflicts Detected
{conflict_text}

## Milestones Completed in This Batch
{chr(10).join(f'- {m.id}: {m.title}' for m in completed_in_batch)}

## Instructions
1. Read each conflicted file
2. Check for type mismatches, duplicate definitions, incompatible interfaces
3. Reconcile into a consistent state
4. Run `tsc --noEmit` (TypeScript) or equivalent type checker
5. Fix any remaining build errors
6. Do NOT add new features - only resolve conflicts between existing code

When all conflicts are resolved and the build is clean, output [DAVELOOP:RESOLVED].
"""

    # Run convergence via DaveLoop (without Task tool - no swarm for convergence)
    result = engine.run(convergence_prompt, project_dir, max_iterations=5)
    if result.resolved:
        print("[Boris] Convergence: All conflicts resolved", flush=True)
        logger.info("Convergence phase completed successfully")
    else:
        print("[Boris] WARNING: Convergence could not resolve all conflicts", flush=True)
        logger.warning("Convergence phase did not fully resolve")


def _print_swarm_dashboard(project_dir: str, batch_num: int, batch_milestones: list):
    """Print a swarm status dashboard showing all active workers in the current batch (B7)."""
    statuses = engine.read_worker_statuses(project_dir)
    if not statuses:
        return

    active = sum(1 for s in statuses.values() if s.get("state") in ("starting", "working"))
    total_actions = sum(s.get("actions", 0) for s in statuses.values())

    print(flush=True)
    print("=== BORIS SWARM STATUS ===", flush=True)
    print(f"Batch {batch_num} | Workers: {active} active / {len(statuses)} total | Actions: {total_actions}", flush=True)
    print(flush=True)

    for m in batch_milestones:
        status = statuses.get(m.id, {})
        state = status.get("state", "unknown")
        actions = status.get("actions", 0)
        reasoning = status.get("reasoning_blocks", 0)
        interrupts = status.get("interrupts", 0)
        last = status.get("last_action", "")

        # Progress indicator based on reasoning blocks (rough proxy)
        bar_len = min(reasoning, 10)
        bar = "#" * bar_len + "-" * (10 - bar_len)

        state_str = state.upper()
        interrupt_str = f" | {interrupts} interrupts" if interrupts > 0 else ""
        print(f"[{m.id}] {m.title[:30]:<30} [{bar}] {state_str} | {actions} actions{interrupt_str}", flush=True)
        if last:
            print(f"      Last: {last}", flush=True)

    # Show file locks if file_lock.py is available
    try:
        from file_lock import FileLockManager
        flm = FileLockManager(project_dir)
        locks = flm.get_locked_files()
        if locks:
            lock_strs = [f"{os.path.basename(f)} ({owner})" for f, owner in locks.items()]
            print(f"\nFile locks: {', '.join(lock_strs)}", flush=True)
    except ImportError:
        pass

    print("===========================", flush=True)
    print(flush=True)


def _apply_preset(args):
    """Apply a swarm preset to args if specified. Preset implies --swarm."""
    if not args.preset:
        return
    args.swarm = True
    preset = SWARM_PRESETS[args.preset]
    # Only override if user didn't explicitly set these
    if args.swarm_budget == 5:  # default value
        args.swarm_budget = preset["swarm_budget"]
    if args.swarm_depth == 1:  # default value
        args.swarm_depth = preset["swarm_depth"]
    if args.isolation == "none":  # default value
        args.isolation = preset.get("isolation", "none")
    if not args.no_converge:
        args.no_converge = preset["no_converge"]


def main():
    """Main Boris orchestration loop."""
    args = parse_args()

    # Apply swarm preset if specified (implies --swarm)
    _apply_preset(args)

    # --stop-at implies incremental mode
    if args.stop_at:
        args.incremental = True

    # Handle --estimate: resolve the task description from either the flag or positional arg
    if args.estimate is not None:
        estimate_task = args.estimate if args.estimate != "__FROM_TASK__" else args.task
        if not estimate_task:
            print("[Boris] Error: --estimate requires a task description", flush=True)
            sys.exit(1)
        args.task = estimate_task

    logger = setup_logging()

    # Initialize token estimator for swarm/turbo mode
    token_estimator = None
    if getattr(args, 'swarm', False) or getattr(args, 'max_tokens', None) or getattr(args, 'turbo', False):
        token_estimator = TokenEstimator(max_tokens=args.max_tokens)

    # Create required dirs
    os.makedirs(PLANS_DIR, exist_ok=True)
    os.makedirs(LOGS_DIR, exist_ok=True)

    project_dir = os.path.abspath(args.dir)

    if args.resume:
        # Resume mode
        st = state_module.load(project_dir)
        if st is None:
            print("[Boris] Error: No saved state found. Cannot resume.", flush=True)
            sys.exit(1)
        plan = st.plan
        start_index = st.current_milestone_index

        # If already in UI testing phase, skip straight to UI loop
        if st.phase == "ui_testing":
            print(f"[Boris] Resuming UI Testing phase...", flush=True)
            logger.info("Resuming UI testing phase")
        else:
            print(f"[Boris] Resuming from milestone {start_index + 1}...", flush=True)
            logger.info("Resuming from milestone %d", start_index + 1)
    elif args.exec_only:
        # Exec-only mode - load an existing plan file and execute it
        plan_file = os.path.abspath(args.exec_only)
        print_banner()
        print(f"[Boris] Exec-only mode: loading plan from {plan_file}", flush=True)
        logger.info("Exec-only mode: loading plan from %s", plan_file)

        try:
            plan = prompts.load_plan_from_file(plan_file)
        except (FileNotFoundError, RuntimeError) as e:
            print(f"[Boris] Error loading plan file: {e}", flush=True)
            logger.error("Failed to load plan file: %s", e)
            sys.exit(1)

        print(f"[Boris] Plan loaded with {len(plan.milestones)} milestones", flush=True)
        logger.info("Plan loaded with %d milestones", len(plan.milestones))

        print_plan_summary(plan)

        # Prompt user to skip milestones before execution
        prompt_skip_milestones(plan, logger)

        # Create initial state from the loaded plan
        st = state_module.State(
            plan=plan,
            current_milestone_index=0,
            project_dir=project_dir,
            git_remote=args.remote,
            no_git=args.no_git,
            turbo=args.turbo,
        )
        state_module.save(st)
        start_index = 0
    else:
        # New task mode - resolve task from args or file
        task = args.task
        if args.file:
            try:
                with open(args.file, "r", encoding="utf-8") as f:
                    task = f.read().strip()
                print(f"[Boris] Read task from file: {args.file} ({len(task)} chars)", flush=True)
            except FileNotFoundError:
                print(f"[Boris] Error: File not found: {args.file}", flush=True)
                sys.exit(1)
            except Exception as e:
                print(f"[Boris] Error reading file: {e}", flush=True)
                sys.exit(1)

        if not task:
            print("[Boris] Error: task is required (pass as argument or use -f FILE)", flush=True)
            sys.exit(1)

        args.task = task

        print_banner()
        logger.info("Starting Boris for task: %s", args.task[:200])

        # Create plan (retry once on timeout)
        print("[Boris] Creating plan...", flush=True)
        plan = None
        for attempt in range(2):
            try:
                plan = prompts.create_plan(args.task, project_dir, turbo=args.turbo)
                break
            except RuntimeError as e:
                if attempt == 0 and "timed out" in str(e).lower():
                    print(f"  [Boris] Plan generation timed out. Retrying...", flush=True)
                    logger.warning("Plan timed out, retrying (attempt %d)", attempt + 2)
                else:
                    print(f"[Boris] Error creating plan: {e}", flush=True)
                    logger.error("Plan creation failed: %s", e)
                    sys.exit(1)
        if plan is None:
            print("[Boris] Error: Failed to create plan after retries.", flush=True)
            sys.exit(1)
        print(f"[Boris] Plan created with {len(plan.milestones)} milestones", flush=True)
        logger.info("Plan created with %d milestones", len(plan.milestones))

        print_plan_summary(plan)

        # --estimate mode: print token estimate and exit without executing
        if args.estimate is not None:
            estimator = TokenEstimator()
            estimate = estimator.estimate_task(plan)
            print("=" * 60, flush=True)
            print("  BORIS - Token Estimate", flush=True)
            print("=" * 60, flush=True)
            print(f"  Milestones:          {estimate['num_milestones']}", flush=True)
            print(f"  Avg iterations each: {estimate['avg_iterations_per_milestone']}", flush=True)
            print(f"  Tokens/milestone:    {estimate['tokens_per_milestone']:,}", flush=True)
            print(f"  Milestone tokens:    {estimate['total_milestone_tokens']:,}", flush=True)
            print(f"  Convergence tokens:  {estimate['convergence_tokens']:,}", flush=True)
            print(f"  ----------------------------------------", flush=True)
            print(f"  TOTAL ESTIMATED:     {estimate['total_tokens']:,} tokens", flush=True)
            print("=" * 60, flush=True)
            print(flush=True)
            print("[Boris] Estimate-only mode. No execution performed.", flush=True)
            return

        if args.plan_only:
            print("[Boris] Plan-only mode. Exiting.", flush=True)
            return

        # Prompt user to skip milestones before execution
        prompt_skip_milestones(plan, logger)

        # Create initial state
        st = state_module.State(
            plan=plan,
            current_milestone_index=0,
            project_dir=project_dir,
            git_remote=args.remote,
            no_git=args.no_git,
            turbo=args.turbo,
        )
        state_module.save(st)
        start_index = 0

    # Track start time for summary
    start_time = datetime.now()

    # Git setup
    if not st.no_git:
        print("[Boris] Initializing git repo...", flush=True)
        git_manager.init_repo(project_dir)
        print("[Boris] Verifying git config (user.name/email)...", flush=True)
        if git_manager.ensure_git_config(project_dir):
            logger.info("Git config verified")
        else:
            logger.warning("Git config verification had issues - commits may fail")
            print("  [Boris] WARNING: Git config issues detected. Commits may fail.", flush=True)
        if args.remote:
            print(f"[Boris] Setting up remote: {args.remote}", flush=True)
            git_manager.setup_remote(project_dir, args.remote)

    # Determine turbo mode (from state on resume, from args on new run)
    turbo = getattr(st, "turbo", False) or getattr(args, "turbo", False)

    # Phase 1: Structural milestones (skip if resuming into UI phase)
    try:
        if st.phase != "ui_testing":
            if turbo:
                # === TURBO MODE: Parallel DaveLoop execution ===
                print(flush=True)
                print("[Boris] TURBO MODE: Parallel DaveLoop execution enabled", flush=True)
                logger.info("TURBO MODE enabled")

                batch_num = 0
                while True:
                    ready = get_ready_milestones(plan)
                    if not ready:
                        # Check if there are still pending milestones (deadlock detection)
                        pending = [m for m in plan.milestones if m.status == "pending"]
                        if pending:
                            pending_ids = [m.id for m in pending]
                            print(f"[Boris] TURBO: No milestones ready but {len(pending)} still pending: {pending_ids}", flush=True)
                            print("[Boris] TURBO: Possible dependency deadlock. Skipping remaining.", flush=True)
                            logger.warning("Turbo deadlock: pending=%s", pending_ids)
                            for m in pending:
                                m.status = "skipped"
                            state_module.save(st)
                        break

                    # Dependency-aware batch validation: filter milestones with skipped deps
                    ready = validate_turbo_batch(ready, plan, logger)
                    if not ready:
                        logger.warning("All ready milestones filtered out by turbo batch validation")
                        break

                    # Foundation-first: first batch only runs milestones with no dependencies
                    if batch_num == 0:
                        foundation = [m for m in ready if not m.depends_on]
                        if foundation:
                            ready = foundation[:1]  # Only one foundation milestone at a time

                    # Token budget check (swarm/turbo mode)
                    if token_estimator:
                        estimated = token_estimator.estimate_batch(len(ready), args.max_iter, args.swarm_budget if getattr(args, 'swarm', False) else 0)
                        if not token_estimator.check_budget(estimated):
                            print(f"[Boris] Token budget exceeded. Estimated: {token_estimator.estimated_tokens + estimated:,}, Budget: {token_estimator.max_tokens:,} tokens", flush=True)
                            logger.warning("Token budget exceeded, stopping execution")
                            break
                        token_estimator.record_tokens(estimated)

                    batch_num += 1
                    batch_ids = [m.id for m in ready]
                    print(flush=True)
                    print(f"[Boris] TURBO: Launching batch {batch_num} of {len(ready)} parallel DaveLoops: {batch_ids}", flush=True)
                    logger.info("TURBO batch %d: %s", batch_num, batch_ids)

                    # Mark all in batch as in_progress
                    for m in ready:
                        m.status = "in_progress"
                    state_module.save(st)

                    # Craft prompts for all milestones in batch
                    tasks = []
                    prompt_map = {}  # milestone.id -> prompt, for retries
                    for m in ready:
                        print(f"[Boris] TURBO: Crafting prompt for {m.id}...", flush=True)
                        # Compute sibling milestones (all others in this batch)
                        siblings = [sib for sib in ready if sib.id != m.id]
                        prompt = prompts.craft_prompt(m, plan, project_dir,
                                                      turbo=True, sibling_milestones=siblings)
                        print(f"[Boris] TURBO: Prompt ready for {m.id} ({len(prompt)} chars)", flush=True)
                        tasks.append((prompt, m))
                        prompt_map[m.id] = prompt

                    # Run all DaveLoops in parallel
                    isolation = getattr(args, 'isolation', 'none') if getattr(args, 'swarm', False) else 'none'
                    parallel_results = engine.run_parallel(tasks, project_dir, args.max_iter, isolation=isolation)

                    # Print swarm dashboard after parallel run completes (B7)
                    _print_swarm_dashboard(project_dir, batch_num, ready)
                    engine.clear_worker_statuses(project_dir)

                    # Process verdicts sequentially (corrections/retries run sequentially)
                    batch_summary = {}
                    for milestone, result in parallel_results:
                        print(f"[Boris] TURBO: Checking verdict for {milestone.id}...", flush=True)
                        verdict_result = engine.check(result, milestone)
                        logger.info(
                            "TURBO milestone %s verdict: %s - %s",
                            milestone.id,
                            verdict_result.verdict.value,
                            verdict_result.reason,
                        )

                        _process_milestone_verdict(
                            verdict_result, result, milestone, plan, st,
                            project_dir, args, logger, prompt_map[milestone.id]
                        )
                        batch_summary[milestone.id] = milestone.status.upper()

                    # Convergence phase: reconcile type conflicts from parallel workers
                    if not getattr(args, 'no_converge', False):
                        completed_in_batch = [m for m, r in parallel_results if m.status == "completed"]
                        if len(completed_in_batch) > 1:
                            print(f"[Boris] TURBO: Running convergence phase for batch {batch_num}...", flush=True)
                            _run_convergence(st, plan, project_dir, completed_in_batch, logger)

                    # Git commits sequentially after entire batch
                    if not st.no_git:
                        for milestone, result in parallel_results:
                            if milestone.status == "completed":
                                print(f"[Boris] TURBO: Committing {milestone.id} to git...", flush=True)
                                if git_manager.commit_milestone(project_dir, milestone):
                                    logger.info("Git commit succeeded for milestone %s", milestone.id)
                                else:
                                    logger.warning("Git commit FAILED for milestone %s", milestone.id)
                                    print(f"  [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)

                    # Save state after whole batch
                    state_module.save(st)

                    # Print batch summary
                    summary_parts = [f"{mid}={status}" for mid, status in batch_summary.items()]
                    print(f"[Boris] TURBO: Batch {batch_num} complete. Results: {', '.join(summary_parts)}", flush=True)
                    logger.info("TURBO batch %d complete: %s", batch_num, batch_summary)

                    # Incremental / --stop-at: pause after batch in turbo
                    should_pause = False
                    if args.stop_at:
                        # Pause only if the --stop-at milestone was in this batch
                        batch_completed_ids = {mid for mid, status in batch_summary.items()
                                               if status in ("COMPLETED", "SKIPPED")}
                        if args.stop_at in batch_completed_ids:
                            should_pause = True
                    elif args.incremental:
                        should_pause = True

                    if should_pause:
                        remaining = sum(1 for m in plan.milestones if m.status == "pending")
                        if remaining > 0:
                            print(flush=True)
                            if args.stop_at:
                                print(f"[Boris] --stop-at {args.stop_at}: pausing after batch {batch_num}.", flush=True)
                            else:
                                print(f"[Boris] Incremental mode: pausing after batch {batch_num}.", flush=True)
                            print(f"[Boris] {remaining} milestone(s) remaining.", flush=True)
                            resume_flags = f"-r --turbo -d \"{project_dir}\""
                            if args.stop_at:
                                resume_flags += f" --stop-at {args.stop_at}"
                            elif args.incremental:
                                resume_flags += " --incremental"
                            print(f"[Boris] Resume with: boris {resume_flags}", flush=True)
                            logger.info("Pausing after batch %d. %d remaining.", batch_num, remaining)
                            state_module.save(st)
                            sys.exit(0)
            else:
                # === SEQUENTIAL MODE (original behavior) ===
                for i in range(start_index, len(plan.milestones)):
                    milestone = plan.milestones[i]

                    if milestone.status == "completed":
                        logger.info("Skipping already completed milestone %s", milestone.id)
                        continue

                    if milestone.status == "skipped":
                        logger.info("Skipping previously skipped milestone %s", milestone.id)
                        continue

                    print(flush=True)
                    print(f"=== Milestone {milestone.id}: {milestone.title} ===", flush=True)
                    logger.info("Starting milestone %s: %s", milestone.id, milestone.title)

                    # Mark in progress
                    milestone.status = "in_progress"
                    st.current_milestone_index = i
                    state_module.save(st)

                    # Craft prompt and execute
                    print(f"[Boris] Crafting prompt for {milestone.id}...", flush=True)
                    prompt = prompts.craft_prompt(milestone, plan, project_dir)
                    print(f"[Boris] Prompt ready ({len(prompt)} chars). Spawning DaveLoop...", flush=True)
                    result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)

                    # Check verdict
                    print(f"[Boris] DaveLoop finished. Checking verdict for {milestone.id}...", flush=True)
                    verdict_result = engine.check(result, milestone)
                    logger.info(
                        "Milestone %s verdict: %s - %s",
                        milestone.id,
                        verdict_result.verdict.value,
                        verdict_result.reason,
                    )

                    if verdict_result.verdict == Verdict.RESOLVED:
                        milestone.status = "completed"
                        milestone.completed_at = datetime.now().isoformat()
                        state_module.save(st)

                        if not st.no_git:
                            print(f"[Boris] Committing milestone {milestone.id} to git...", flush=True)
                            if git_manager.commit_milestone(project_dir, milestone):
                                logger.info("Git commit succeeded for milestone %s", milestone.id)
                            else:
                                logger.warning("Git commit FAILED for milestone %s", milestone.id)
                                print(f"  [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)

                        print(f"[Boris] Milestone {milestone.id} COMPLETE", flush=True)
                        logger.info("Milestone %s completed", milestone.id)

                    elif verdict_result.verdict == Verdict.OFF_PLAN:
                        # Attempt correction
                        correction_count = 0
                        resolved = False

                        while correction_count < MAX_CORRECTIONS:
                            correction_count += 1
                            print(
                                f"  [Boris] Off-plan detected. Correction attempt {correction_count}/{MAX_CORRECTIONS}...",
                                flush=True,
                            )
                            logger.info(
                                "Off-plan correction attempt %d for milestone %s",
                                correction_count,
                                milestone.id,
                            )

                            correction_prompt = prompts.craft_correction(
                                result.output, milestone, plan, verdict_result.reason
                            )
                            result = engine.run(correction_prompt, project_dir, args.max_iter, milestone=milestone)
                            verdict_result = engine.check(result, milestone)

                            if verdict_result.verdict == Verdict.RESOLVED:
                                milestone.status = "completed"
                                milestone.completed_at = datetime.now().isoformat()
                                state_module.save(st)

                                if not st.no_git:
                                    print(f"[Boris] Committing corrected milestone {milestone.id} to git...", flush=True)
                                    if git_manager.commit_milestone(project_dir, milestone):
                                        logger.info("Git commit succeeded for milestone %s (after correction)", milestone.id)
                                    else:
                                        logger.warning("Git commit FAILED for milestone %s (after correction)", milestone.id)
                                        print(f"  [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)

                                print(f"[Boris] Milestone {milestone.id} COMPLETE (after correction)", flush=True)
                                logger.info("Milestone %s completed after correction", milestone.id)
                                resolved = True
                                break

                        if not resolved:
                            print(f"  [Boris] Milestone {milestone.id} could not be corrected. Skipping.", flush=True)
                            logger.warning("Milestone %s skipped after failed corrections", milestone.id)
                            milestone.status = "skipped"
                            state_module.save(st)

                    elif verdict_result.verdict == Verdict.FAILED:
                        # Retry logic
                        retry_count = 0
                        resolved = False

                        while retry_count < MAX_RETRIES:
                            retry_count += 1
                            milestone.retry_count = retry_count
                            print(
                                f"  [Boris] Failed. Retry {retry_count}/{MAX_RETRIES}...",
                                flush=True,
                            )
                            logger.info(
                                "Retry %d for milestone %s", retry_count, milestone.id
                            )

                            result = engine.run(prompt, project_dir, args.max_iter, milestone=milestone)
                            verdict_result = engine.check(result, milestone)

                            if verdict_result.verdict == Verdict.RESOLVED:
                                milestone.status = "completed"
                                milestone.completed_at = datetime.now().isoformat()
                                state_module.save(st)

                                if not st.no_git:
                                    print(f"[Boris] Committing retried milestone {milestone.id} to git...", flush=True)
                                    if git_manager.commit_milestone(project_dir, milestone):
                                        logger.info("Git commit succeeded for milestone %s (after retry)", milestone.id)
                                    else:
                                        logger.warning("Git commit FAILED for milestone %s (after retry)", milestone.id)
                                        print(f"  [Boris] WARNING: Git commit failed for {milestone.id}", flush=True)

                                print(f"[Boris] Milestone {milestone.id} COMPLETE (after retry)", flush=True)
                                logger.info("Milestone %s completed after retry", milestone.id)
                                resolved = True
                                break

                        if not resolved:
                            print(
                                f"  [Boris] Milestone {milestone.id} failed after {MAX_RETRIES} retries. Skipping.",
                                flush=True,
                            )
                            logger.warning("Milestone %s skipped after failed retries", milestone.id)
                            milestone.status = "skipped"
                            state_module.save(st)

                    # Save state after each milestone outcome
                    state_module.save(st)

                    # Incremental mode: pause after each milestone
                    # --stop-at: only pause when the target milestone completes
                    should_pause = False
                    if args.stop_at and milestone.status in ("completed", "skipped"):
                        if milestone.id == args.stop_at:
                            should_pause = True
                    elif args.incremental and milestone.status in ("completed", "skipped"):
                        should_pause = True

                    if should_pause:
                        remaining = sum(1 for m in plan.milestones if m.status == "pending")
                        if remaining > 0:
                            print(flush=True)
                            if args.stop_at:
                                print(f"[Boris] --stop-at {args.stop_at}: pausing after {milestone.id}.", flush=True)
                            else:
                                print(f"[Boris] Incremental mode: pausing after {milestone.id}.", flush=True)
                            print(f"[Boris] {remaining} milestone(s) remaining.", flush=True)
                            resume_flags = f"-r -d \"{project_dir}\""
                            if args.stop_at:
                                resume_flags += f" --stop-at {args.stop_at}"
                            elif args.incremental:
                                resume_flags += " --incremental"
                            print(f"[Boris] Resume with: boris {resume_flags}", flush=True)
                            logger.info("Pausing after %s. %d remaining.", milestone.id, remaining)
                            state_module.save(st)
                            sys.exit(0)

        # Phase 2: UI Testing & Polish
        _run_ui_phase(st, plan, project_dir, args, logger)

    except KeyboardInterrupt:
        print(flush=True)
        print(f"[Boris] Interrupted. State saved. Resume with: boris -r -d \"{project_dir}\"", flush=True)
        logger.info("Interrupted by user. State saved.")
        state_module.save(st)
        sys.exit(130)

    # Final push
    if not st.no_git:
        print("[Boris] Pushing final changes to remote...", flush=True)
        git_manager.final_push(project_dir)

    # Summary and proper exit
    print_final_summary(plan, project_dir, start_time)

    completed = sum(1 for m in plan.milestones if m.status == "completed")
    total = len(plan.milestones)
    if completed == total:
        sys.exit(0)
    else:
        sys.exit(1)


if __name__ == "__main__":
    main()
