The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\smart_orchestrator.py has been updated successfully.

from parallel_agent import ParallelAgentRunner, AgentResult
from server.services.feature_graph import FeatureGraph, ParallelFeature
from server.services.smart_scheduler import SmartScheduler
from progress import get_feature_stats

"""
Smart Orchestrator
==================

Uses Claude to dynamically decide how many agents to run in parallel
based on feature analysis, complexity estimation, and conflict prediction.
"""

import asyncio
import json
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

from claude_agent_sdk import ClaudeSDKClient

from parallel_agent import ParallelAgentRunner, AgentResult
from server.services.feature_graph import FeatureGraph, ParallelFeature
from progress import get_feature_stats

logger = logging.getLogger(__name__)

@dataclass
class FeatureCluster:
    """A group of features that can safely run together."""
    features: list[ParallelFeature]
    estimated_parallelism: int
    reasoning: str
    predicted_conflicts: list[str] = field(default_factory=list)
    total_complexity: int = 0

@dataclass
class OrchestrationDecision:
    """Decision about how to run features."""
    recommended_parallelism: int
    features_to_run: list[ParallelFeature]
    reasoning: str
    risk_level: str  # "low", "medium", "high"
    estimated_duration_minutes: int = 0

ANALYSIS_PROMPT = """You are an expert software architect analyzing features for parallel execution.

Given these pending features, analyze them and decide:
1. How many can safely run in parallel
2. Which specific features to run together
3. Risk assessment for parallel execution

Consider:
- Features touching similar files/directories should NOT run together
- UI features can often parallelize with backend features
- Database/schema changes should run alone
- Test features can parallelize with implementation features
- Simple features (1-2 steps) are safer to parallelize than complex ones

## Pending Features

{features_json}

## Current State
- Features in progress: {in_progress_count}
- Features completed: {completed_count}
- Recent failures: {recent_failures}

## Response Format

Respond with a JSON object:
```json
{{
  "recommended_parallelism": <number 1-5>,
  "feature_ids_to_run": [<list of feature IDs to run now>],
  "reasoning": "<brief explanation of your decision>",
  "risk_level": "<low|medium|high>",
  "estimated_minutes": <rough time estimate>,
  "clusters": [
    {{
      "feature_ids": [<ids that can run together>],
      "reason": "<why these are safe together>"
    }}
  ],
  "warnings": ["<any concerns about these features>"]
}}
```

Be conservative - it's better to run fewer in parallel than risk conflicts.
"""

class SmartOrchestrator:
    """
    Intelligent orchestrator that uses Claude to make parallelization decisions.

    Instead of a fixed max_parallel, this orchestrator:
    1. Analyzes pending features using Claude
    2. Clusters features by predicted file conflicts
    3. Estimates complexity and risk
    4. Dynamically adjusts parallelism based on conditions
    """

    def __init__(
        self,
        project_dir: Path,
        model: str = "claude-sonnet-4-20250514",
        min_parallel: int = 1,
        max_parallel: int = 5,
        yolo_mode: bool = False,
    ):
        """
        Initialize the smart orchestrator.

        Args:
            project_dir: Path to the project directory
            model: Claude model for both analysis and coding
            min_parallel: Minimum parallel agents
            max_parallel: Maximum parallel agents (cap)
            yolo_mode: Skip testing if True
        """
        self.project_dir = project_dir
        self.model = model
        self.min_parallel = min_parallel
        self.max_parallel = max_parallel
        self.yolo_mode = yolo_mode

        self.feature_graph = FeatureGraph(project_dir)
        self._recent_failures: list[str] = []
        self._analysis_cache: dict[str, OrchestrationDecision] = {}

    async def analyze_features(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Use Claude to analyze features and decide parallelization.

        Args:
            features: List of pending features

        Returns:
            OrchestrationDecision with recommendations
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No pending features",
                risk_level="low",
            )

        # Get current stats
        stats = get_feature_stats(self.project_dir) or {}
        in_progress = stats.get("in_progress", 0)
        completed = stats.get("passing", 0)

        # Prepare features for analysis
        features_data = []
        for f in features[:20]:  # Limit to 20 for context
            features_data.append({
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "priority": f.priority,
                "description": f.description[:200],  # Truncate
                "steps_count": len(f.steps),
                "complexity": f.estimated_complexity,
            })

        prompt = ANALYSIS_PROMPT.format(
            features_json=json.dumps(features_data, indent=2),
            in_progress_count=in_progress,
            completed_count=completed,
            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
        )

        # Quick analysis using Claude
        try:
            # Use a lightweight client for analysis
            from anthropic import Anthropic
            client = Anthropic()

            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )

            # Parse response
            response_text = response.content[0].text

            # Extract JSON from response
            json_start = response_text.find("{")
            json_end = response_text.rfind("}") + 1
            if json_start >= 0 and json_end > json_start:
                decision_data = json.loads(response_text[json_start:json_end])
            else:
                raise ValueError("No JSON found in response")

            # Map feature IDs to features
            feature_map = {f.id: f for f in features}
            features_to_run = [
                feature_map[fid]
                for fid in decision_data.get("feature_ids_to_run", [])
                if fid in feature_map
            ]

            # Apply caps
            parallelism = min(
                max(decision_data.get("recommended_parallelism", 1), self.min_parallel),
                self.max_parallel,
            )

            return OrchestrationDecision(
                recommended_parallelism=parallelism,
                features_to_run=features_to_run[:parallelism],
                reasoning=decision_data.get("reasoning", "Analysis complete"),
                risk_level=decision_data.get("risk_level", "medium"),
                estimated_duration_minutes=decision_data.get("estimated_minutes", 30),
            )

        except Exception as e:
            logger.warning(f"Analysis failed, using fallback: {e}")
            return self._fallback_decision(features)

    def _fallback_decision(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Fallback decision when Claude analysis fails.

        Uses simple heuristics based on feature categories.
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No features available",
                risk_level="low",
            )

        # Group by category
        categories = {}
        for f in features:
            cat = f.category.lower()
            if cat not in categories:
                categories[cat] = []
            categories[cat].append(f)

        # Pick one from each category (up to max)
        selected = []
        for cat, cat_features in categories.items():
            if len(selected) < self.max_parallel:
                selected.append(cat_features[0])

        # If we have room and features left, add more
        remaining = [f for f in features if f not in selected]
        while len(selected) < self.min_parallel and remaining:
            selected.append(remaining.pop(0))

        return OrchestrationDecision(
            recommended_parallelism=len(selected),
            features_to_run=selected,
            reasoning="Fallback: One feature per category",
            risk_level="medium",
        )

    async def run_smart(
        self,
        max_iterations: Optional[int] = None,
    ) -> dict:
        """
        Run the smart orchestration loop.

        Args:
            max_iterations: Maximum iteration cycles

        Returns:
            Summary statistics
        """
        print("\n" + "=" * 70)
        print("  SMART ORCHESTRATOR")
        print("=" * 70)
        print(f"\nProject: {self.project_dir}")
        print(f"Model: {self.model}")
        print(f"Parallelism range: {self.min_parallel}-{self.max_parallel}")
        print()

        total_completed = 0
        total_failed = 0
        iteration = 0

        while True:
            iteration += 1

            if max_iterations and iteration > max_iterations:
                print(f"\nReached max iterations ({max_iterations})")
                break

            # Get available features
            features = self.feature_graph.get_parallelizable_features(
                count=20,  # Get more for analysis
            )

            if not features:
                stats = get_feature_stats(self.project_dir)
                if stats and stats.get("pending", 0) == 0:
                    print("\n All features completed!")
                    break
                print("\nNo parallelizable features, waiting...")
                await asyncio.sleep(5)
                continue

            # Analyze and decide
            print(f"\n--- Iteration {iteration} ---")
            print(f"Analyzing {len(features)} pending features...")

            decision = await self.analyze_features(features)

            print(f"\nDecision: Run {decision.recommended_parallelism} agents in parallel")
            print(f"Risk level: {decision.risk_level}")
            print(f"Reasoning: {decision.reasoning}")
            print(f"\nFeatures selected:")
            for f in decision.features_to_run:
                print(f"  - #{f.id}: {f.name} [{f.category}]")

            if not decision.features_to_run:
                print("\nNo features selected, waiting...")
                await asyncio.sleep(5)
                continue

            # Create runner with dynamic parallelism
            runner = ParallelAgentRunner(
                project_dir=self.project_dir,
                model=self.model,
                max_parallel=decision.recommended_parallelism,
                yolo_mode=self.yolo_mode,
            )

            # Run the selected features
            tasks = []
            for i, feature in enumerate(decision.features_to_run):
                agent_id = f"smart-{iteration}-{i}"
                task = asyncio.create_task(
                    runner.run_single_feature(feature, agent_id)
                )
                tasks.append((feature, task))

            # Wait for completion
            for feature, task in tasks:
                try:
                    result = await task
                    if result.success:
                        total_completed += 1
                        print(f"  Completed: #{feature.id} {feature.name}")
                    else:
                        total_failed += 1
                        self._recent_failures.append(f"#{feature.id}")
                        print(f"  Failed: #{feature.id} - {result.error}")
                except Exception as e:
                    total_failed += 1
                    self._recent_failures.append(f"#{feature.id}")
                    print(f"  Error: #{feature.id} - {e}")

            # Brief delay
            await asyncio.sleep(3)

        return {
            "total_completed": total_completed,
            "total_failed": total_failed,
            "iterations": iteration,
        }

