File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\smart_orchestrator.py

"""
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']}")


That's a good smart orchestrator, but I want to make it even smarter by adding more heuristics and also adding an endpoint so it can be controlled from the UI. Let me also add a more sophisticated analysis that considers:

1. Feature similarity detection (name/description similarity)
2. Historical performance data
3. Adaptive throttling based on recent failures
4. Resource awareness

Let me enhance it further.


Let me enhance this with more sophisticated analysis and add a REST API endpoint for UI control.


Write

"""
Smart Scheduler
===============

Intelligent scheduling system that dynamically determines optimal
parallelism based on feature analysis, historical data, and system state.
"""

import asyncio
import json
import logging
import re
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional, Callable, Any
from collections import defaultdict

logger = logging.getLogger(__name__)

@dataclass
class FeatureAnalysis:
    """Analysis of a single feature."""
    feature_id: int
    name: str
    category: str
    complexity_score: int  # 1-10
    predicted_files: list[str]
    predicted_duration_minutes: int
    risk_factors: list[str]
    can_parallelize_with: list[int]  # Feature IDs
    should_run_alone: bool = False

@dataclass
class SchedulerDecision:
    """Scheduling decision with full reasoning."""
    parallelism: int
    feature_ids: list[int]
    reasoning: str
    confidence: float  # 0-1
    risk_level: str
    estimated_duration: int  # minutes
    warnings: list[str] = field(default_factory=list)

@dataclass
class HistoricalRecord:
    """Record of past feature execution."""
    feature_id: int
    feature_name: str
    category: str
    success: bool
    duration_seconds: float
    files_touched: list[str]
    timestamp: datetime
    parallelism_level: int

class SmartScheduler:
    """
    Intelligent scheduler that learns and adapts.

    Decision factors:
    1. Feature complexity (steps, description length, category)
    2. Predicted file conflicts (category-based, name-based heuristics)
    3. Historical performance (past durations, failure rates)
    4. Current system state (in-progress count, recent failures)
    5. Feature clustering (similarity detection)
    """

    # Category conflict matrix - categories that shouldn't run together
    CONFLICT_CATEGORIES = {
        ("database", "migration"),
        ("database", "schema"),
        ("migration", "schema"),
        ("config", "setup"),
        ("auth", "security"),
    }

    # Categories that are generally safe to parallelize
    SAFE_PARALLEL_CATEGORIES = {
        "ui", "frontend", "component", "style", "test", "docs",
    }

    # Keywords indicating features should run alone
    SOLO_KEYWORDS = [
        "refactor", "restructure", "migrate", "breaking", "schema",
        "database", "init", "setup", "config", "security", "auth",
    ]

    def __init__(
        self,
        project_dir: Path,
        min_parallel: int = 1,
        max_parallel: int = 5,
    ):
        """
        Initialize the smart scheduler.

        Args:
            project_dir: Project directory
            min_parallel: Minimum parallelism
            max_parallel: Maximum parallelism cap
        """
        self.project_dir = project_dir
        self.min_parallel = min_parallel
        self.max_parallel = max_parallel

        # Historical data
        self._history: list[HistoricalRecord] = []
        self._failure_streak = 0
        self._last_decision_time: Optional[datetime] = None

        # Load history if exists
        self._load_history()

    def _load_history(self) -> None:
        """Load historical data from disk."""
        history_file = self.project_dir / ".scheduler_history.json"
        if history_file.exists():
            try:
                with open(history_file, "r") as f:
                    data = json.load(f)
                self._history = [
                    HistoricalRecord(
                        feature_id=r["feature_id"],
                        feature_name=r["feature_name"],
                        category=r["category"],
                        success=r["success"],
                        duration_seconds=r["duration_seconds"],
                        files_touched=r.get("files_touched", []),
                        timestamp=datetime.fromisoformat(r["timestamp"]),
                        parallelism_level=r.get("parallelism_level", 1),
                    )
                    for r in data
                ]
                logger.info(f"Loaded {len(self._history)} history records")
            except Exception as e:
                logger.warning(f"Failed to load history: {e}")

    def _save_history(self) -> None:
        """Save historical data to disk."""
        history_file = self.project_dir / ".scheduler_history.json"
        try:
            data = [
                {
                    "feature_id": r.feature_id,
                    "feature_name": r.feature_name,
                    "category": r.category,
                    "success": r.success,
                    "duration_seconds": r.duration_seconds,
                    "files_touched": r.files_touched,
                    "timestamp": r.timestamp.isoformat(),
                    "parallelism_level": r.parallelism_level,
                }
                for r in self._history[-100:]  # Keep last 100
            ]
            with open(history_file, "w") as f:
                json.dump(data, f, indent=2)
        except Exception as e:
            logger.warning(f"Failed to save history: {e}")

    def record_result(
        self,
        feature_id: int,
        feature_name: str,
        category: str,
        success: bool,
        duration_seconds: float,
        files_touched: list[str],
        parallelism_level: int,
    ) -> None:
        """
        Record a feature execution result.

        Args:
            feature_id: Feature ID
            feature_name: Feature name
            category: Feature category
            success: Whether it succeeded
            duration_seconds: How long it took
            files_touched: Files that were modified
            parallelism_level: How many agents were running
        """
        record = HistoricalRecord(
            feature_id=feature_id,
            feature_name=feature_name,
            category=category,
            success=success,
            duration_seconds=duration_seconds,
            files_touched=files_touched,
            timestamp=datetime.now(),
            parallelism_level=parallelism_level,
        )
        self._history.append(record)

        # Track failure streak
        if success:
            self._failure_streak = 0
        else:
            self._failure_streak += 1

        self._save_history()

    def analyze_feature(self, feature: dict) -> FeatureAnalysis:
        """
        Analyze a single feature.

        Args:
            feature: Feature dict with id, name, category, description, steps

        Returns:
            FeatureAnalysis with predictions
        """
        name = feature.get("name", "").lower()
        category = feature.get("category", "").lower()
        description = feature.get("description", "").lower()
        steps = feature.get("steps", [])

        # Calculate complexity score (1-10)
        complexity = 1

        # Steps contribute
        complexity += min(len(steps), 5)

        # Description length
        if len(description) > 500:
            complexity += 2
        elif len(description) > 200:
            complexity += 1

        # Keywords that increase complexity
        complex_keywords = ["refactor", "migrate", "integrate", "overhaul", "redesign"]
        if any(kw in name or kw in description for kw in complex_keywords):
            complexity += 2

        complexity = min(complexity, 10)

        # Predict files based on category and name
        predicted_files = self._predict_files(name, category, description)

        # Estimate duration based on complexity and history
        duration = self._estimate_duration(category, complexity)

        # Identify risk factors
        risk_factors = []
        if any(kw in name or kw in description for kw in self.SOLO_KEYWORDS):
            risk_factors.append("Contains high-risk keywords")
        if complexity >= 7:
            risk_factors.append("High complexity")
        if "database" in category or "database" in description:
            risk_factors.append("Database changes")

        # Should run alone?
        should_solo = (
            complexity >= 8 or
            any(kw in name for kw in ["migrate", "schema", "init", "setup"]) or
            len(risk_factors) >= 2
        )

        return FeatureAnalysis(
            feature_id=feature["id"],
            name=feature["name"],
            category=category,
            complexity_score=complexity,
            predicted_files=predicted_files,
            predicted_duration_minutes=duration,
            risk_factors=risk_factors,
            can_parallelize_with=[],  # Filled in later
            should_run_alone=should_solo,
        )

    def _predict_files(
        self,
        name: str,
        category: str,
        description: str,
    ) -> list[str]:
        """Predict which files/directories a feature will touch."""
        files = []

        # Category-based predictions
        category_patterns = {
            "ui": ["src/components/", "src/pages/", "src/styles/"],
            "frontend": ["src/components/", "src/pages/", "src/lib/"],
            "component": ["src/components/"],
            "api": ["src/api/", "src/routes/", "server/"],
            "backend": ["server/", "src/api/", "src/services/"],
            "database": ["src/db/", "src/models/", "migrations/"],
            "test": ["tests/", "__tests__/", "*.test.*"],
            "auth": ["src/auth/", "src/middleware/"],
            "config": ["config/", "*.config.*"],
        }

        for cat, patterns in category_patterns.items():
            if cat in category:
                files.extend(patterns)

        # Name-based extraction
        # Convert camelCase/PascalCase to path
        name_parts = re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)', name)
        if name_parts:
            potential_path = "/".join(p.lower() for p in name_parts[:2])
            files.append(f"src/{potential_path}")

        return list(set(files))

    def _estimate_duration(self, category: str, complexity: int) -> int:
        """Estimate duration in minutes based on history and complexity."""
        # Check historical average for category
        category_history = [
            r for r in self._history
            if r.category.lower() == category.lower() and r.success
        ]

        if category_history:
            avg_duration = sum(r.duration_seconds for r in category_history) / len(category_history)
            return int(avg_duration / 60) + complexity * 2

        # Default estimate based on complexity
        return 5 + complexity * 3

    def _calculate_similarity(
        self,
        f1: FeatureAnalysis,
        f2: FeatureAnalysis,
    ) -> float:
        """
        Calculate similarity between two features (0-1).
        Higher similarity = more likely to conflict.
        """
        similarity = 0.0

        # Same category
        if f1.category == f2.category:
            similarity += 0.3

        # Conflicting categories
        cat_pair = tuple(sorted([f1.category, f2.category]))
        if cat_pair in self.CONFLICT_CATEGORIES:
            similarity += 0.5

        # File overlap
        f1_files = set(f1.predicted_files)
        f2_files = set(f2.predicted_files)
        if f1_files and f2_files:
            overlap = len(f1_files & f2_files) / len(f1_files | f2_files)
            similarity += overlap * 0.4

        # Name similarity (simple word overlap)
        f1_words = set(re.findall(r'\w+', f1.name.lower()))
        f2_words = set(re.findall(r'\w+', f2.name.lower()))
        if f1_words and f2_words:
            word_overlap = len(f1_words & f2_words) / len(f1_words | f2_words)
            similarity += word_overlap * 0.2

        return min(similarity, 1.0)

    def make_decision(
        self,
        features: list[dict],
        in_progress_count: int = 0,
    ) -> SchedulerDecision:
        """
        Make a scheduling decision.

        Args:
            features: List of pending feature dicts
            in_progress_count: Number of features currently in progress

        Returns:
            SchedulerDecision with recommendations
        """
        if not features:
            return SchedulerDecision(
                parallelism=0,
                feature_ids=[],
                reasoning="No pending features",
                confidence=1.0,
                risk_level="low",
                estimated_duration=0,
            )

        # Analyze all features
        analyses = [self.analyze_feature(f) for f in features]
        analyses.sort(key=lambda a: (a.should_run_alone, -a.complexity_score))

        # Track warnings
        warnings = []

        # Adjust max based on failure streak
        effective_max = self.max_parallel
        if self._failure_streak >= 3:
            effective_max = max(1, effective_max - 2)
            warnings.append(f"Reduced parallelism due to {self._failure_streak} recent failures")
        elif self._failure_streak >= 1:
            effective_max = max(1, effective_max - 1)

        # Account for already running
        available_slots = max(0, effective_max - in_progress_count)
        if available_slots == 0:
            return SchedulerDecision(
                parallelism=0,
                feature_ids=[],
                reasoning=f"Maximum parallelism reached ({in_progress_count} in progress)",
                confidence=1.0,
                risk_level="low",
                estimated_duration=0,
                warnings=warnings,
            )

        # Find features that should run alone
        solo_features = [a for a in analyses if a.should_run_alone]
        if solo_features and in_progress_count == 0:
            # Run solo feature
            feature = solo_features[0]
            return SchedulerDecision(
                parallelism=1,
                feature_ids=[feature.feature_id],
                reasoning=f"Running solo: {', '.join(feature.risk_factors)}",
                confidence=0.9,
                risk_level="medium",
                estimated_duration=feature.predicted_duration_minutes,
                warnings=warnings + feature.risk_factors,
            )

        # Cluster compatible features
        safe_features = [a for a in analyses if not a.should_run_alone]
        if not safe_features:
            # All features are risky, pick lowest complexity
            feature = min(analyses, key=lambda a: a.complexity_score)
            return SchedulerDecision(
                parallelism=1,
                feature_ids=[feature.feature_id],
                reasoning="All features have risk factors, running lowest complexity",
                confidence=0.7,
                risk_level="high",
                estimated_duration=feature.predicted_duration_minutes,
                warnings=warnings,
            )

        # Build clusters of non-conflicting features
        selected = []
        for analysis in safe_features:
            if len(selected) >= available_slots:
                break

            # Check conflict with already selected
            conflicts = False
            for existing in selected:
                similarity = self._calculate_similarity(analysis, existing)
                if similarity > 0.5:  # Conflict threshold
                    conflicts = True
                    break

            if not conflicts:
                selected.append(analysis)

        # Ensure minimum
        if len(selected) < self.min_parallel and safe_features:
            remaining = [f for f in safe_features if f not in selected]
            while len(selected) < self.min_parallel and remaining:
                selected.append(remaining.pop(0))

        # Calculate confidence and risk
        avg_complexity = sum(a.complexity_score for a in selected) / len(selected) if selected else 0
        risk_level = "low" if avg_complexity < 4 else "medium" if avg_complexity < 7 else "high"
        confidence = max(0.5, 1.0 - (avg_complexity / 20) - (len(selected) / 10))

        total_duration = max(a.predicted_duration_minutes for a in selected) if selected else 0

        # Build reasoning
        categories = set(a.category for a in selected)
        reasoning = f"Running {len(selected)} features from categories: {', '.join(categories)}"

        return SchedulerDecision(
            parallelism=len(selected),
            feature_ids=[a.feature_id for a in selected],
            reasoning=reasoning,
            confidence=confidence,
            risk_level=risk_level,
            estimated_duration=total_duration,
            warnings=warnings,
        )

    def get_stats(self) -> dict:
        """Get scheduler statistics."""
        if not self._history:
            return {
                "total_runs": 0,
                "success_rate": 0,
                "avg_duration_seconds": 0,
                "failure_streak": self._failure_streak,
            }

        successes = sum(1 for r in self._history if r.success)
        avg_duration = sum(r.duration_seconds for r in self._history) / len(self._history)

        return {
            "total_runs": len(self._history),
            "success_rate": successes / len(self._history),
            "avg_duration_seconds": avg_duration,
            "failure_streak": self._failure_streak,
            "categories_seen": list(set(r.category for r in self._history)),
        }


File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\services\smart_scheduler.py

"""
Smart Scheduler
===============

