""" DevLoop Orchestrator - Master controller for closed-loop development. Coordinates: - Gemini Pro: Strategic planning, analysis, test design - Claude Code: Implementation, code changes, fixes - Dashboard: Automated testing, verification - LAC: Learning capture and retrieval """ import asyncio import json from dataclasses import dataclass, field from datetime import datetime from enum import Enum from pathlib import Path from typing import Any, Dict, List, Optional, Callable import logging logger = logging.getLogger(__name__) class LoopPhase(Enum): """Current phase in the development loop.""" IDLE = "idle" PLANNING = "planning" IMPLEMENTING = "implementing" TESTING = "testing" ANALYZING = "analyzing" FIXING = "fixing" VERIFYING = "verifying" @dataclass class LoopState: """Current state of the development loop.""" phase: LoopPhase = LoopPhase.IDLE iteration: int = 0 current_task: Optional[str] = None test_results: Optional[Dict] = None analysis: Optional[Dict] = None last_update: str = field(default_factory=lambda: datetime.now().isoformat()) @dataclass class IterationResult: """Result of a single development iteration.""" iteration: int plan: Optional[Dict] = None implementation: Optional[Dict] = None test_results: Optional[Dict] = None analysis: Optional[Dict] = None fixes: Optional[List[Dict]] = None verification: Optional[Dict] = None success: bool = False duration_seconds: float = 0.0 @dataclass class CycleReport: """Complete report for a development cycle.""" objective: str start_time: str = field(default_factory=lambda: datetime.now().isoformat()) end_time: Optional[str] = None iterations: List[IterationResult] = field(default_factory=list) status: str = "in_progress" total_duration_seconds: float = 0.0 class DevLoopOrchestrator: """ Autonomous development loop orchestrator. Coordinates Gemini (planning) + Claude Code (implementation) + Dashboard (testing) in a continuous improvement cycle. Flow: 1. Gemini: Plan features/fixes 2. Claude Code: Implement 3. Dashboard: Test 4. Gemini: Analyze results 5. Claude Code: Fix issues 6. Dashboard: Verify 7. Loop back with learnings """ def __init__( self, config: Optional[Dict] = None, gemini_client: Optional[Any] = None, claude_bridge: Optional[Any] = None, dashboard_runner: Optional[Any] = None, ): """ Initialize the orchestrator. Args: config: Configuration dict with API keys and settings gemini_client: Pre-configured Gemini client (optional) claude_bridge: Pre-configured Claude Code bridge (optional) dashboard_runner: Pre-configured Dashboard test runner (optional) """ self.config = config or self._default_config() self.state = LoopState() self.subscribers: List[Callable] = [] # Initialize components lazily self._gemini = gemini_client self._claude_bridge = claude_bridge self._dashboard = dashboard_runner self._lac = None # History for learning self.cycle_history: List[CycleReport] = [] def _default_config(self) -> Dict: """Default configuration.""" return { "max_iterations": 10, "auto_fix_threshold": "high", # Only auto-fix high+ severity "learning_enabled": True, "dashboard_url": "http://localhost:3000", "websocket_url": "ws://localhost:8000", "test_timeout_ms": 30000, } @property def gemini(self): """Lazy-load Gemini planner.""" if self._gemini is None: from .planning import GeminiPlanner self._gemini = GeminiPlanner(self.config.get("gemini", {})) return self._gemini @property def claude_bridge(self): """Lazy-load Claude Code bridge.""" if self._claude_bridge is None: from .claude_bridge import ClaudeCodeBridge self._claude_bridge = ClaudeCodeBridge(self.config.get("claude", {})) return self._claude_bridge @property def dashboard(self): """Lazy-load Dashboard test runner.""" if self._dashboard is None: from .test_runner import DashboardTestRunner self._dashboard = DashboardTestRunner(self.config) return self._dashboard @property def lac(self): """Lazy-load LAC (Learning Atomizer Core).""" if self._lac is None and self.config.get("learning_enabled", True): try: from knowledge_base.lac import get_lac self._lac = get_lac() except ImportError: logger.warning("LAC not available, learning disabled") return self._lac def subscribe(self, callback: Callable[[LoopState], None]): """Subscribe to state updates.""" self.subscribers.append(callback) def unsubscribe(self, callback: Callable): """Unsubscribe from state updates.""" if callback in self.subscribers: self.subscribers.remove(callback) def _notify_subscribers(self): """Notify all subscribers of state change.""" self.state.last_update = datetime.now().isoformat() for callback in self.subscribers: try: callback(self.state) except Exception as e: logger.error(f"Subscriber error: {e}") def _update_state(self, phase: Optional[LoopPhase] = None, task: Optional[str] = None): """Update state and notify subscribers.""" if phase: self.state.phase = phase if task: self.state.current_task = task self._notify_subscribers() async def run_development_cycle( self, objective: str, context: Optional[Dict] = None, max_iterations: Optional[int] = None, ) -> CycleReport: """ Execute a complete development cycle. Args: objective: What to achieve (e.g., "Create support_arm optimization study") context: Additional context (study spec, problem statement, etc.) max_iterations: Override default max iterations Returns: CycleReport with all iteration results """ max_iter = max_iterations or self.config.get("max_iterations", 10) report = CycleReport(objective=objective) start_time = datetime.now() logger.info(f"Starting development cycle: {objective}") try: while not self._is_objective_complete(report) and len(report.iterations) < max_iter: iteration_result = await self._run_iteration(objective, context) report.iterations.append(iteration_result) # Record learning from successful patterns if iteration_result.success and self.lac: await self._record_learning(iteration_result) # Check for max iterations if len(report.iterations) >= max_iter: report.status = "max_iterations_reached" logger.warning(f"Max iterations ({max_iter}) reached") break except Exception as e: report.status = f"error: {str(e)}" logger.error(f"Development cycle error: {e}") report.end_time = datetime.now().isoformat() report.total_duration_seconds = (datetime.now() - start_time).total_seconds() if report.status == "in_progress": report.status = "completed" self.cycle_history.append(report) self._update_state(LoopPhase.IDLE) return report def _is_objective_complete(self, report: CycleReport) -> bool: """Check if the objective has been achieved.""" if not report.iterations: return False last_iter = report.iterations[-1] # Success if last iteration passed all tests if last_iter.success and last_iter.test_results: tests = last_iter.test_results if tests.get("summary", {}).get("failed", 0) == 0: return True return False async def _run_iteration(self, objective: str, context: Optional[Dict]) -> IterationResult: """Run a single iteration through all phases.""" start_time = datetime.now() result = IterationResult(iteration=self.state.iteration) try: # Phase 1: Planning (Gemini) self._update_state(LoopPhase.PLANNING, "Creating implementation plan") result.plan = await self._planning_phase(objective, context) # Phase 2: Implementation (Claude Code) self._update_state(LoopPhase.IMPLEMENTING, "Implementing changes") result.implementation = await self._implementation_phase(result.plan) # Phase 3: Testing (Dashboard) self._update_state(LoopPhase.TESTING, "Running tests") result.test_results = await self._testing_phase(result.plan) self.state.test_results = result.test_results # Phase 4: Analysis (Gemini) self._update_state(LoopPhase.ANALYZING, "Analyzing results") result.analysis = await self._analysis_phase(result.test_results) self.state.analysis = result.analysis # Phases 5-6: Fix & Verify if needed if result.analysis and result.analysis.get("issues_found"): self._update_state(LoopPhase.FIXING, "Implementing fixes") result.fixes = await self._fixing_phase(result.analysis) self._update_state(LoopPhase.VERIFYING, "Verifying fixes") result.verification = await self._verification_phase(result.fixes) result.success = result.verification.get("all_passed", False) else: result.success = True except Exception as e: logger.error(f"Iteration {self.state.iteration} failed: {e}") result.success = False result.duration_seconds = (datetime.now() - start_time).total_seconds() self.state.iteration += 1 return result async def _planning_phase(self, objective: str, context: Optional[Dict]) -> Dict: """Gemini creates implementation plan.""" # Gather context historical_learnings = [] if self.lac: historical_learnings = self.lac.get_relevant_insights(objective) plan_request = { "objective": objective, "context": context or {}, "previous_results": self.state.test_results, "historical_learnings": historical_learnings, } try: plan = await self.gemini.create_plan(plan_request) logger.info(f"Plan created with {len(plan.get('tasks', []))} tasks") return plan except Exception as e: logger.error(f"Planning phase failed: {e}") return {"error": str(e), "tasks": [], "test_scenarios": []} async def _implementation_phase(self, plan: Dict) -> Dict: """Claude Code implements the plan.""" if not plan or plan.get("error"): return {"status": "skipped", "reason": "No valid plan"} try: result = await self.claude_bridge.execute_plan(plan) return { "status": result.get("status", "unknown"), "files_modified": result.get("files", []), "warnings": result.get("warnings", []), } except Exception as e: logger.error(f"Implementation phase failed: {e}") return {"status": "error", "error": str(e)} async def _testing_phase(self, plan: Dict) -> Dict: """Dashboard runs automated tests.""" test_scenarios = plan.get("test_scenarios", []) if not test_scenarios: # Generate default tests based on objective test_scenarios = self._generate_default_tests(plan) try: results = await self.dashboard.run_test_suite(test_scenarios) return results except Exception as e: logger.error(f"Testing phase failed: {e}") return { "status": "error", "error": str(e), "summary": {"passed": 0, "failed": 1, "total": 1}, } def _generate_default_tests(self, plan: Dict) -> List[Dict]: """Generate default test scenarios based on the plan.""" objective = plan.get("objective", "") tests = [] # Study creation tests if "study" in objective.lower() or "create" in objective.lower(): tests.extend( [ { "id": "test_study_exists", "name": "Study directory exists", "type": "filesystem", "check": "directory_exists", }, { "id": "test_spec_valid", "name": "AtomizerSpec is valid", "type": "api", "endpoint": "/api/studies/{study_id}/spec/validate", }, { "id": "test_dashboard_loads", "name": "Dashboard loads study", "type": "browser", "action": "load_study", }, ] ) # Optimization tests if "optimi" in objective.lower(): tests.extend( [ { "id": "test_run_trial", "name": "Single trial executes", "type": "cli", "command": "python run_optimization.py --test", }, ] ) return tests async def _analysis_phase(self, test_results: Dict) -> Dict: """Gemini analyzes test results.""" try: from .analyzer import ProblemAnalyzer analyzer = ProblemAnalyzer(self.gemini) return await analyzer.analyze_test_results(test_results) except Exception as e: logger.error(f"Analysis phase failed: {e}") return { "issues_found": True, "issues": [{"description": str(e), "severity": "high"}], "fix_plans": {}, } async def _fixing_phase(self, analysis: Dict) -> List[Dict]: """Claude Code implements fixes.""" fixes = [] for issue in analysis.get("issues", []): fix_plan = analysis.get("fix_plans", {}).get(issue.get("id", "unknown")) if fix_plan: try: result = await self.claude_bridge.execute_fix(fix_plan) fixes.append( { "issue_id": issue.get("id"), "status": result.get("status"), "files_modified": result.get("files", []), } ) except Exception as e: fixes.append( { "issue_id": issue.get("id"), "status": "error", "error": str(e), } ) return fixes async def _verification_phase(self, fixes: List[Dict]) -> Dict: """Dashboard verifies fixes.""" # Re-run tests for each fix all_passed = True verification_results = [] for fix in fixes: if fix.get("status") == "error": all_passed = False verification_results.append( { "issue_id": fix.get("issue_id"), "passed": False, "reason": fix.get("error"), } ) else: # Run targeted test result = await self.dashboard.verify_fix(fix) verification_results.append(result) if not result.get("passed", False): all_passed = False return { "all_passed": all_passed, "results": verification_results, } async def _record_learning(self, iteration: IterationResult): """Store successful patterns for future reference.""" if not self.lac: return try: self.lac.record_insight( category="success_pattern", context=f"DevLoop iteration {iteration.iteration}", insight=f"Successfully completed: {iteration.plan.get('objective', 'unknown')}", confidence=0.8, tags=["devloop", "success"], ) except Exception as e: logger.warning(f"Failed to record learning: {e}") # ======================================================================== # Single-step operations (for manual control) # ======================================================================== async def step_plan(self, objective: str, context: Optional[Dict] = None) -> Dict: """Execute only the planning phase.""" self._update_state(LoopPhase.PLANNING, objective) plan = await self._planning_phase(objective, context) self._update_state(LoopPhase.IDLE) return plan async def step_implement(self, plan: Dict) -> Dict: """Execute only the implementation phase.""" self._update_state(LoopPhase.IMPLEMENTING) result = await self._implementation_phase(plan) self._update_state(LoopPhase.IDLE) return result async def step_test(self, scenarios: List[Dict]) -> Dict: """Execute only the testing phase.""" self._update_state(LoopPhase.TESTING) result = await self._testing_phase({"test_scenarios": scenarios}) self._update_state(LoopPhase.IDLE) return result async def step_analyze(self, test_results: Dict) -> Dict: """Execute only the analysis phase.""" self._update_state(LoopPhase.ANALYZING) result = await self._analysis_phase(test_results) self._update_state(LoopPhase.IDLE) return result def get_state(self) -> Dict: """Get current state as dict.""" return { "phase": self.state.phase.value, "iteration": self.state.iteration, "current_task": self.state.current_task, "test_results": self.state.test_results, "last_update": self.state.last_update, } def export_history(self, filepath: Optional[Path] = None) -> Dict: """Export cycle history for analysis.""" history = { "exported_at": datetime.now().isoformat(), "total_cycles": len(self.cycle_history), "cycles": [ { "objective": c.objective, "status": c.status, "iterations": len(c.iterations), "duration_seconds": c.total_duration_seconds, } for c in self.cycle_history ], } if filepath: with open(filepath, "w") as f: json.dump(history, f, indent=2) return history