async def run_smart_orchestrator(
    project_dir: Path,
    model: str = "claude-sonnet-4-20250514",
    max_iterations: Optional[int] = None,
    yolo_mode: bool = False,
) -> dict:
    """
    Convenience function to run smart orchestrator.

    Args:
        project_dir: Project directory
        model: Claude model
        max_iterations: Max iterations
        yolo_mode: Skip testing

    Returns:
        Summary statistics
    """
    orchestrator = SmartOrchestrator(
        project_dir=project_dir,
        model=model,
        yolo_mode=yolo_mode,
    )

    return await orchestrator.run_smart(max_iterations=max_iterations)

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Smart orchestrator for parallel coding")
    parser.add_argument(
        "--project-dir",
        type=str,
        required=True,
        help="Project directory",
    )
    parser.add_argument(
        "--model",
        type=str,
        default="claude-sonnet-4-20250514",
        help="Claude model",
    )
    parser.add_argument(
        "--max-iterations",
        type=int,
        default=None,
        help="Maximum iterations",
    )
    parser.add_argument(
        "--yolo",
        action="store_true",
        help="YOLO mode (skip testing)",
    )

    args = parser.parse_args()

    project_path = Path(args.project_dir)
    if not project_path.is_absolute():
        try:
            from registry import get_project_path
            resolved = get_project_path(args.project_dir)
            if resolved:
                project_path = resolved
        except ImportError:
            pass

    result = asyncio.run(run_smart_orchestrator(
        project_dir=project_path,
        model=args.model,
        max_iterations=args.max_iterations,
        yolo_mode=args.yolo,
    ))

    print("\n" + "=" * 70)
    print("  ORCHESTRATION COMPLETE")
    print("=" * 70)
    print(f"\nCompleted: {result['total_completed']}")
    print(f"Failed: {result['total_failed']}")
    print(f"Iterations: {result['iterations']}")

+from server.services.smart_scheduler import SmartScheduler


Now let me update the SmartOrchestrator class to use the SmartScheduler for initial decisions and only consult Claude when the scheduler is uncertain (low confidence).


   100→    3. Estimates complexity and risk
   101→    4. Dynamically adjusts parallelism based on conditions
   102→    """
   103→
   104→    def __init__(
   105→        self,
   106→        project_dir: Path,
   107→        model: str = "claude-sonnet-4-20250514",
   108→        min_parallel: int = 1,
   109→        max_parallel: int = 5,
   110→        yolo_mode: bool = False,
   111→    ):
   112→        """
   113→        Initialize the smart orchestrator.
   114→
   115→        Args:
   116→            project_dir: Path to the project directory
   117→            model: Claude model for both analysis and coding
   118→            min_parallel: Minimum parallel agents
   119→            max_parallel: Maximum parallel agents (cap)
   120→            yolo_mode: Skip testing if True
   121→        """
   122→        self.project_dir = project_dir
   123→        self.model = model
   124→        self.min_parallel = min_parallel
   125→        self.max_parallel = max_parallel
   126→        self.yolo_mode = yolo_mode
   127→
   128→        self.feature_graph = FeatureGraph(project_dir)
   129→        self._recent_failures: list[str] = []
   130→        self._analysis_cache: dict[str, OrchestrationDecision] = {}
   131→
   132→    async def analyze_features(
   133→        self,
   134→        features: list[ParallelFeature],
   135→    ) -> OrchestrationDecision:
   136→        """
   137→        Use Claude to analyze features and decide parallelization.
   138→
   139→        Args:
   140→            features: List of pending features
   141→
   142→        Returns:
   143→            OrchestrationDecision with recommendations
   144→        """
   145→        if not features:
   146→            return OrchestrationDecision(
   147→                recommended_parallelism=0,
   148→                features_to_run=[],
   149→                reasoning="No pending features",
   150→                risk_level="low",
   151→            )
   152→
   153→        # Get current stats
   154→        stats = get_feature_stats(self.project_dir) or {}
   155→        in_progress = stats.get("in_progress", 0)
   156→        completed = stats.get("passing", 0)
   157→
   158→        # Prepare features for analysis
   159→        features_data = []
   160→        for f in features[:20]:  # Limit to 20 for context
   161→            features_data.append({
   162→                "id": f.id,
   163→                "name": f.name,
   164→                "category": f.category,
   165→                "priority": f.priority,
   166→                "description": f.description[:200],  # Truncate
   167→                "steps_count": len(f.steps),
   168→                "complexity": f.estimated_complexity,
   169→            })
   170→
   171→        prompt = ANALYSIS_PROMPT.format(
   172→            features_json=json.dumps(features_data, indent=2),
   173→            in_progress_count=in_progress,
   174→            completed_count=completed,
   175→            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
   176→        )
   177→
   178→        # Quick analysis using Claude
   179→        try:
   180→            # Use a lightweight client for analysis
   181→            from anthropic import Anthropic
   182→            client = Anthropic()
   183→
   184→            response = client.messages.create(
   185→                model="claude-sonnet-4-20250514",
   186→                max_tokens=1024,
   187→                messages=[{"role": "user", "content": prompt}],
   188→            )
   189→
   190→            # Parse response
   191→            response_text = response.content[0].text
   192→
   193→            # Extract JSON from response
   194→            json_start = response_text.find("{")
   195→            json_end = response_text.rfind("}") + 1
   196→            if json_start >= 0 and json_end > json_start:
   197→                decision_data = json.loads(response_text[json_start:json_end])
   198→            else:
   199→                raise ValueError("No JSON found in response")

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

    3. Estimates complexity and risk
    4. Dynamically adjusts parallelism based on conditions
    """

    def __init__(
        self,
        project_dir: Path,
        model: str = "claude-sonnet-4-20250514",
        min_parallel: int = 1,
        max_parallel: int = 5,
        yolo_mode: bool = False,
    ):
        """
        Initialize the smart orchestrator.

        Args:
            project_dir: Path to the project directory
            model: Claude model for both analysis and coding
            min_parallel: Minimum parallel agents
            max_parallel: Maximum parallel agents (cap)
            yolo_mode: Skip testing if True
        """
        self.project_dir = project_dir
        self.model = model
        self.min_parallel = min_parallel
        self.max_parallel = max_parallel
        self.yolo_mode = yolo_mode

        self.feature_graph = FeatureGraph(project_dir)
        self._recent_failures: list[str] = []
        self._analysis_cache: dict[str, OrchestrationDecision] = {}

    async def analyze_features(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Use Claude to analyze features and decide parallelization.

        Args:
            features: List of pending features

        Returns:
            OrchestrationDecision with recommendations
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No pending features",
                risk_level="low",
            )

        # Get current stats
        stats = get_feature_stats(self.project_dir) or {}
        in_progress = stats.get("in_progress", 0)
        completed = stats.get("passing", 0)

        # Prepare features for analysis
        features_data = []
        for f in features[:20]:  # Limit to 20 for context
            features_data.append({
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "priority": f.priority,
                "description": f.description[:200],  # Truncate
                "steps_count": len(f.steps),
                "complexity": f.estimated_complexity,
            })

        prompt = ANALYSIS_PROMPT.format(
            features_json=json.dumps(features_data, indent=2),
            in_progress_count=in_progress,
            completed_count=completed,
            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
        )

        # Quick analysis using Claude
        try:
            # Use a lightweight client for analysis
            from anthropic import Anthropic
            client = Anthropic()

            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )

            # Parse response
            response_text = response.content[0].text

            # Extract JSON from response
            json_start = response_text.find("{")
            json_end = response_text.rfind("}") + 1
            if json_start >= 0 and json_end > json_start:
                decision_data = json.loads(response_text[json_start:json_end])
            else:
                raise ValueError("No JSON found in response")


Let me update the SmartOrchestrator's __init__ and analyze_features methods to use the SmartScheduler first, and only call Claude when the scheduler has low confidence.


Edit

self.feature_graph = FeatureGraph(project_dir)
        self._recent_failures: list[str] = []
        self._analysis_cache: dict[str, OrchestrationDecision] = {}

    async def analyze_features(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Use Claude to analyze features and decide parallelization.

        Args:
            features: List of pending features

        Returns:
            OrchestrationDecision with recommendations
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No pending features",
                risk_level="low",
            )

        # Get current stats
        stats = get_feature_stats(self.project_dir) or {}
        in_progress = stats.get("in_progress", 0)
        completed = stats.get("passing", 0)

        # Prepare features for analysis
        features_data = []
        for f in features[:20]:  # Limit to 20 for context
            features_data.append({
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "priority": f.priority,
                "description": f.description[:200],  # Truncate
                "steps_count": len(f.steps),
                "complexity": f.estimated_complexity,
            })

        prompt = ANALYSIS_PROMPT.format(
            features_json=json.dumps(features_data, indent=2),
            in_progress_count=in_progress,
            completed_count=completed,
            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
        )

        # Quick analysis using Claude
        try:
            # Use a lightweight client for analysis
            from anthropic import Anthropic
            client = Anthropic()

            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )

            # Parse response
            response_text = response.content[0].text

            # Extract JSON from response
            json_start = response_text.find("{")
            json_end = response_text.rfind("}") + 1
            if json_start >= 0 and json_end > json_start:
                decision_data = json.loads(response_text[json_start:json_end])
            else:
                raise ValueError("No JSON found in response")