Intelligent scheduling system that dynamically determines optimal
parallelism based on feature analysis, historical data, and system state.
"""

import asyncio
import json
import logging
import re
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional, Callable, Any
from collections import defaultdict

logger = logging.getLogger(__name__)

@dataclass
class FeatureAnalysis:
    """Analysis of a single feature."""
    feature_id: int
    name: str
    category: str
    complexity_score: int  # 1-10
    predicted_files: list[str]
    predicted_duration_minutes: int
    risk_factors: list[str]
    can_parallelize_with: list[int]  # Feature IDs
    should_run_alone: bool = False

@dataclass
class SchedulerDecision:
    """Scheduling decision with full reasoning."""
    parallelism: int
    feature_ids: list[int]
    reasoning: str
    confidence: float  # 0-1
    risk_level: str
    estimated_duration: int  # minutes
    warnings: list[str] = field(default_factory=list)

@dataclass
class HistoricalRecord:
    """Record of past feature execution."""
    feature_id: int
    feature_name: str
    category: str
    success: bool
    duration_seconds: float
    files_touched: list[str]
    timestamp: datetime
    parallelism_level: int

class SmartScheduler:
    """
    Intelligent scheduler that learns and adapts.

    Decision factors:
    1. Feature complexity (steps, description length, category)
    2. Predicted file conflicts (category-based, name-based heuristics)
    3. Historical performance (past durations, failure rates)
    4. Current system state (in-progress count, recent failures)
    5. Feature clustering (similarity detection)
    """

    # Category conflict matrix - categories that shouldn't run together
    CONFLICT_CATEGORIES = {
        ("database", "migration"),
        ("database", "schema"),
        ("migration", "schema"),
        ("config", "setup"),
        ("auth", "security"),
    }

    # Categories that are generally safe to parallelize
    SAFE_PARALLEL_CATEGORIES = {
        "ui", "frontend", "component", "style", "test", "docs",
    }

    # Keywords indicating features should run alone
    SOLO_KEYWORDS = [
        "refactor", "restructure", "migrate", "breaking", "schema",
        "database", "init", "setup", "config", "security", "auth",
    ]

    def __init__(
        self,
        project_dir: Path,
        min_parallel: int = 1,
        max_parallel: int = 5,
    ):
        """
        Initialize the smart scheduler.

        Args:
            project_dir: Project directory
            min_parallel: Minimum parallelism
            max_parallel: Maximum parallelism cap
        """
        self.project_dir = project_dir
        self.min_parallel = min_parallel
        self.max_parallel = max_parallel

        # Historical data
        self._history: list[HistoricalRecord] = []
        self._failure_streak = 0
        self._last_decision_time: Optional[datetime] = None

        # Load history if exists
        self._load_history()

    def _load_history(self) -> None:
        """Load historical data from disk."""
        history_file = self.project_dir / ".scheduler_history.json"
        if history_file.exists():
            try:
                with open(history_file, "r") as f:
                    data = json.load(f)
                self._history = [
                    HistoricalRecord(
                        feature_id=r["feature_id"],
                        feature_name=r["feature_name"],
                        category=r["category"],
                        success=r["success"],
                        duration_seconds=r["duration_seconds"],
                        files_touched=r.get("files_touched", []),
                        timestamp=datetime.fromisoformat(r["timestamp"]),
                        parallelism_level=r.get("parallelism_level", 1),
                    )
                    for r in data
                ]
                logger.info(f"Loaded {len(self._history)} history records")
            except Exception as e:
                logger.warning(f"Failed to load history: {e}")

    def _save_history(self) -> None:
        """Save historical data to disk."""
        history_file = self.project_dir / ".scheduler_history.json"
        try:
            data = [
                {
                    "feature_id": r.feature_id,
                    "feature_name": r.feature_name,
                    "category": r.category,
                    "success": r.success,
                    "duration_seconds": r.duration_seconds,
                    "files_touched": r.files_touched,
                    "timestamp": r.timestamp.isoformat(),
                    "parallelism_level": r.parallelism_level,
                }
                for r in self._history[-100:]  # Keep last 100
            ]
            with open(history_file, "w") as f:
                json.dump(data, f, indent=2)
        except Exception as e:
            logger.warning(f"Failed to save history: {e}")

    def record_result(
        self,
        feature_id: int,
        feature_name: str,
        category: str,
        success: bool,
        duration_seconds: float,
        files_touched: list[str],
        parallelism_level: int,
    ) -> None:
        """
        Record a feature execution result.

        Args:
            feature_id: Feature ID
            feature_name: Feature name
            category: Feature category
            success: Whether it succeeded
            duration_seconds: How long it took
            files_touched: Files that were modified
            parallelism_level: How many agents were running
        """
        record = HistoricalRecord(
            feature_id=feature_id,
            feature_name=feature_name,
            category=category,
            success=success,
            duration_seconds=duration_seconds,
            files_touched=files_touched,
            timestamp=datetime.now(),
            parallelism_level=parallelism_level,
        )
        self._history.append(record)

        # Track failure streak
        if success:
            self._failure_streak = 0
        else:
            self._failure_streak += 1

        self._save_history()

    def analyze_feature(self, feature: dict) -> FeatureAnalysis:
        """
        Analyze a single feature.

        Args:
            feature: Feature dict with id, name, category, description, steps

        Returns:
            FeatureAnalysis with predictions
        """
        name = feature.get("name", "").lower()
        category = feature.get("category", "").lower()
        description = feature.get("description", "").lower()
        steps = feature.get("steps", [])

        # Calculate complexity score (1-10)
        complexity = 1

        # Steps contribute
        complexity += min(len(steps), 5)

        # Description length
        if len(description) > 500:
            complexity += 2
        elif len(description) > 200:
            complexity += 1

        # Keywords that increase complexity
        complex_keywords = ["refactor", "migrate", "integrate", "overhaul", "redesign"]
        if any(kw in name or kw in description for kw in complex_keywords):
            complexity += 2

        complexity = min(complexity, 10)

        # Predict files based on category and name
        predicted_files = self._predict_files(name, category, description)

        # Estimate duration based on complexity and history
        duration = self._estimate_duration(category, complexity)

        # Identify risk factors
        risk_factors = []
        if any(kw in name or kw in description for kw in self.SOLO_KEYWORDS):
            risk_factors.append("Contains high-risk keywords")
        if complexity >= 7:
            risk_factors.append("High complexity")
        if "database" in category or "database" in description:
            risk_factors.append("Database changes")

        # Should run alone?
        should_solo = (
            complexity >= 8 or
            any(kw in name for kw in ["migrate", "schema", "init", "setup"]) or
            len(risk_factors) >= 2
        )

        return FeatureAnalysis(
            feature_id=feature["id"],
            name=feature["name"],
            category=category,
            complexity_score=complexity,
            predicted_files=predicted_files,
            predicted_duration_minutes=duration,
            risk_factors=risk_factors,
            can_parallelize_with=[],  # Filled in later
            should_run_alone=should_solo,
        )

    def _predict_files(
        self,
        name: str,
        category: str,
        description: str,
    ) -> list[str]:
        """Predict which files/directories a feature will touch."""
        files = []

        # Category-based predictions
        category_patterns = {
            "ui": ["src/components/", "src/pages/", "src/styles/"],
            "frontend": ["src/components/", "src/pages/", "src/lib/"],
            "component": ["src/components/"],
            "api": ["src/api/", "src/routes/", "server/"],
            "backend": ["server/", "src/api/", "src/services/"],
            "database": ["src/db/", "src/models/", "migrations/"],
            "test": ["tests/", "__tests__/", "*.test.*"],
            "auth": ["src/auth/", "src/middleware/"],
            "config": ["config/", "*.config.*"],
        }

        for cat, patterns in category_patterns.items():
            if cat in category:
                files.extend(patterns)

        # Name-based extraction
        # Convert camelCase/PascalCase to path
        name_parts = re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)', name)
        if name_parts:
            potential_path = "/".join(p.lower() for p in name_parts[:2])
            files.append(f"src/{potential_path}")

        return list(set(files))

    def _estimate_duration(self, category: str, complexity: int) -> int:
        """Estimate duration in minutes based on history and complexity."""
        # Check historical average for category
        category_history = [
            r for r in self._history
            if r.category.lower() == category.lower() and r.success
        ]

        if category_history:
            avg_duration = sum(r.duration_seconds for r in category_history) / len(category_history)
            return int(avg_duration / 60) + complexity * 2

        # Default estimate based on complexity
        return 5 + complexity * 3

    def _calculate_similarity(
        self,
        f1: FeatureAnalysis,
        f2: FeatureAnalysis,
    ) -> float:
        """
        Calculate similarity between two features (0-1).
        Higher similarity = more likely to conflict.
        """
        similarity = 0.0

        # Same category
        if f1.category == f2.category:
            similarity += 0.3

        # Conflicting categories
        cat_pair = tuple(sorted([f1.category, f2.category]))
        if cat_pair in self.CONFLICT_CATEGORIES:
            similarity += 0.5

        # File overlap
        f1_files = set(f1.predicted_files)
        f2_files = set(f2.predicted_files)
        if f1_files and f2_files:
            overlap = len(f1_files & f2_files) / len(f1_files | f2_files)
            similarity += overlap * 0.4

        # Name similarity (simple word overlap)
        f1_words = set(re.findall(r'\w+', f1.name.lower()))
        f2_words = set(re.findall(r'\w+', f2.name.lower()))
        if f1_words and f2_words:
            word_overlap = len(f1_words & f2_words) / len(f1_words | f2_words)
            similarity += word_overlap * 0.2

        return min(similarity, 1.0)

    def make_decision(
        self,
        features: list[dict],
        in_progress_count: int = 0,
    ) -> SchedulerDecision:
        """
        Make a scheduling decision.

        Args:
            features: List of pending feature dicts
            in_progress_count: Number of features currently in progress

        Returns:
            SchedulerDecision with recommendations
        """
        if not features:
            return SchedulerDecision(
                parallelism=0,
                feature_ids=[],
                reasoning="No pending features",
                confidence=1.0,
                risk_level="low",
                estimated_duration=0,
            )

        # Analyze all features
        analyses = [self.analyze_feature(f) for f in features]
        analyses.sort(key=lambda a: (a.should_run_alone, -a.complexity_score))

        # Track warnings
        warnings = []

        # Adjust max based on failure streak
        effective_max = self.max_parallel
        if self._failure_streak >= 3:
            effective_max = max(1, effective_max - 2)
            warnings.append(f"Reduced parallelism due to {self._failure_streak} recent failures")
        elif self._failure_streak >= 1:
            effective_max = max(1, effective_max - 1)

        # Account for already running
        available_slots = max(0, effective_max - in_progress_count)
        if available_slots == 0:
            return SchedulerDecision(
                parallelism=0,
                feature_ids=[],
                reasoning=f"Maximum parallelism reached ({in_progress_count} in progress)",
                confidence=1.0,
                risk_level="low",
                estimated_duration=0,
                warnings=warnings,
            )

        # Find features that should run alone
        solo_features = [a for a in analyses if a.should_run_alone]
        if solo_features and in_progress_count == 0:
            # Run solo feature
            feature = solo_features[0]
            return SchedulerDecision(
                parallelism=1,
                feature_ids=[feature.feature_id],
                reasoning=f"Running solo: {', '.join(feature.risk_factors)}",
                confidence=0.9,
                risk_level="medium",
                estimated_duration=feature.predicted_duration_minutes,
                warnings=warnings + feature.risk_factors,
            )

        # Cluster compatible features
        safe_features = [a for a in analyses if not a.should_run_alone]
        if not safe_features:
            # All features are risky, pick lowest complexity
            feature = min(analyses, key=lambda a: a.complexity_score)
            return SchedulerDecision(
                parallelism=1,
                feature_ids=[feature.feature_id],
                reasoning="All features have risk factors, running lowest complexity",
                confidence=0.7,
                risk_level="high",
                estimated_duration=feature.predicted_duration_minutes,
                warnings=warnings,
            )

        # Build clusters of non-conflicting features
        selected = []
        for analysis in safe_features:
            if len(selected) >= available_slots:
                break

            # Check conflict with already selected
            conflicts = False
            for existing in selected:
                similarity = self._calculate_similarity(analysis, existing)
                if similarity > 0.5:  # Conflict threshold
                    conflicts = True
                    break

            if not conflicts:
                selected.append(analysis)

        # Ensure minimum
        if len(selected) < self.min_parallel and safe_features:
            remaining = [f for f in safe_features if f not in selected]
            while len(selected) < self.min_parallel and remaining:
                selected.append(remaining.pop(0))

        # Calculate confidence and risk
        avg_complexity = sum(a.complexity_score for a in selected) / len(selected) if selected else 0
        risk_level = "low" if avg_complexity < 4 else "medium" if avg_complexity < 7 else "high"
        confidence = max(0.5, 1.0 - (avg_complexity / 20) - (len(selected) / 10))

        total_duration = max(a.predicted_duration_minutes for a in selected) if selected else 0

        # Build reasoning
        categories = set(a.category for a in selected)
        reasoning = f"Running {len(selected)} features from categories: {', '.join(categories)}"

        return SchedulerDecision(
            parallelism=len(selected),
            feature_ids=[a.feature_id for a in selected],
            reasoning=reasoning,
            confidence=confidence,
            risk_level=risk_level,
            estimated_duration=total_duration,
            warnings=warnings,
        )

    def get_stats(self) -> dict:
        """Get scheduler statistics."""
        if not self._history:
            return {
                "total_runs": 0,
                "success_rate": 0,
                "avg_duration_seconds": 0,
                "failure_streak": self._failure_streak,
            }

        successes = sum(1 for r in self._history if r.success)
        avg_duration = sum(r.duration_seconds for r in self._history) / len(self._history)

        return {
            "total_runs": len(self._history),
            "success_rate": successes / len(self._history),
            "avg_duration_seconds": avg_duration,
            "failure_streak": self._failure_streak,
            "categories_seen": list(set(r.category for r in self._history)),
        }


Now let me create an API router for the smart scheduler so it can be controlled from the UI.


Now let me add a router for smart scheduling control.


"""
Scheduler Router
================