self.feature_graph = FeatureGraph(project_dir)
        self.scheduler = SmartScheduler(project_dir, min_parallel, max_parallel)
        self._recent_failures: list[str] = []
        self._analysis_cache: dict[str, OrchestrationDecision] = {}
        self._claude_threshold = 0.6  # Use Claude if scheduler confidence below this

    async def analyze_features(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Analyze features and decide parallelization.

        Uses fast heuristic-based SmartScheduler first. Only consults
        Claude if the scheduler has low confidence in its decision.

        Args:
            features: List of pending features

        Returns:
            OrchestrationDecision with recommendations
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No pending features",
                risk_level="low",
            )

        # Get current stats
        stats = get_feature_stats(self.project_dir) or {}
        in_progress = stats.get("in_progress", 0)

        # First, try the fast heuristic-based scheduler
        features_data = [
            {
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "description": f.description,
                "steps": f.steps,
            }
            for f in features
        ]

        scheduler_decision = self.scheduler.make_decision(features_data, in_progress)

        # If scheduler is confident, use its decision
        if scheduler_decision.confidence >= self._claude_threshold:
            feature_map = {f.id: f for f in features}
            selected_features = [
                feature_map[fid]
                for fid in scheduler_decision.feature_ids
                if fid in feature_map
            ]

            logger.info(
                f"Scheduler decision (confidence={scheduler_decision.confidence:.2f}): "
                f"{scheduler_decision.parallelism} agents"
            )

            return OrchestrationDecision(
                recommended_parallelism=scheduler_decision.parallelism,
                features_to_run=selected_features,
                reasoning=f"[Heuristic] {scheduler_decision.reasoning}",
                risk_level=scheduler_decision.risk_level,
                estimated_duration_minutes=scheduler_decision.estimated_duration,
            )

        # Scheduler is uncertain - consult Claude for a better decision
        logger.info(
            f"Scheduler uncertain (confidence={scheduler_decision.confidence:.2f}), "
            "consulting Claude..."
        )

        # Prepare features for Claude analysis
        features_analysis = []
        for f in features[:20]:  # Limit to 20 for context
            features_analysis.append({
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "priority": f.priority,
                "description": f.description[:200],  # Truncate
                "steps_count": len(f.steps),
                "complexity": f.estimated_complexity,
            })

        completed = stats.get("passing", 0)
        prompt = ANALYSIS_PROMPT.format(
            features_json=json.dumps(features_analysis, indent=2),
            in_progress_count=in_progress,
            completed_count=completed,
            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
        )

        # Call Claude for analysis
        try:
            from anthropic import Anthropic
            client = Anthropic()

            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )

            response_text = response.content[0].text

            # Extract JSON from response
            json_start = response_text.find("{")
            json_end = response_text.rfind("}") + 1
            if json_start >= 0 and json_end > json_start:
                decision_data = json.loads(response_text[json_start:json_end])
            else:
                raise ValueError("No JSON found in response")


self.feature_graph = FeatureGraph(project_dir)
        self.scheduler = SmartScheduler(project_dir, min_parallel, max_parallel)
        self._recent_failures: list[str] = []
        self._analysis_cache: dict[str, OrchestrationDecision] = {}
        self._claude_threshold = 0.6  # Use Claude if scheduler confidence below this

    async def analyze_features(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Analyze features and decide parallelization.

        Uses fast heuristic-based SmartScheduler first. Only consults
        Claude if the scheduler has low confidence in its decision.

        Args:
            features: List of pending features

        Returns:
            OrchestrationDecision with recommendations
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No pending features",
                risk_level="low",
            )

        # Get current stats
        stats = get_feature_stats(self.project_dir) or {}
        in_progress = stats.get("in_progress", 0)

        # First, try the fast heuristic-based scheduler
        features_data = [
            {
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "description": f.description,
                "steps": f.steps,
            }
            for f in features
        ]

        scheduler_decision = self.scheduler.make_decision(features_data, in_progress)

        # If scheduler is confident, use its decision
        if scheduler_decision.confidence >= self._claude_threshold:
            feature_map = {f.id: f for f in features}
            selected_features = [
                feature_map[fid]
                for fid in scheduler_decision.feature_ids
                if fid in feature_map
            ]

            logger.info(
                f"Scheduler decision (confidence={scheduler_decision.confidence:.2f}): "
                f"{scheduler_decision.parallelism} agents"
            )

            return OrchestrationDecision(
                recommended_parallelism=scheduler_decision.parallelism,
                features_to_run=selected_features,
                reasoning=f"[Heuristic] {scheduler_decision.reasoning}",
                risk_level=scheduler_decision.risk_level,
                estimated_duration_minutes=scheduler_decision.estimated_duration,
            )

        # Scheduler is uncertain - consult Claude for a better decision
        logger.info(
            f"Scheduler uncertain (confidence={scheduler_decision.confidence:.2f}), "
            "consulting Claude..."
        )

        # Prepare features for Claude analysis
        features_analysis = []
        for f in features[:20]:  # Limit to 20 for context
            features_analysis.append({
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "priority": f.priority,
                "description": f.description[:200],  # Truncate
                "steps_count": len(f.steps),
                "complexity": f.estimated_complexity,
            })

        completed = stats.get("passing", 0)
        prompt = ANALYSIS_PROMPT.format(
            features_json=json.dumps(features_analysis, indent=2),
            in_progress_count=in_progress,
            completed_count=completed,
            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
        )

        # Call Claude for analysis
        try:
            from anthropic import Anthropic
            client = Anthropic()

            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )

            response_text = response.content[0].text

            # Extract JSON from response
            json_start = response_text.find("{")
            json_end = response_text.rfind("}") + 1
            if json_start >= 0 and json_end > json_start:
                decision_data = json.loads(response_text[json_start:json_end])
            else:
                raise ValueError("No JSON found in response")

"""
Smart Orchestrator
==================

Uses Claude to dynamically decide how many agents to run in parallel
based on feature analysis, complexity estimation, and conflict prediction.
"""

import asyncio
import json
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

from claude_agent_sdk import ClaudeSDKClient

from parallel_agent import ParallelAgentRunner, AgentResult
from server.services.feature_graph import FeatureGraph, ParallelFeature
from server.services.smart_scheduler import SmartScheduler
from progress import get_feature_stats

logger = logging.getLogger(__name__)

@dataclass
class FeatureCluster:
    """A group of features that can safely run together."""
    features: list[ParallelFeature]
    estimated_parallelism: int
    reasoning: str
    predicted_conflicts: list[str] = field(default_factory=list)
    total_complexity: int = 0

@dataclass
class OrchestrationDecision:
    """Decision about how to run features."""
    recommended_parallelism: int
    features_to_run: list[ParallelFeature]
    reasoning: str
    risk_level: str  # "low", "medium", "high"
    estimated_duration_minutes: int = 0

ANALYSIS_PROMPT = """You are an expert software architect analyzing features for parallel execution.

Given these pending features, analyze them and decide:
1. How many can safely run in parallel
2. Which specific features to run together
3. Risk assessment for parallel execution

Consider:
- Features touching similar files/directories should NOT run together
- UI features can often parallelize with backend features
- Database/schema changes should run alone
- Test features can parallelize with implementation features
- Simple features (1-2 steps) are safer to parallelize than complex ones

## Pending Features

{features_json}

## Current State
- Features in progress: {in_progress_count}
- Features completed: {completed_count}
- Recent failures: {recent_failures}

## Response Format

Respond with a JSON object:
```json
{{
  "recommended_parallelism": <number 1-5>,
  "feature_ids_to_run": [<list of feature IDs to run now>],
  "reasoning": "<brief explanation of your decision>",
  "risk_level": "<low|medium|high>",
  "estimated_minutes": <rough time estimate>,
  "clusters": [
    {{
      "feature_ids": [<ids that can run together>],
      "reason": "<why these are safe together>"
    }}
  ],
  "warnings": ["<any concerns about these features>"]
}}
```

Be conservative - it's better to run fewer in parallel than risk conflicts.
"""

class SmartOrchestrator:
    """
    Intelligent orchestrator that uses Claude to make parallelization decisions.

    Instead of a fixed max_parallel, this orchestrator:
    1. Analyzes pending features using Claude
    2. Clusters features by predicted file conflicts
    3. Estimates complexity and risk
    4. Dynamically adjusts parallelism based on conditions
    """

    def __init__(
        self,
        project_dir: Path,
        model: str = "claude-sonnet-4-20250514",
        min_parallel: int = 1,
        max_parallel: int = 5,
        yolo_mode: bool = False,
    ):
        """
        Initialize the smart orchestrator.

        Args:
            project_dir: Path to the project directory
            model: Claude model for both analysis and coding
            min_parallel: Minimum parallel agents
            max_parallel: Maximum parallel agents (cap)
            yolo_mode: Skip testing if True
        """
        self.project_dir = project_dir
        self.model = model
        self.min_parallel = min_parallel
        self.max_parallel = max_parallel
        self.yolo_mode = yolo_mode

        self.feature_graph = FeatureGraph(project_dir)
        self._recent_failures: list[str] = []
        self._analysis_cache: dict[str, OrchestrationDecision] = {}

    async def analyze_features(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Use Claude to analyze features and decide parallelization.

        Args:
            features: List of pending features

        Returns:
            OrchestrationDecision with recommendations
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No pending features",
                risk_level="low",
            )

        # Get current stats
        stats = get_feature_stats(self.project_dir) or {}
        in_progress = stats.get("in_progress", 0)
        completed = stats.get("passing", 0)

        # Prepare features for analysis
        features_data = []
        for f in features[:20]:  # Limit to 20 for context
            features_data.append({
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "priority": f.priority,
                "description": f.description[:200],  # Truncate
                "steps_count": len(f.steps),
                "complexity": f.estimated_complexity,
            })

        prompt = ANALYSIS_PROMPT.format(
            features_json=json.dumps(features_data, indent=2),
            in_progress_count=in_progress,
            completed_count=completed,
            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
        )

        # Quick analysis using Claude
        try:
            # Use a lightweight client for analysis
            from anthropic import Anthropic
            client = Anthropic()

            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )

            # Parse response
            response_text = response.content[0].text

            # Extract JSON from response
            json_start = response_text.find("{")
            json_end = response_text.rfind("}") + 1
            if json_start >= 0 and json_end > json_start:
                decision_data = json.loads(response_text[json_start:json_end])
            else:
                raise ValueError("No JSON found in response")

            # Map feature IDs to features
            feature_map = {f.id: f for f in features}
            features_to_run = [
                feature_map[fid]
                for fid in decision_data.get("feature_ids_to_run", [])
                if fid in feature_map
            ]

            # Apply caps
            parallelism = min(
                max(decision_data.get("recommended_parallelism", 1), self.min_parallel),
                self.max_parallel,
            )

            return OrchestrationDecision(
                recommended_parallelism=parallelism,
                features_to_run=features_to_run[:parallelism],
                reasoning=decision_data.get("reasoning", "Analysis complete"),
                risk_level=decision_data.get("risk_level", "medium"),
                estimated_duration_minutes=decision_data.get("estimated_minutes", 30),
            )

        except Exception as e:
            logger.warning(f"Analysis failed, using fallback: {e}")
            return self._fallback_decision(features)

    def _fallback_decision(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Fallback decision when Claude analysis fails.

        Uses simple heuristics based on feature categories.
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No features available",
                risk_level="low",
            )

        # Group by category
        categories = {}
        for f in features:
            cat = f.category.lower()
            if cat not in categories:
                categories[cat] = []
            categories[cat].append(f)

        # Pick one from each category (up to max)
        selected = []
        for cat, cat_features in categories.items():
            if len(selected) < self.max_parallel:
                selected.append(cat_features[0])

        # If we have room and features left, add more
        remaining = [f for f in features if f not in selected]
        while len(selected) < self.min_parallel and remaining:
            selected.append(remaining.pop(0))

        return OrchestrationDecision(
            recommended_parallelism=len(selected),
            features_to_run=selected,
            reasoning="Fallback: One feature per category",
            risk_level="medium",
        )

    async def run_smart(
        self,
        max_iterations: Optional[int] = None,
    ) -> dict:
        """
        Run the smart orchestration loop.

        Args:
            max_iterations: Maximum iteration cycles

        Returns:
            Summary statistics
        """
        print("\n" + "=" * 70)
        print("  SMART ORCHESTRATOR")
        print("=" * 70)
        print(f"\nProject: {self.project_dir}")
        print(f"Model: {self.model}")
        print(f"Parallelism range: {self.min_parallel}-{self.max_parallel}")
        print()

        total_completed = 0
        total_failed = 0
        iteration = 0

        while True:
            iteration += 1

            if max_iterations and iteration > max_iterations:
                print(f"\nReached max iterations ({max_iterations})")
                break

            # Get available features
            features = self.feature_graph.get_parallelizable_features(
                count=20,  # Get more for analysis
            )

            if not features:
                stats = get_feature_stats(self.project_dir)
                if stats and stats.get("pending", 0) == 0:
                    print("\n All features completed!")
                    break
                print("\nNo parallelizable features, waiting...")
                await asyncio.sleep(5)
                continue

            # Analyze and decide
            print(f"\n--- Iteration {iteration} ---")
            print(f"Analyzing {len(features)} pending features...")

            decision = await self.analyze_features(features)

            print(f"\nDecision: Run {decision.recommended_parallelism} agents in parallel")
            print(f"Risk level: {decision.risk_level}")
            print(f"Reasoning: {decision.reasoning}")
            print(f"\nFeatures selected:")
            for f in decision.features_to_run:
                print(f"  - #{f.id}: {f.name} [{f.category}]")

            if not decision.features_to_run:
                print("\nNo features selected, waiting...")
                await asyncio.sleep(5)
                continue

            # Create runner with dynamic parallelism
            runner = ParallelAgentRunner(
                project_dir=self.project_dir,
                model=self.model,
                max_parallel=decision.recommended_parallelism,
                yolo_mode=self.yolo_mode,
            )

            # Run the selected features
            tasks = []
            for i, feature in enumerate(decision.features_to_run):
                agent_id = f"smart-{iteration}-{i}"
                task = asyncio.create_task(
                    runner.run_single_feature(feature, agent_id)
                )
                tasks.append((feature, task))

            # Wait for completion
            for feature, task in tasks:
                try:
                    result = await task
                    if result.success:
                        total_completed += 1
                        print(f"  Completed: #{feature.id} {feature.name}")
                    else:
                        total_failed += 1
                        self._recent_failures.append(f"#{feature.id}")
                        print(f"  Failed: #{feature.id} - {result.error}")
                except Exception as e:
                    total_failed += 1
                    self._recent_failures.append(f"#{feature.id}")
                    print(f"  Error: #{feature.id} - {e}")

            # Brief delay
            await asyncio.sleep(3)

        return {
            "total_completed": total_completed,
            "total_failed": total_failed,
            "iterations": iteration,
        }

async def run_smart_orchestrator(
    project_dir: Path,
    model: str = "claude-sonnet-4-20250514",
    max_iterations: Optional[int] = None,
    yolo_mode: bool = False,
) -> dict:
    """
    Convenience function to run smart orchestrator.

    Args:
        project_dir: Project directory
        model: Claude model
        max_iterations: Max iterations
        yolo_mode: Skip testing

    Returns:
        Summary statistics
    """
    orchestrator = SmartOrchestrator(
        project_dir=project_dir,
        model=model,
        yolo_mode=yolo_mode,
    )

    return await orchestrator.run_smart(max_iterations=max_iterations)

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Smart orchestrator for parallel coding")
    parser.add_argument(
        "--project-dir",
        type=str,
        required=True,
        help="Project directory",
    )
    parser.add_argument(
        "--model",
        type=str,
        default="claude-sonnet-4-20250514",
        help="Claude model",
    )
    parser.add_argument(
        "--max-iterations",
        type=int,
        default=None,
        help="Maximum iterations",
    )
    parser.add_argument(
        "--yolo",
        action="store_true",
        help="YOLO mode (skip testing)",
    )

    args = parser.parse_args()

    project_path = Path(args.project_dir)
    if not project_path.is_absolute():
        try:
            from registry import get_project_path
            resolved = get_project_path(args.project_dir)
            if resolved:
                project_path = resolved
        except ImportError:
            pass

    result = asyncio.run(run_smart_orchestrator(
        project_dir=project_path,
        model=args.model,
        max_iterations=args.max_iterations,
        yolo_mode=args.yolo,
    ))

    print("\n" + "=" * 70)
    print("  ORCHESTRATION COMPLETE")
    print("=" * 70)
    print(f"\nCompleted: {result['total_completed']}")
    print(f"Failed: {result['total_failed']}")
    print(f"Iterations: {result['iterations']}")

+        self.scheduler = SmartScheduler(project_dir, min_parallel, max_parallel)

+        self._claude_threshold = 0.6  # Use Claude if scheduler confidence below this

-        Use Claude to analyze features and decide parallelization.

+        Analyze features and decide parallelization.

+        Uses fast heuristic-based SmartScheduler first. Only consults

+        Claude if the scheduler has low confidence in its decision.

-        completed = stats.get("passing", 0)

-        # Prepare features for analysis

+        # First, try the fast heuristic-based scheduler

+                "category": f.category,

+                "description": f.description,

+        scheduler_decision = self.scheduler.make_decision(features_data, in_progress)

+        # If scheduler is confident, use its decision

+        if scheduler_decision.confidence >= self._claude_threshold:

+            feature_map = {f.id: f for f in features}

+                for fid in scheduler_decision.feature_ids

+                f"Scheduler decision (confidence={scheduler_decision.confidence:.2f}): "

+                f"{scheduler_decision.parallelism} agents"