REST API for the smart scheduler - analyze features and get
parallelization recommendations.
"""

import logging
from pathlib import Path
from typing import Optional

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

from ..services.smart_scheduler import SmartScheduler, SchedulerDecision

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/scheduler", tags=["scheduler"])

# Cache schedulers by project
_schedulers: dict[str, SmartScheduler] = {}

def _get_project_path(project_name: str) -> Path:
    """Get project path from registry."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import get_project_path
    return get_project_path(project_name)

def _get_scheduler(project_name: str) -> SmartScheduler:
    """Get or create scheduler for project."""
    if project_name not in _schedulers:
        project_dir = _get_project_path(project_name)
        if not project_dir:
            raise HTTPException(status_code=404, detail="Project not found")
        _schedulers[project_name] = SmartScheduler(project_dir)
    return _schedulers[project_name]

# ============================================================================
# Request/Response Models
# ============================================================================

class FeatureInput(BaseModel):
    """Feature for analysis."""
    id: int
    name: str
    category: str
    description: str = ""
    steps: list[str] = []

class AnalyzeRequest(BaseModel):
    """Request to analyze features."""
    features: list[FeatureInput]
    in_progress_count: int = 0

class DecisionResponse(BaseModel):
    """Scheduling decision response."""
    parallelism: int
    feature_ids: list[int]
    reasoning: str
    confidence: float
    risk_level: str
    estimated_duration_minutes: int
    warnings: list[str]

class FeatureAnalysisResponse(BaseModel):
    """Individual feature analysis."""
    feature_id: int
    name: str
    complexity_score: int
    predicted_files: list[str]
    predicted_duration_minutes: int
    risk_factors: list[str]
    should_run_alone: bool

class StatsResponse(BaseModel):
    """Scheduler statistics."""
    total_runs: int
    success_rate: float
    avg_duration_seconds: float
    failure_streak: int
    categories_seen: list[str] = []

class RecordResultRequest(BaseModel):
    """Record execution result."""
    feature_id: int
    feature_name: str
    category: str
    success: bool
    duration_seconds: float
    files_touched: list[str] = []
    parallelism_level: int = 1

# ============================================================================
# Endpoints
# ============================================================================

@router.post("/projects/{project_name}/analyze", response_model=DecisionResponse)
async def analyze_features(project_name: str, request: AnalyzeRequest):
    """
    Analyze features and get parallelization recommendation.

    This endpoint uses the smart scheduler to:
    1. Analyze feature complexity
    2. Predict file conflicts
    3. Consider historical performance
    4. Return optimal parallelism recommendation
    """
    scheduler = _get_scheduler(project_name)

    features = [f.model_dump() for f in request.features]
    decision = scheduler.make_decision(features, request.in_progress_count)

    return DecisionResponse(
        parallelism=decision.parallelism,
        feature_ids=decision.feature_ids,
        reasoning=decision.reasoning,
        confidence=decision.confidence,
        risk_level=decision.risk_level,
        estimated_duration_minutes=decision.estimated_duration,
        warnings=decision.warnings,
    )