+            return OrchestrationDecision(

+                recommended_parallelism=scheduler_decision.parallelism,

+                features_to_run=selected_features,

+                reasoning=f"[Heuristic] {scheduler_decision.reasoning}",

+                risk_level=scheduler_decision.risk_level,

+                estimated_duration_minutes=scheduler_decision.estimated_duration,

+        # Scheduler is uncertain - consult Claude for a better decision

+            f"Scheduler uncertain (confidence={scheduler_decision.confidence:.2f}), "

+        # Prepare features for Claude analysis

+        completed = stats.get("passing", 0)

-            features_json=json.dumps(features_data, indent=2),

+            features_json=json.dumps(features_analysis, indent=2),

-            # Use a lightweight client for analysis


Good, now let me also update the fallback method and the run_smart method to record results back to the scheduler so it can learn.


   200→            f"Scheduler uncertain (confidence={scheduler_decision.confidence:.2f}), "
   201→            "consulting Claude..."
   202→        )
   203→
   204→        # Prepare features for Claude analysis
   205→        features_analysis = []
   206→        for f in features[:20]:  # Limit to 20 for context
   207→            features_analysis.append({
   208→                "id": f.id,
   209→                "name": f.name,
   210→                "category": f.category,
   211→                "priority": f.priority,
   212→                "description": f.description[:200],  # Truncate
   213→                "steps_count": len(f.steps),
   214→                "complexity": f.estimated_complexity,
   215→            })
   216→
   217→        completed = stats.get("passing", 0)
   218→        prompt = ANALYSIS_PROMPT.format(
   219→            features_json=json.dumps(features_analysis, indent=2),
   220→            in_progress_count=in_progress,
   221→            completed_count=completed,
   222→            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
   223→        )
   224→
   225→        # Call Claude for analysis
   226→        try:
   227→            from anthropic import Anthropic
   228→            client = Anthropic()
   229→
   230→            response = client.messages.create(
   231→                model="claude-sonnet-4-20250514",
   232→                max_tokens=1024,
   233→                messages=[{"role": "user", "content": prompt}],
   234→            )
   235→
   236→            response_text = response.content[0].text
   237→
   238→            # Extract JSON from response
   239→            json_start = response_text.find("{")
   240→            json_end = response_text.rfind("}") + 1
   241→            if json_start >= 0 and json_end > json_start:
   242→                decision_data = json.loads(response_text[json_start:json_end])
   243→            else:
   244→                raise ValueError("No JSON found in response")
   245→
   246→            # Map feature IDs to features
   247→            feature_map = {f.id: f for f in features}
   248→            features_to_run = [
   249→                feature_map[fid]
   250→                for fid in decision_data.get("feature_ids_to_run", [])
   251→                if fid in feature_map
   252→            ]
   253→
   254→            # Apply caps
   255→            parallelism = min(
   256→                max(decision_data.get("recommended_parallelism", 1), self.min_parallel),
   257→                self.max_parallel,
   258→            )
   259→
   260→            return OrchestrationDecision(
   261→                recommended_parallelism=parallelism,
   262→                features_to_run=features_to_run[:parallelism],
   263→                reasoning=decision_data.get("reasoning", "Analysis complete"),
   264→                risk_level=decision_data.get("risk_level", "medium"),
   265→                estimated_duration_minutes=decision_data.get("estimated_minutes", 30),
   266→            )
   267→
   268→        except Exception as e:
   269→            logger.warning(f"Analysis failed, using fallback: {e}")
   270→            return self._fallback_decision(features)
   271→
   272→    def _fallback_decision(
   273→        self,
   274→        features: list[ParallelFeature],
   275→    ) -> OrchestrationDecision:
   276→        """
   277→        Fallback decision when Claude analysis fails.
   278→
   279→        Uses simple heuristics based on feature categories.
   280→        """
   281→        if not features:
   282→            return OrchestrationDecision(
   283→                recommended_parallelism=0,
   284→                features_to_run=[],
   285→                reasoning="No features available",
   286→                risk_level="low",
   287→            )
   288→
   289→        # Group by category
   290→        categories = {}
   291→        for f in features:
   292→            cat = f.category.lower()
   293→            if cat not in categories:
   294→                categories[cat] = []
   295→            categories[cat].append(f)
   296→
   297→        # Pick one from each category (up to max)
   298→        selected = []
   299→        for cat, cat_features in categories.items():

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

            f"Scheduler uncertain (confidence={scheduler_decision.confidence:.2f}), "
            "consulting Claude..."
        )

        # Prepare features for Claude analysis
        features_analysis = []
        for f in features[:20]:  # Limit to 20 for context
            features_analysis.append({
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "priority": f.priority,
                "description": f.description[:200],  # Truncate
                "steps_count": len(f.steps),
                "complexity": f.estimated_complexity,
            })

        completed = stats.get("passing", 0)
        prompt = ANALYSIS_PROMPT.format(
            features_json=json.dumps(features_analysis, indent=2),
            in_progress_count=in_progress,
            completed_count=completed,
            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
        )

        # Call Claude for analysis
        try:
            from anthropic import Anthropic
            client = Anthropic()

            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )

            response_text = response.content[0].text

            # Extract JSON from response
            json_start = response_text.find("{")
            json_end = response_text.rfind("}") + 1
            if json_start >= 0 and json_end > json_start:
                decision_data = json.loads(response_text[json_start:json_end])
            else:
                raise ValueError("No JSON found in response")

            # Map feature IDs to features
            feature_map = {f.id: f for f in features}
            features_to_run = [
                feature_map[fid]
                for fid in decision_data.get("feature_ids_to_run", [])
                if fid in feature_map
            ]

            # Apply caps
            parallelism = min(
                max(decision_data.get("recommended_parallelism", 1), self.min_parallel),
                self.max_parallel,
            )

            return OrchestrationDecision(
                recommended_parallelism=parallelism,
                features_to_run=features_to_run[:parallelism],
                reasoning=decision_data.get("reasoning", "Analysis complete"),
                risk_level=decision_data.get("risk_level", "medium"),
                estimated_duration_minutes=decision_data.get("estimated_minutes", 30),
            )

        except Exception as e:
            logger.warning(f"Analysis failed, using fallback: {e}")
            return self._fallback_decision(features)

    def _fallback_decision(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Fallback decision when Claude analysis fails.

        Uses simple heuristics based on feature categories.
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No features available",
                risk_level="low",
            )

        # Group by category
        categories = {}
        for f in features:
            cat = f.category.lower()
            if cat not in categories:
                categories[cat] = []
            categories[cat].append(f)

        # Pick one from each category (up to max)
        selected = []
        for cat, cat_features in categories.items():


Now let me read the run_smart method to update it to record results to the scheduler.


   300→            if len(selected) < self.max_parallel:
   301→                selected.append(cat_features[0])
   302→
   303→        # If we have room and features left, add more
   304→        remaining = [f for f in features if f not in selected]
   305→        while len(selected) < self.min_parallel and remaining:
   306→            selected.append(remaining.pop(0))
   307→
   308→        return OrchestrationDecision(
   309→            recommended_parallelism=len(selected),
   310→            features_to_run=selected,
   311→            reasoning="Fallback: One feature per category",
   312→            risk_level="medium",
   313→        )
   314→
   315→    async def run_smart(
   316→        self,
   317→        max_iterations: Optional[int] = None,
   318→    ) -> dict:
   319→        """
   320→        Run the smart orchestration loop.
   321→
   322→        Args:
   323→            max_iterations: Maximum iteration cycles
   324→
   325→        Returns:
   326→            Summary statistics
   327→        """
   328→        print("\n" + "=" * 70)
   329→        print("  SMART ORCHESTRATOR")
   330→        print("=" * 70)
   331→        print(f"\nProject: {self.project_dir}")
   332→        print(f"Model: {self.model}")
   333→        print(f"Parallelism range: {self.min_parallel}-{self.max_parallel}")
   334→        print()
   335→
   336→        total_completed = 0
   337→        total_failed = 0
   338→        iteration = 0
   339→
   340→        while True:
   341→            iteration += 1
   342→
   343→            if max_iterations and iteration > max_iterations:
   344→                print(f"\nReached max iterations ({max_iterations})")
   345→                break
   346→
   347→            # Get available features
   348→            features = self.feature_graph.get_parallelizable_features(
   349→                count=20,  # Get more for analysis
   350→            )
   351→
   352→            if not features:
   353→                stats = get_feature_stats(self.project_dir)
   354→                if stats and stats.get("pending", 0) == 0:
   355→                    print("\n All features completed!")
   356→                    break
   357→                print("\nNo parallelizable features, waiting...")
   358→                await asyncio.sleep(5)
   359→                continue
   360→
   361→            # Analyze and decide
   362→            print(f"\n--- Iteration {iteration} ---")
   363→            print(f"Analyzing {len(features)} pending features...")
   364→
   365→            decision = await self.analyze_features(features)
   366→
   367→            print(f"\nDecision: Run {decision.recommended_parallelism} agents in parallel")
   368→            print(f"Risk level: {decision.risk_level}")
   369→            print(f"Reasoning: {decision.reasoning}")
   370→            print(f"\nFeatures selected:")
   371→            for f in decision.features_to_run:
   372→                print(f"  - #{f.id}: {f.name} [{f.category}]")
   373→
   374→            if not decision.features_to_run:
   375→                print("\nNo features selected, waiting...")
   376→                await asyncio.sleep(5)
   377→                continue
   378→
   379→            # Create runner with dynamic parallelism
   380→            runner = ParallelAgentRunner(
   381→                project_dir=self.project_dir,
   382→                model=self.model,
   383→                max_parallel=decision.recommended_parallelism,
   384→                yolo_mode=self.yolo_mode,
   385→            )
   386→
   387→            # Run the selected features
   388→            tasks = []
   389→            for i, feature in enumerate(decision.features_to_run):
   390→                agent_id = f"smart-{iteration}-{i}"
   391→                task = asyncio.create_task(
   392→                    runner.run_single_feature(feature, agent_id)
   393→                )
   394→                tasks.append((feature, task))
   395→
   396→            # Wait for completion
   397→            for feature, task in tasks:
   398→                try:
   399→                    result = await task
   400→                    if result.success:
   401→                        total_completed += 1
   402→                        print(f"  Completed: #{feature.id} {feature.name}")
   403→                    else:
   404→                        total_failed += 1
   405→                        self._recent_failures.append(f"#{feature.id}")
   406→                        print(f"  Failed: #{feature.id} - {result.error}")
   407→                except Exception as e:
   408→                    total_failed += 1
   409→                    self._recent_failures.append(f"#{feature.id}")
   410→                    print(f"  Error: #{feature.id} - {e}")
   411→
   412→            # Brief delay
   413→            await asyncio.sleep(3)
   414→
   415→        return {
   416→            "total_completed": total_completed,
   417→            "total_failed": total_failed,
   418→            "iterations": iteration,
   419→        }

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

            if len(selected) < self.max_parallel:
                selected.append(cat_features[0])

        # If we have room and features left, add more
        remaining = [f for f in features if f not in selected]
        while len(selected) < self.min_parallel and remaining:
            selected.append(remaining.pop(0))

        return OrchestrationDecision(
            recommended_parallelism=len(selected),
            features_to_run=selected,
            reasoning="Fallback: One feature per category",
            risk_level="medium",
        )

    async def run_smart(
        self,
        max_iterations: Optional[int] = None,
    ) -> dict:
        """
        Run the smart orchestration loop.

        Args:
            max_iterations: Maximum iteration cycles

        Returns:
            Summary statistics
        """
        print("\n" + "=" * 70)
        print("  SMART ORCHESTRATOR")
        print("=" * 70)
        print(f"\nProject: {self.project_dir}")
        print(f"Model: {self.model}")
        print(f"Parallelism range: {self.min_parallel}-{self.max_parallel}")
        print()

        total_completed = 0
        total_failed = 0
        iteration = 0

        while True:
            iteration += 1

            if max_iterations and iteration > max_iterations:
                print(f"\nReached max iterations ({max_iterations})")
                break

            # Get available features
            features = self.feature_graph.get_parallelizable_features(
                count=20,  # Get more for analysis
            )

            if not features:
                stats = get_feature_stats(self.project_dir)
                if stats and stats.get("pending", 0) == 0:
                    print("\n All features completed!")
                    break
                print("\nNo parallelizable features, waiting...")
                await asyncio.sleep(5)
                continue

            # Analyze and decide
            print(f"\n--- Iteration {iteration} ---")
            print(f"Analyzing {len(features)} pending features...")

            decision = await self.analyze_features(features)

            print(f"\nDecision: Run {decision.recommended_parallelism} agents in parallel")
            print(f"Risk level: {decision.risk_level}")
            print(f"Reasoning: {decision.reasoning}")
            print(f"\nFeatures selected:")
            for f in decision.features_to_run:
                print(f"  - #{f.id}: {f.name} [{f.category}]")

            if not decision.features_to_run:
                print("\nNo features selected, waiting...")
                await asyncio.sleep(5)
                continue

            # Create runner with dynamic parallelism
            runner = ParallelAgentRunner(
                project_dir=self.project_dir,
                model=self.model,
                max_parallel=decision.recommended_parallelism,
                yolo_mode=self.yolo_mode,
            )

            # Run the selected features
            tasks = []
            for i, feature in enumerate(decision.features_to_run):
                agent_id = f"smart-{iteration}-{i}"
                task = asyncio.create_task(
                    runner.run_single_feature(feature, agent_id)
                )
                tasks.append((feature, task))

            # Wait for completion
            for feature, task in tasks:
                try:
                    result = await task
                    if result.success:
                        total_completed += 1
                        print(f"  Completed: #{feature.id} {feature.name}")
                    else:
                        total_failed += 1
                        self._recent_failures.append(f"#{feature.id}")
                        print(f"  Failed: #{feature.id} - {result.error}")
                except Exception as e:
                    total_failed += 1
                    self._recent_failures.append(f"#{feature.id}")
                    print(f"  Error: #{feature.id} - {e}")

            # Brief delay
            await asyncio.sleep(3)

        return {
            "total_completed": total_completed,
            "total_failed": total_failed,
            "iterations": iteration,
        }