@router.post("/projects/{project_name}/analyze-feature", response_model=FeatureAnalysisResponse)
async def analyze_single_feature(project_name: str, feature: FeatureInput):
    """Analyze a single feature."""
    scheduler = _get_scheduler(project_name)

    analysis = scheduler.analyze_feature(feature.model_dump())

    return FeatureAnalysisResponse(
        feature_id=analysis.feature_id,
        name=analysis.name,
        complexity_score=analysis.complexity_score,
        predicted_files=analysis.predicted_files,
        predicted_duration_minutes=analysis.predicted_duration_minutes,
        risk_factors=analysis.risk_factors,
        should_run_alone=analysis.should_run_alone,
    )

@router.post("/projects/{project_name}/record-result")
async def record_result(project_name: str, request: RecordResultRequest):
    """Record a feature execution result for learning."""
    scheduler = _get_scheduler(project_name)

    scheduler.record_result(
        feature_id=request.feature_id,
        feature_name=request.feature_name,
        category=request.category,
        success=request.success,
        duration_seconds=request.duration_seconds,
        files_touched=request.files_touched,
        parallelism_level=request.parallelism_level,
    )

    return {"status": "recorded"}

@router.get("/projects/{project_name}/stats", response_model=StatsResponse)
async def get_stats(project_name: str):
    """Get scheduler statistics and learning data."""
    scheduler = _get_scheduler(project_name)
    stats = scheduler.get_stats()

    return StatsResponse(
        total_runs=stats["total_runs"],
        success_rate=stats["success_rate"],
        avg_duration_seconds=stats["avg_duration_seconds"],
        failure_streak=stats["failure_streak"],
        categories_seen=stats.get("categories_seen", []),
    )

@router.post("/projects/{project_name}/reset")
async def reset_scheduler(project_name: str):
    """Reset scheduler history and learning data."""
    if project_name in _schedulers:
        del _schedulers[project_name]

    return {"status": "reset"}


File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\scheduler.py

"""
Scheduler Router
================

REST API for the smart scheduler - analyze features and get
parallelization recommendations.
"""

import logging
from pathlib import Path
from typing import Optional

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

from ..services.smart_scheduler import SmartScheduler, SchedulerDecision

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/scheduler", tags=["scheduler"])

# Cache schedulers by project
_schedulers: dict[str, SmartScheduler] = {}

def _get_project_path(project_name: str) -> Path:
    """Get project path from registry."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import get_project_path
    return get_project_path(project_name)

def _get_scheduler(project_name: str) -> SmartScheduler:
    """Get or create scheduler for project."""
    if project_name not in _schedulers:
        project_dir = _get_project_path(project_name)
        if not project_dir:
            raise HTTPException(status_code=404, detail="Project not found")
        _schedulers[project_name] = SmartScheduler(project_dir)
    return _schedulers[project_name]

# ============================================================================
# Request/Response Models
# ============================================================================

class FeatureInput(BaseModel):
    """Feature for analysis."""
    id: int
    name: str
    category: str
    description: str = ""
    steps: list[str] = []

class AnalyzeRequest(BaseModel):
    """Request to analyze features."""
    features: list[FeatureInput]
    in_progress_count: int = 0

class DecisionResponse(BaseModel):
    """Scheduling decision response."""
    parallelism: int
    feature_ids: list[int]
    reasoning: str
    confidence: float
    risk_level: str
    estimated_duration_minutes: int
    warnings: list[str]

class FeatureAnalysisResponse(BaseModel):
    """Individual feature analysis."""
    feature_id: int
    name: str
    complexity_score: int
    predicted_files: list[str]
    predicted_duration_minutes: int
    risk_factors: list[str]
    should_run_alone: bool

class StatsResponse(BaseModel):
    """Scheduler statistics."""
    total_runs: int
    success_rate: float
    avg_duration_seconds: float
    failure_streak: int
    categories_seen: list[str] = []

class RecordResultRequest(BaseModel):
    """Record execution result."""
    feature_id: int
    feature_name: str
    category: str
    success: bool
    duration_seconds: float
    files_touched: list[str] = []
    parallelism_level: int = 1

# ============================================================================
# Endpoints
# ============================================================================

@router.post("/projects/{project_name}/analyze", response_model=DecisionResponse)
async def analyze_features(project_name: str, request: AnalyzeRequest):
    """
    Analyze features and get parallelization recommendation.

    This endpoint uses the smart scheduler to:
    1. Analyze feature complexity
    2. Predict file conflicts
    3. Consider historical performance
    4. Return optimal parallelism recommendation
    """
    scheduler = _get_scheduler(project_name)

    features = [f.model_dump() for f in request.features]
    decision = scheduler.make_decision(features, request.in_progress_count)

    return DecisionResponse(
        parallelism=decision.parallelism,
        feature_ids=decision.feature_ids,
        reasoning=decision.reasoning,
        confidence=decision.confidence,
        risk_level=decision.risk_level,
        estimated_duration_minutes=decision.estimated_duration,
        warnings=decision.warnings,
    )

@router.post("/projects/{project_name}/analyze-feature", response_model=FeatureAnalysisResponse)
async def analyze_single_feature(project_name: str, feature: FeatureInput):
    """Analyze a single feature."""
    scheduler = _get_scheduler(project_name)

    analysis = scheduler.analyze_feature(feature.model_dump())

    return FeatureAnalysisResponse(
        feature_id=analysis.feature_id,
        name=analysis.name,
        complexity_score=analysis.complexity_score,
        predicted_files=analysis.predicted_files,
        predicted_duration_minutes=analysis.predicted_duration_minutes,
        risk_factors=analysis.risk_factors,
        should_run_alone=analysis.should_run_alone,
    )

@router.post("/projects/{project_name}/record-result")
async def record_result(project_name: str, request: RecordResultRequest):
    """Record a feature execution result for learning."""
    scheduler = _get_scheduler(project_name)

    scheduler.record_result(
        feature_id=request.feature_id,
        feature_name=request.feature_name,
        category=request.category,
        success=request.success,
        duration_seconds=request.duration_seconds,
        files_touched=request.files_touched,
        parallelism_level=request.parallelism_level,
    )

    return {"status": "recorded"}

@router.get("/projects/{project_name}/stats", response_model=StatsResponse)
async def get_stats(project_name: str):
    """Get scheduler statistics and learning data."""
    scheduler = _get_scheduler(project_name)
    stats = scheduler.get_stats()

    return StatsResponse(
        total_runs=stats["total_runs"],
        success_rate=stats["success_rate"],
        avg_duration_seconds=stats["avg_duration_seconds"],
        failure_streak=stats["failure_streak"],
        categories_seen=stats.get("categories_seen", []),
    )

@router.post("/projects/{project_name}/reset")
async def reset_scheduler(project_name: str):
    """Reset scheduler history and learning data."""
    if project_name in _schedulers:
        del _schedulers[project_name]

    return {"status": "reset"}


Now let me register the scheduler router in main.py and update the routers __init__.py.


Now let me register the scheduler router.


     1→"""
     2→API Routers
     3→===========
     4→
     5→FastAPI routers for different API endpoints.
     6→"""
     7→
     8→from .agent import router as agent_router
     9→from .assistant_chat import router as assistant_chat_router
    10→from .features import router as features_router
    11→from .filesystem import router as filesystem_router
    12→from .projects import router as projects_router
    13→from .spec_creation import router as spec_creation_router
    14→from .terminal import router as terminal_router
    15→from .git import router as git_router
    16→
    17→__all__ = [
    18→    "projects_router",
    19→    "features_router",
    20→    "agent_router",
    21→    "spec_creation_router",
    22→    "filesystem_router",
    23→    "assistant_chat_router",
    24→    "terminal_router",
    25→    "git_router",
    26→]
    27→

<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>

"""
API Routers
===========

FastAPI routers for different API endpoints.
"""

from .agent import router as agent_router
from .assistant_chat import router as assistant_chat_router
from .features import router as features_router
from .filesystem import router as filesystem_router
from .projects import router as projects_router
from .spec_creation import router as spec_creation_router
from .terminal import router as terminal_router
from .git import router as git_router

__all__ = [
    "projects_router",
    "features_router",
    "agent_router",
    "spec_creation_router",
    "filesystem_router",
    "assistant_chat_router",
    "terminal_router",
    "git_router",
]


Let me update the routers init and main.py to include the scheduler router.


Edit

from .terminal import router as terminal_router
from .git import router as git_router

__all__ = [
    "projects_router",
    "features_router",
    "agent_router",
    "spec_creation_router",
    "filesystem_router",
    "assistant_chat_router",
    "terminal_router",
    "git_router",
]

from .terminal import router as terminal_router
from .git import router as git_router
from .scheduler import router as scheduler_router

__all__ = [
    "projects_router",
    "features_router",
    "agent_router",
    "spec_creation_router",
    "filesystem_router",
    "assistant_chat_router",
    "terminal_router",
    "git_router",
    "scheduler_router",
]


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

from .terminal import router as terminal_router
from .git import router as git_router
from .scheduler import router as scheduler_router

__all__ = [
    "projects_router",
    "features_router",
    "agent_router",
    "spec_creation_router",
    "filesystem_router",
    "assistant_chat_router",
    "terminal_router",
    "git_router",
    "scheduler_router",
]

"""
API Routers
===========

FastAPI routers for different API endpoints.
"""

from .agent import router as agent_router
from .assistant_chat import router as assistant_chat_router
from .features import router as features_router
from .filesystem import router as filesystem_router
from .projects import router as projects_router
from .spec_creation import router as spec_creation_router
from .terminal import router as terminal_router
from .git import router as git_router

__all__ = [
    "projects_router",
    "features_router",
    "agent_router",
    "spec_creation_router",
    "filesystem_router",
    "assistant_chat_router",
    "terminal_router",
    "git_router",
]

+from .scheduler import router as scheduler_router


Now let me update main.py to include the scheduler router.


from .routers import (
    agent_router,
    assistant_chat_router,
    features_router,
    filesystem_router,
    projects_router,
    spec_creation_router,
    terminal_router,
    git_router,
)

from .routers import (
    agent_router,
    assistant_chat_router,
    features_router,
    filesystem_router,
    projects_router,
    spec_creation_router,
    terminal_router,
    git_router,
    scheduler_router,
)


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

from .routers import (
    agent_router,
    assistant_chat_router,
    features_router,
    filesystem_router,
    projects_router,
    spec_creation_router,
    terminal_router,
    git_router,
    scheduler_router,
)

"""
FastAPI Main Application
========================

Main entry point for the Autonomous Coding UI server.
Provides REST API, WebSocket, and static file serving.
"""

import shutil
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI, HTTPException, Request, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

from .routers import (
    agent_router,
    assistant_chat_router,
    features_router,
    filesystem_router,
    projects_router,
    spec_creation_router,
    terminal_router,
    git_router,
)
from .schemas import SetupStatus
from .services.assistant_chat_session import cleanup_all_sessions as cleanup_assistant_sessions
from .services.process_manager import cleanup_all_managers
from .websocket import project_websocket

# Paths
ROOT_DIR = Path(__file__).parent.parent
UI_DIST_DIR = ROOT_DIR / "ui" / "dist"

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifespan context manager for startup and shutdown."""
    # Startup
    yield
    # Shutdown - cleanup all running agents and assistant sessions
    await cleanup_all_managers()
    await cleanup_assistant_sessions()

# Create FastAPI app
app = FastAPI(
    title="Autonomous Coding UI",
    description="Web UI for the Autonomous Coding Agent",
    version="1.0.0",
    lifespan=lifespan,
)

# CORS - allow only localhost origins for security
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",      # Vite dev server
        "http://127.0.0.1:5173",
        "http://localhost:8888",      # Production
        "http://127.0.0.1:8888",
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ============================================================================
# Security Middleware
# ============================================================================