Now let me update the run_smart method to record results to the scheduler for learning. I'll update the section where results are processed.


# Wait for completion
            for feature, task in tasks:
                try:
                    result = await task
                    if result.success:
                        total_completed += 1
                        print(f"  Completed: #{feature.id} {feature.name}")
                    else:
                        total_failed += 1
                        self._recent_failures.append(f"#{feature.id}")
                        print(f"  Failed: #{feature.id} - {result.error}")
                except Exception as e:
                    total_failed += 1
                    self._recent_failures.append(f"#{feature.id}")
                    print(f"  Error: #{feature.id} - {e}")

# Wait for completion and record results for learning
            parallelism_level = decision.recommended_parallelism
            for feature, task in tasks:
                try:
                    result = await task
                    # Record result for scheduler learning
                    self.scheduler.record_result(
                        feature_id=feature.id,
                        feature_name=feature.name,
                        category=feature.category,
                        success=result.success,
                        duration_seconds=result.duration_seconds,
                        files_touched=result.files_touched,
                        parallelism_level=parallelism_level,
                    )

                    if result.success:
                        total_completed += 1
                        print(f"  Completed: #{feature.id} {feature.name} ({result.duration_seconds:.1f}s)")
                    else:
                        total_failed += 1
                        self._recent_failures.append(f"#{feature.id}")
                        print(f"  Failed: #{feature.id} - {result.error}")
                except Exception as e:
                    total_failed += 1
                    self._recent_failures.append(f"#{feature.id}")
                    print(f"  Error: #{feature.id} - {e}")
                    # Record failure
                    self.scheduler.record_result(
                        feature_id=feature.id,
                        feature_name=feature.name,
                        category=feature.category,
                        success=False,
                        duration_seconds=0,
                        files_touched=[],
                        parallelism_level=parallelism_level,
                    )


# Wait for completion and record results for learning
            parallelism_level = decision.recommended_parallelism
            for feature, task in tasks:
                try:
                    result = await task
                    # Record result for scheduler learning
                    self.scheduler.record_result(
                        feature_id=feature.id,
                        feature_name=feature.name,
                        category=feature.category,
                        success=result.success,
                        duration_seconds=result.duration_seconds,
                        files_touched=result.files_touched,
                        parallelism_level=parallelism_level,
                    )

                    if result.success:
                        total_completed += 1
                        print(f"  Completed: #{feature.id} {feature.name} ({result.duration_seconds:.1f}s)")
                    else:
                        total_failed += 1
                        self._recent_failures.append(f"#{feature.id}")
                        print(f"  Failed: #{feature.id} - {result.error}")
                except Exception as e:
                    total_failed += 1
                    self._recent_failures.append(f"#{feature.id}")
                    print(f"  Error: #{feature.id} - {e}")
                    # Record failure
                    self.scheduler.record_result(
                        feature_id=feature.id,
                        feature_name=feature.name,
                        category=feature.category,
                        success=False,
                        duration_seconds=0,
                        files_touched=[],
                        parallelism_level=parallelism_level,
                    )

"""
Smart Orchestrator
==================

Uses Claude to dynamically decide how many agents to run in parallel
based on feature analysis, complexity estimation, and conflict prediction.
"""

import asyncio
import json
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

from claude_agent_sdk import ClaudeSDKClient

from parallel_agent import ParallelAgentRunner, AgentResult
from server.services.feature_graph import FeatureGraph, ParallelFeature
from server.services.smart_scheduler import SmartScheduler
from progress import get_feature_stats

logger = logging.getLogger(__name__)

@dataclass
class FeatureCluster:
    """A group of features that can safely run together."""
    features: list[ParallelFeature]
    estimated_parallelism: int
    reasoning: str
    predicted_conflicts: list[str] = field(default_factory=list)
    total_complexity: int = 0

@dataclass
class OrchestrationDecision:
    """Decision about how to run features."""
    recommended_parallelism: int
    features_to_run: list[ParallelFeature]
    reasoning: str
    risk_level: str  # "low", "medium", "high"
    estimated_duration_minutes: int = 0

ANALYSIS_PROMPT = """You are an expert software architect analyzing features for parallel execution.

Given these pending features, analyze them and decide:
1. How many can safely run in parallel
2. Which specific features to run together
3. Risk assessment for parallel execution

Consider:
- Features touching similar files/directories should NOT run together
- UI features can often parallelize with backend features
- Database/schema changes should run alone
- Test features can parallelize with implementation features
- Simple features (1-2 steps) are safer to parallelize than complex ones

## Pending Features

{features_json}

## Current State
- Features in progress: {in_progress_count}
- Features completed: {completed_count}
- Recent failures: {recent_failures}

## Response Format

Respond with a JSON object:
```json
{{
  "recommended_parallelism": <number 1-5>,
  "feature_ids_to_run": [<list of feature IDs to run now>],
  "reasoning": "<brief explanation of your decision>",
  "risk_level": "<low|medium|high>",
  "estimated_minutes": <rough time estimate>,
  "clusters": [
    {{
      "feature_ids": [<ids that can run together>],
      "reason": "<why these are safe together>"
    }}
  ],
  "warnings": ["<any concerns about these features>"]
}}
```

Be conservative - it's better to run fewer in parallel than risk conflicts.
"""

class SmartOrchestrator:
    """
    Intelligent orchestrator that uses Claude to make parallelization decisions.

    Instead of a fixed max_parallel, this orchestrator:
    1. Analyzes pending features using Claude
    2. Clusters features by predicted file conflicts
    3. Estimates complexity and risk
    4. Dynamically adjusts parallelism based on conditions
    """

    def __init__(
        self,
        project_dir: Path,
        model: str = "claude-sonnet-4-20250514",
        min_parallel: int = 1,
        max_parallel: int = 5,
        yolo_mode: bool = False,
    ):
        """
        Initialize the smart orchestrator.

        Args:
            project_dir: Path to the project directory
            model: Claude model for both analysis and coding
            min_parallel: Minimum parallel agents
            max_parallel: Maximum parallel agents (cap)
            yolo_mode: Skip testing if True
        """
        self.project_dir = project_dir
        self.model = model
        self.min_parallel = min_parallel
        self.max_parallel = max_parallel
        self.yolo_mode = yolo_mode

        self.feature_graph = FeatureGraph(project_dir)
        self.scheduler = SmartScheduler(project_dir, min_parallel, max_parallel)
        self._recent_failures: list[str] = []
        self._analysis_cache: dict[str, OrchestrationDecision] = {}
        self._claude_threshold = 0.6  # Use Claude if scheduler confidence below this

    async def analyze_features(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Analyze features and decide parallelization.

        Uses fast heuristic-based SmartScheduler first. Only consults
        Claude if the scheduler has low confidence in its decision.

        Args:
            features: List of pending features

        Returns:
            OrchestrationDecision with recommendations
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No pending features",
                risk_level="low",
            )

        # Get current stats
        stats = get_feature_stats(self.project_dir) or {}
        in_progress = stats.get("in_progress", 0)

        # First, try the fast heuristic-based scheduler
        features_data = [
            {
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "description": f.description,
                "steps": f.steps,
            }
            for f in features
        ]

        scheduler_decision = self.scheduler.make_decision(features_data, in_progress)

        # If scheduler is confident, use its decision
        if scheduler_decision.confidence >= self._claude_threshold:
            feature_map = {f.id: f for f in features}
            selected_features = [
                feature_map[fid]
                for fid in scheduler_decision.feature_ids
                if fid in feature_map
            ]

            logger.info(
                f"Scheduler decision (confidence={scheduler_decision.confidence:.2f}): "
                f"{scheduler_decision.parallelism} agents"
            )

            return OrchestrationDecision(
                recommended_parallelism=scheduler_decision.parallelism,
                features_to_run=selected_features,
                reasoning=f"[Heuristic] {scheduler_decision.reasoning}",
                risk_level=scheduler_decision.risk_level,
                estimated_duration_minutes=scheduler_decision.estimated_duration,
            )

        # Scheduler is uncertain - consult Claude for a better decision
        logger.info(
            f"Scheduler uncertain (confidence={scheduler_decision.confidence:.2f}), "
            "consulting Claude..."
        )

        # Prepare features for Claude analysis
        features_analysis = []
        for f in features[:20]:  # Limit to 20 for context
            features_analysis.append({
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "priority": f.priority,
                "description": f.description[:200],  # Truncate
                "steps_count": len(f.steps),
                "complexity": f.estimated_complexity,
            })

        completed = stats.get("passing", 0)
        prompt = ANALYSIS_PROMPT.format(
            features_json=json.dumps(features_analysis, indent=2),
            in_progress_count=in_progress,
            completed_count=completed,
            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
        )

        # Call Claude for analysis
        try:
            from anthropic import Anthropic
            client = Anthropic()

            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )

            response_text = response.content[0].text

            # Extract JSON from response
            json_start = response_text.find("{")
            json_end = response_text.rfind("}") + 1
            if json_start >= 0 and json_end > json_start:
                decision_data = json.loads(response_text[json_start:json_end])
            else:
                raise ValueError("No JSON found in response")

            # Map feature IDs to features
            feature_map = {f.id: f for f in features}
            features_to_run = [
                feature_map[fid]
                for fid in decision_data.get("feature_ids_to_run", [])
                if fid in feature_map
            ]

            # Apply caps
            parallelism = min(
                max(decision_data.get("recommended_parallelism", 1), self.min_parallel),
                self.max_parallel,
            )

            return OrchestrationDecision(
                recommended_parallelism=parallelism,
                features_to_run=features_to_run[:parallelism],
                reasoning=decision_data.get("reasoning", "Analysis complete"),
                risk_level=decision_data.get("risk_level", "medium"),
                estimated_duration_minutes=decision_data.get("estimated_minutes", 30),
            )

        except Exception as e:
            logger.warning(f"Analysis failed, using fallback: {e}")
            return self._fallback_decision(features)

    def _fallback_decision(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Fallback decision when Claude analysis fails.

        Uses simple heuristics based on feature categories.
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No features available",
                risk_level="low",
            )

        # Group by category
        categories = {}
        for f in features:
            cat = f.category.lower()
            if cat not in categories:
                categories[cat] = []
            categories[cat].append(f)

        # Pick one from each category (up to max)
        selected = []
        for cat, cat_features in categories.items():
            if len(selected) < self.max_parallel:
                selected.append(cat_features[0])

        # If we have room and features left, add more
        remaining = [f for f in features if f not in selected]
        while len(selected) < self.min_parallel and remaining:
            selected.append(remaining.pop(0))

        return OrchestrationDecision(
            recommended_parallelism=len(selected),
            features_to_run=selected,
            reasoning="Fallback: One feature per category",
            risk_level="medium",
        )

    async def run_smart(
        self,
        max_iterations: Optional[int] = None,
    ) -> dict:
        """
        Run the smart orchestration loop.

        Args:
            max_iterations: Maximum iteration cycles

        Returns:
            Summary statistics
        """
        print("\n" + "=" * 70)
        print("  SMART ORCHESTRATOR")
        print("=" * 70)
        print(f"\nProject: {self.project_dir}")
        print(f"Model: {self.model}")
        print(f"Parallelism range: {self.min_parallel}-{self.max_parallel}")
        print()

        total_completed = 0
        total_failed = 0
        iteration = 0

        while True:
            iteration += 1

            if max_iterations and iteration > max_iterations:
                print(f"\nReached max iterations ({max_iterations})")
                break

            # Get available features
            features = self.feature_graph.get_parallelizable_features(
                count=20,  # Get more for analysis
            )

            if not features:
                stats = get_feature_stats(self.project_dir)
                if stats and stats.get("pending", 0) == 0:
                    print("\n All features completed!")
                    break
                print("\nNo parallelizable features, waiting...")
                await asyncio.sleep(5)
                continue

            # Analyze and decide
            print(f"\n--- Iteration {iteration} ---")
            print(f"Analyzing {len(features)} pending features...")

            decision = await self.analyze_features(features)

            print(f"\nDecision: Run {decision.recommended_parallelism} agents in parallel")
            print(f"Risk level: {decision.risk_level}")
            print(f"Reasoning: {decision.reasoning}")
            print(f"\nFeatures selected:")
            for f in decision.features_to_run:
                print(f"  - #{f.id}: {f.name} [{f.category}]")

            if not decision.features_to_run:
                print("\nNo features selected, waiting...")
                await asyncio.sleep(5)
                continue

            # Create runner with dynamic parallelism
            runner = ParallelAgentRunner(
                project_dir=self.project_dir,
                model=self.model,
                max_parallel=decision.recommended_parallelism,
                yolo_mode=self.yolo_mode,
            )

            # Run the selected features
            tasks = []
            for i, feature in enumerate(decision.features_to_run):
                agent_id = f"smart-{iteration}-{i}"
                task = asyncio.create_task(
                    runner.run_single_feature(feature, agent_id)
                )
                tasks.append((feature, task))

            # Wait for completion
            for feature, task in tasks:
                try:
                    result = await task
                    if result.success:
                        total_completed += 1
                        print(f"  Completed: #{feature.id} {feature.name}")
                    else:
                        total_failed += 1
                        self._recent_failures.append(f"#{feature.id}")
                        print(f"  Failed: #{feature.id} - {result.error}")
                except Exception as e:
                    total_failed += 1
                    self._recent_failures.append(f"#{feature.id}")
                    print(f"  Error: #{feature.id} - {e}")

            # Brief delay
            await asyncio.sleep(3)

        return {
            "total_completed": total_completed,
            "total_failed": total_failed,
            "iterations": iteration,
        }

async def run_smart_orchestrator(
    project_dir: Path,
    model: str = "claude-sonnet-4-20250514",
    max_iterations: Optional[int] = None,
    yolo_mode: bool = False,
) -> dict:
    """
    Convenience function to run smart orchestrator.

    Args:
        project_dir: Project directory
        model: Claude model
        max_iterations: Max iterations
        yolo_mode: Skip testing

    Returns:
        Summary statistics
    """
    orchestrator = SmartOrchestrator(
        project_dir=project_dir,
        model=model,
        yolo_mode=yolo_mode,
    )

    return await orchestrator.run_smart(max_iterations=max_iterations)

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Smart orchestrator for parallel coding")
    parser.add_argument(
        "--project-dir",
        type=str,
        required=True,
        help="Project directory",
    )
    parser.add_argument(
        "--model",
        type=str,
        default="claude-sonnet-4-20250514",
        help="Claude model",
    )
    parser.add_argument(
        "--max-iterations",
        type=int,
        default=None,
        help="Maximum iterations",
    )
    parser.add_argument(
        "--yolo",
        action="store_true",
        help="YOLO mode (skip testing)",
    )

    args = parser.parse_args()

    project_path = Path(args.project_dir)
    if not project_path.is_absolute():
        try:
            from registry import get_project_path
            resolved = get_project_path(args.project_dir)
            if resolved:
                project_path = resolved
        except ImportError:
            pass

    result = asyncio.run(run_smart_orchestrator(
        project_dir=project_path,
        model=args.model,
        max_iterations=args.max_iterations,
        yolo_mode=args.yolo,
    ))

    print("\n" + "=" * 70)
    print("  ORCHESTRATION COMPLETE")
    print("=" * 70)
    print(f"\nCompleted: {result['total_completed']}")
    print(f"Failed: {result['total_failed']}")
    print(f"Iterations: {result['iterations']}")