@app.middleware("http")
async def require_localhost(request: Request, call_next):
    """Only allow requests from localhost."""
    client_host = request.client.host if request.client else None

    # Allow localhost connections
    if client_host not in ("127.0.0.1", "::1", "localhost", None):
        raise HTTPException(status_code=403, detail="Localhost access only")

    return await call_next(request)

# ============================================================================
# Include Routers
# ============================================================================

app.include_router(projects_router)
app.include_router(features_router)
app.include_router(agent_router)
app.include_router(spec_creation_router)
app.include_router(filesystem_router)
app.include_router(assistant_chat_router)
app.include_router(terminal_router)
app.include_router(git_router)

# ============================================================================
# WebSocket Endpoint
# ============================================================================

@app.websocket("/ws/projects/{project_name}")
async def websocket_endpoint(websocket: WebSocket, project_name: str):
    """WebSocket endpoint for real-time project updates."""
    await project_websocket(websocket, project_name)

# ============================================================================
# Setup & Health Endpoints
# ============================================================================

@app.get("/api/health")
async def health_check():
    """Health check endpoint."""
    return {"status": "healthy"}

@app.get("/api/setup/status", response_model=SetupStatus)
async def setup_status():
    """Check system setup status."""
    # Check for Claude CLI
    claude_cli = shutil.which("claude") is not None

    # Check for credentials file
    credentials_path = Path.home() / ".claude" / ".credentials.json"
    credentials = credentials_path.exists()

    # Check for Node.js and npm
    node = shutil.which("node") is not None
    npm = shutil.which("npm") is not None

    return SetupStatus(
        claude_cli=claude_cli,
        credentials=credentials,
        node=node,
        npm=npm,
    )

# ============================================================================
# Static File Serving (Production)
# ============================================================================

# Serve React build files if they exist
if UI_DIST_DIR.exists():
    # Mount static assets
    app.mount("/assets", StaticFiles(directory=UI_DIST_DIR / "assets"), name="assets")

    @app.get("/")
    async def serve_index():
        """Serve the React app index.html."""
        return FileResponse(UI_DIST_DIR / "index.html")

    @app.get("/{path:path}")
    async def serve_spa(path: str):
        """
        Serve static files or fall back to index.html for SPA routing.
        """
        # Check if the path is an API route (shouldn't hit this due to router ordering)
        if path.startswith("api/") or path.startswith("ws/"):
            raise HTTPException(status_code=404)

        # Try to serve the file directly
        file_path = UI_DIST_DIR / path
        if file_path.exists() and file_path.is_file():
            return FileResponse(file_path)

        # Fall back to index.html for SPA routing
        return FileResponse(UI_DIST_DIR / "index.html")

# ============================================================================
# Main Entry Point
# ============================================================================

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "server.main:app",
        host="127.0.0.1",  # Localhost only for security
        port=8888,
        reload=True,
    )


"""
FastAPI Main Application
========================

Main entry point for the Autonomous Coding UI server.
Provides REST API, WebSocket, and static file serving.
"""

import shutil
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI, HTTPException, Request, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

from .routers import (
    agent_router,
    assistant_chat_router,
    features_router,
    filesystem_router,
    projects_router,
    spec_creation_router,
    terminal_router,
    git_router,
    scheduler_router,
)
from .schemas import SetupStatus
from .services.assistant_chat_session import cleanup_all_sessions as cleanup_assistant_sessions
from .services.process_manager import cleanup_all_managers
from .websocket import project_websocket

# Paths
ROOT_DIR = Path(__file__).parent.parent
UI_DIST_DIR = ROOT_DIR / "ui" / "dist"

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifespan context manager for startup and shutdown."""
    # Startup
    yield
    # Shutdown - cleanup all running agents and assistant sessions
    await cleanup_all_managers()
    await cleanup_assistant_sessions()

# Create FastAPI app
app = FastAPI(
    title="Autonomous Coding UI",
    description="Web UI for the Autonomous Coding Agent",
    version="1.0.0",
    lifespan=lifespan,
)

# CORS - allow only localhost origins for security
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",      # Vite dev server
        "http://127.0.0.1:5173",
        "http://localhost:8888",      # Production
        "http://127.0.0.1:8888",
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ============================================================================
# Security Middleware
# ============================================================================

@app.middleware("http")
async def require_localhost(request: Request, call_next):
    """Only allow requests from localhost."""
    client_host = request.client.host if request.client else None

    # Allow localhost connections
    if client_host not in ("127.0.0.1", "::1", "localhost", None):
        raise HTTPException(status_code=403, detail="Localhost access only")

    return await call_next(request)

# ============================================================================
# Include Routers
# ============================================================================

app.include_router(projects_router)
app.include_router(features_router)
app.include_router(agent_router)
app.include_router(spec_creation_router)
app.include_router(filesystem_router)
app.include_router(assistant_chat_router)
app.include_router(terminal_router)
app.include_router(git_router)

# ============================================================================
# WebSocket Endpoint
# ============================================================================

@app.websocket("/ws/projects/{project_name}")
async def websocket_endpoint(websocket: WebSocket, project_name: str):
    """WebSocket endpoint for real-time project updates."""
    await project_websocket(websocket, project_name)

# ============================================================================
# Setup & Health Endpoints
# ============================================================================

@app.get("/api/health")
async def health_check():
    """Health check endpoint."""
    return {"status": "healthy"}

@app.get("/api/setup/status", response_model=SetupStatus)
async def setup_status():
    """Check system setup status."""
    # Check for Claude CLI
    claude_cli = shutil.which("claude") is not None

    # Check for credentials file
    credentials_path = Path.home() / ".claude" / ".credentials.json"
    credentials = credentials_path.exists()

    # Check for Node.js and npm
    node = shutil.which("node") is not None
    npm = shutil.which("npm") is not None

    return SetupStatus(
        claude_cli=claude_cli,
        credentials=credentials,
        node=node,
        npm=npm,
    )

# ============================================================================
# Static File Serving (Production)
# ============================================================================

# Serve React build files if they exist
if UI_DIST_DIR.exists():
    # Mount static assets
    app.mount("/assets", StaticFiles(directory=UI_DIST_DIR / "assets"), name="assets")

    @app.get("/")
    async def serve_index():
        """Serve the React app index.html."""
        return FileResponse(UI_DIST_DIR / "index.html")

    @app.get("/{path:path}")
    async def serve_spa(path: str):
        """
        Serve static files or fall back to index.html for SPA routing.
        """
        # Check if the path is an API route (shouldn't hit this due to router ordering)
        if path.startswith("api/") or path.startswith("ws/"):
            raise HTTPException(status_code=404)

        # Try to serve the file directly
        file_path = UI_DIST_DIR / path
        if file_path.exists() and file_path.is_file():
            return FileResponse(file_path)

        # Fall back to index.html for SPA routing
        return FileResponse(UI_DIST_DIR / "index.html")

# ============================================================================
# Main Entry Point
# ============================================================================

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "server.main:app",
        host="127.0.0.1",  # Localhost only for security
        port=8888,
        reload=True,
    )


Now let me update the services __init__.py to export the smart scheduler.


from .file_lock import (
    FileLockManager,
    LockInfo,
    LockResult,
    get_lock_manager,
    reset_lock_manager,
)

__all__ = [

from .file_lock import (
    FileLockManager,
    LockInfo,
    LockResult,
    get_lock_manager,
    reset_lock_manager,
)
from .smart_scheduler import (
    SmartScheduler,
    FeatureAnalysis,
    SchedulerDecision,
)

__all__ = [


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

from .file_lock import (
    FileLockManager,
    LockInfo,
    LockResult,
    get_lock_manager,
    reset_lock_manager,
)
from .smart_scheduler import (
    SmartScheduler,
    FeatureAnalysis,
    SchedulerDecision,
)

__all__ = [

"""
Backend Services
================