+            # Wait for completion and record results for learning

+            parallelism_level = decision.recommended_parallelism

+                    # Record result for scheduler learning

+                    self.scheduler.record_result(

+                        feature_id=feature.id,

+                        feature_name=feature.name,

+                        category=feature.category,

+                        success=result.success,

+                        duration_seconds=result.duration_seconds,

+                        files_touched=result.files_touched,

+                        parallelism_level=parallelism_level,

-                        print(f"  Completed: #{feature.id} {feature.name}")

+                        print(f"  Completed: #{feature.id} {feature.name} ({result.duration_seconds:.1f}s)")

+                        duration_seconds=0,

+                        files_touched=[],


Now let me provide a summary of what was added for dynamic scheduling.


Done! Here's what was added for dynamic scheduling:

## Smart Scheduling System

### SmartScheduler (`server/services/smart_scheduler.py`)
Fast heuristic-based scheduler that:
- **Analyzes complexity** based on steps, description length, keywords
- **Predicts file conflicts** from category and feature names
- **Learns from history** - tracks past execution times and failure rates
- **Adapts dynamically** - reduces parallelism after failures
- **Detects solo features** - database migrations, refactors run alone

### SmartOrchestrator (`smart_orchestrator.py`)
Two-tier decision system:
1. **Fast path**: Uses SmartScheduler heuristics (instant)
2. **Smart path**: Consults Claude when confidence < 0.6

```
Decision Flow:
┌─────────────────────────────────────────────────────────┐
│  Pending Features                                        │
└─────────────────┬───────────────────────────────────────┘
                  ▼
┌─────────────────────────────────────────────────────────┐
│  SmartScheduler (heuristics)                            │
│  - Category conflict detection                          │
│  - Complexity scoring                                   │
│  - Historical performance                               │
│  - File overlap prediction                              │
└─────────────────┬───────────────────────────────────────┘
                  ▼
        ┌─────────────────┐
        │ Confidence ≥ 0.6 │
        └────────┬────────┘
                 │
    ┌────────────┴────────────┐
    ▼                         ▼
┌───────────┐         ┌───────────────┐
│ Use fast  │         │ Consult Claude │
│ decision  │         │ for analysis   │
└───────────┘         └───────────────┘
```

### API Endpoint (`/api/scheduler/`)
- `POST /projects/{name}/analyze` - Get parallelization recommendation
- `GET /projects/{name}/stats` - View learning statistics
- `POST /projects/{name}/record-result` - Feed back results

### Run It
```bash
python smart_orchestrator.py --project-dir my-project
```

The scheduler learns over time - as it processes more features, its predictions improve and it needs Claude less often.


Ok, I still use init.bat to start it?


The user is asking about how to start the Autocoder system. Let me check what init.bat does and whether we need to update it or create new start scripts for the new features.


Let me check the current startup scripts.


     1→@echo off
     2→cd /d "%~dp0"
     3→
     4→echo.
     5→echo ========================================
     6→echo   Autonomous Coding Agent
     7→echo ========================================
     8→echo.
     9→
    10→REM Check if Claude CLI is installed
    11→where claude >nul 2>nul
    12→if %errorlevel% neq 0 (
    13→    echo [ERROR] Claude CLI not found
    14→    echo.
    15→    echo Please install Claude CLI first:
    16→    echo   https://claude.ai/download
    17→    echo.
    18→    echo Then run this script again.
    19→    echo.
    20→    pause
    21→    exit /b 1
    22→)
    23→
    24→echo [OK] Claude CLI found
    25→
    26→REM Check if user has credentials (check for ~/.claude/.credentials.json)
    27→set "CLAUDE_CREDS=%USERPROFILE%\.claude\.credentials.json"
    28→if exist "%CLAUDE_CREDS%" (
    29→    echo [OK] Claude credentials found
    30→    goto :setup_venv
    31→)
    32→
    33→REM No credentials - prompt user to login
    34→echo [!] Not authenticated with Claude
    35→echo.
    36→echo You need to run 'claude login' to authenticate.
    37→echo This will open a browser window to sign in.
    38→echo.
    39→set /p "LOGIN_CHOICE=Would you like to run 'claude login' now? (y/n): "
    40→
    41→if /i "%LOGIN_CHOICE%"=="y" (
    42→    echo.
    43→    echo Running 'claude login'...
    44→    echo Complete the login in your browser, then return here.
    45→    echo.
    46→    call claude login
    47→
    48→    REM Check if login succeeded
    49→    if exist "%CLAUDE_CREDS%" (
    50→        echo.
    51→        echo [OK] Login successful!
    52→        goto :setup_venv
    53→    ) else (
    54→        echo.
    55→        echo [ERROR] Login failed or was cancelled.
    56→        echo Please try again.
    57→        pause
    58→        exit /b 1
    59→    )
    60→) else (
    61→    echo.
    62→    echo Please run 'claude login' manually, then try again.
    63→    pause
    64→    exit /b 1
    65→)
    66→
    67→:setup_venv
    68→echo.
    69→
    70→REM Check if venv exists, create if not
    71→if not exist "venv\Scripts\activate.bat" (
    72→    echo Creating virtual environment...
    73→    python -m venv venv
    74→)
    75→
    76→REM Activate the virtual environment
    77→call venv\Scripts\activate.bat
    78→
    79→REM Install dependencies
    80→echo Installing dependencies...
    81→pip install -r requirements.txt --quiet
    82→
    83→REM Run the app
    84→python start.py
    85→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

@echo off
cd /d "%~dp0"

echo.
echo ========================================
echo   Autonomous Coding Agent
echo ========================================
echo.

REM Check if Claude CLI is installed
where claude >nul 2>nul
if %errorlevel% neq 0 (
    echo [ERROR] Claude CLI not found
    echo.
    echo Please install Claude CLI first:
    echo   https://claude.ai/download
    echo.
    echo Then run this script again.
    echo.
    pause
    exit /b 1
)

echo [OK] Claude CLI found

REM Check if user has credentials (check for ~/.claude/.credentials.json)
set "CLAUDE_CREDS=%USERPROFILE%\.claude\.credentials.json"
if exist "%CLAUDE_CREDS%" (
    echo [OK] Claude credentials found
    goto :setup_venv
)

REM No credentials - prompt user to login
echo [!] Not authenticated with Claude
echo.
echo You need to run 'claude login' to authenticate.
echo This will open a browser window to sign in.
echo.
set /p "LOGIN_CHOICE=Would you like to run 'claude login' now? (y/n): "

if /i "%LOGIN_CHOICE%"=="y" (
    echo.
    echo Running 'claude login'...
    echo Complete the login in your browser, then return here.
    echo.
    call claude login

    REM Check if login succeeded
    if exist "%CLAUDE_CREDS%" (
        echo.
        echo [OK] Login successful!
        goto :setup_venv
    ) else (
        echo.
        echo [ERROR] Login failed or was cancelled.
        echo Please try again.
        pause
        exit /b 1
    )
) else (
    echo.
    echo Please run 'claude login' manually, then try again.
    pause
    exit /b 1
)

:setup_venv
echo.

REM Check if venv exists, create if not
if not exist "venv\Scripts\activate.bat" (
    echo Creating virtual environment...
    python -m venv venv
)

REM Activate the virtual environment
call venv\Scripts\activate.bat

REM Install dependencies
echo Installing dependencies...
pip install -r requirements.txt --quiet

REM Run the app
python start.py


     1→@echo off
     2→cd /d "%~dp0"
     3→REM AutoCoder UI Launcher for Windows
     4→REM This script launches the web UI for the autonomous coding agent.
     5→
     6→echo.
     7→echo ====================================
     8→echo   AutoCoder UI
     9→echo ====================================
    10→echo.
    11→
    12→REM Check if Python is available
    13→where python >nul 2>&1
    14→if %ERRORLEVEL% neq 0 (
    15→    echo ERROR: Python not found in PATH
    16→    echo Please install Python from https://python.org
    17→    pause
    18→    exit /b 1
    19→)
    20→
    21→REM Check if venv exists, create if not
    22→if not exist "venv" (
    23→    echo Creating virtual environment...
    24→    python -m venv venv
    25→)
    26→
    27→REM Activate the virtual environment
    28→call venv\Scripts\activate.bat
    29→
    30→REM Install dependencies
    31→echo Installing dependencies...
    32→pip install -r requirements.txt --quiet
    33→
    34→REM Run the Python launcher
    35→python "%~dp0start_ui.py" %*
    36→
    37→pause
    38→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

@echo off
cd /d "%~dp0"
REM AutoCoder UI Launcher for Windows
REM This script launches the web UI for the autonomous coding agent.

echo.
echo ====================================
echo   AutoCoder UI
echo ====================================
echo.

REM Check if Python is available
where python >nul 2>&1
if %ERRORLEVEL% neq 0 (
    echo ERROR: Python not found in PATH
    echo Please install Python from https://python.org
    pause
    exit /b 1
)

REM Check if venv exists, create if not
if not exist "venv" (
    echo Creating virtual environment...
    python -m venv venv
)

REM Activate the virtual environment
call venv\Scripts\activate.bat

REM Install dependencies
echo Installing dependencies...
pip install -r requirements.txt --quiet

REM Run the Python launcher
python "%~dp0start_ui.py" %*

pause


I see there are two startup scripts:
1. `start.bat` - Runs the CLI version via `start.py`
2. `start_ui.bat` - Runs the web UI via `start_ui.py`

Let me check start_ui.py to see how it starts the server.