Business logic and process management services.
"""

from .process_manager import AgentProcessManager
from .credential_resolver import CredentialResolver, get_resolver
from .resource_manager import (
    ResourceManager,
    Resource,
    ResourceScope,
    ResourceType,
    AuthType,
    ResourceAuth,
    VerificationResult,
    get_manager,
)
from .feature_graph import (
    FeatureGraph,
    ParallelFeature,
    FeatureConflict,
)
from .file_lock import (
    FileLockManager,
    LockInfo,
    LockResult,
    get_lock_manager,
    reset_lock_manager,
)

__all__ = [
    "AgentProcessManager",
    "CredentialResolver",
    "get_resolver",
    "ResourceManager",
    "Resource",
    "ResourceScope",
    "ResourceType",
    "AuthType",
    "ResourceAuth",
    "VerificationResult",
    "get_manager",
    # Feature graph
    "FeatureGraph",
    "ParallelFeature",
    "FeatureConflict",
    # File locking
    "FileLockManager",
    "LockInfo",
    "LockResult",
    "get_lock_manager",
    "reset_lock_manager",
]

get_lock_manager,
reset_lock_manager,
)
+from .smart_scheduler import (
+    SmartScheduler,
+    FeatureAnalysis,
+    SchedulerDecision,
+)
__all__ = [
"AgentProcessManager",


Now let me update the __all__ list to include the smart scheduler exports.


# File locking
    "FileLockManager",
    "LockInfo",
    "LockResult",
    "get_lock_manager",
    "reset_lock_manager",
]

# File locking
    "FileLockManager",
    "LockInfo",
    "LockResult",
    "get_lock_manager",
    "reset_lock_manager",
    # Smart scheduler
    "SmartScheduler",
    "FeatureAnalysis",
    "SchedulerDecision",
]


# File locking
    "FileLockManager",
    "LockInfo",
    "LockResult",
    "get_lock_manager",
    "reset_lock_manager",
    # Smart scheduler
    "SmartScheduler",
    "FeatureAnalysis",
    "SchedulerDecision",
]

"""
Backend Services
================

Business logic and process management services.
"""

from .process_manager import AgentProcessManager
from .credential_resolver import CredentialResolver, get_resolver
from .resource_manager import (
    ResourceManager,
    Resource,
    ResourceScope,
    ResourceType,
    AuthType,
    ResourceAuth,
    VerificationResult,
    get_manager,
)
from .feature_graph import (
    FeatureGraph,
    ParallelFeature,
    FeatureConflict,
)
from .file_lock import (
    FileLockManager,
    LockInfo,
    LockResult,
    get_lock_manager,
    reset_lock_manager,
)
from .smart_scheduler import (
    SmartScheduler,
    FeatureAnalysis,
    SchedulerDecision,
)

__all__ = [
    "AgentProcessManager",
    "CredentialResolver",
    "get_resolver",
    "ResourceManager",
    "Resource",
    "ResourceScope",
    "ResourceType",
    "AuthType",
    "ResourceAuth",
    "VerificationResult",
    "get_manager",
    # Feature graph
    "FeatureGraph",
    "ParallelFeature",
    "FeatureConflict",
    # File locking
    "FileLockManager",
    "LockInfo",
    "LockResult",
    "get_lock_manager",
    "reset_lock_manager",
]

"LockResult",
"get_lock_manager",
"reset_lock_manager",
+    # Smart scheduler
+    "SmartScheduler",
+    "FeatureAnalysis",
+    "SchedulerDecision",
]


Now let me update the smart_orchestrator.py to use the SmartScheduler instead of calling Claude directly for each decision. This way it uses the heuristic-based scheduler for quick decisions and only calls Claude for complex cases.


Now let me update the smart_orchestrator to use the SmartScheduler.


from server.services.feature_graph import FeatureGraph, ParallelFeature
from server.services.file_lock import FileLockManager, get_lock_manager
from server.agents.git_agent import GitAgent, GitAgentConfig

from server.services.feature_graph import FeatureGraph, ParallelFeature
from server.services.file_lock import FileLockManager, get_lock_manager
from server.services.smart_scheduler import SmartScheduler
from server.agents.git_agent import GitAgent, GitAgentConfig


<tool_use_error>String to replace not found in file.
String: from server.services.feature_graph import FeatureGraph, ParallelFeature
from server.services.file_lock import FileLockManager, get_lock_manager
from server.agents.git_agent import GitAgent, GitAgentConfig</tool_use_error>

Error: String to replace not found in file.
String: from server.services.feature_graph import FeatureGraph, ParallelFeature
from server.services.file_lock import FileLockManager, get_lock_manager
from server.agents.git_agent import GitAgent, GitAgentConfig


Let me read the file to see the current state.


     1→"""
     2→Smart Orchestrator
     3→==================
     4→
     5→Uses Claude to dynamically decide how many agents to run in parallel
     6→based on feature analysis, complexity estimation, and conflict prediction.
     7→"""
     8→
     9→import asyncio
    10→import json
    11→import logging
    12→from dataclasses import dataclass, field
    13→from pathlib import Path
    14→from typing import Optional
    15→
    16→from claude_agent_sdk import ClaudeSDKClient
    17→
    18→from parallel_agent import ParallelAgentRunner, AgentResult
    19→from server.services.feature_graph import FeatureGraph, ParallelFeature
    20→from progress import get_feature_stats
    21→
    22→logger = logging.getLogger(__name__)
    23→
    24→
    25→@dataclass
    26→class FeatureCluster:
    27→    """A group of features that can safely run together."""
    28→    features: list[ParallelFeature]
    29→    estimated_parallelism: int
    30→    reasoning: str
    31→    predicted_conflicts: list[str] = field(default_factory=list)
    32→    total_complexity: int = 0
    33→
    34→
    35→@dataclass
    36→class OrchestrationDecision:
    37→    """Decision about how to run features."""
    38→    recommended_parallelism: int
    39→    features_to_run: list[ParallelFeature]
    40→    reasoning: str
    41→    risk_level: str  # "low", "medium", "high"
    42→    estimated_duration_minutes: int = 0
    43→
    44→
    45→ANALYSIS_PROMPT = """You are an expert software architect analyzing features for parallel execution.
    46→
    47→Given these pending features, analyze them and decide:
    48→1. How many can safely run in parallel
    49→2. Which specific features to run together
    50→3. Risk assessment for parallel execution
    51→
    52→Consider:
    53→- Features touching similar files/directories should NOT run together
    54→- UI features can often parallelize with backend features
    55→- Database/schema changes should run alone
    56→- Test features can parallelize with implementation features
    57→- Simple features (1-2 steps) are safer to parallelize than complex ones
    58→
    59→## Pending Features
    60→

<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>

"""
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


I see - the file doesn't have the same imports I expected. Let me just rewrite the SmartOrchestrator class to use the SmartScheduler properly. I'll update it to use the heuristic-based scheduler for quick decisions and only call Claude for uncertain cases.


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

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