""" Claude Code Bridge - Interface between DevLoop and Claude Code execution. Handles: - Translating Gemini plans into Claude Code instructions - Executing code changes through OpenCode extension or CLI - Capturing implementation results """ import asyncio import json import logging import os import subprocess from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional logger = logging.getLogger(__name__) @dataclass class ImplementationResult: """Result of a Claude Code implementation.""" status: str # "success", "partial", "error" files_modified: List[str] warnings: List[str] errors: List[str] duration_seconds: float class ClaudeCodeBridge: """ Bridge between Gemini plans and Claude Code execution. Supports multiple execution modes: - CLI: Direct Claude Code CLI invocation - API: Anthropic API for code generation (if API key available) - Manual: Generate instructions for human execution """ def __init__(self, config: Optional[Dict] = None): """ Initialize the bridge. Args: config: Configuration with execution mode and API settings """ self.config = config or {} self.workspace = Path(self.config.get("workspace", "C:/Users/antoi/Atomizer")) self.execution_mode = self.config.get("mode", "cli") self._client = None @property def client(self): """Lazy-load Anthropic client if API mode.""" if self._client is None and self.execution_mode == "api": try: import anthropic api_key = self.config.get("api_key") or os.environ.get("ANTHROPIC_API_KEY") if api_key: self._client = anthropic.Anthropic(api_key=api_key) logger.info("Anthropic client initialized") except ImportError: logger.warning("anthropic package not installed") return self._client def create_implementation_session(self, plan: Dict) -> str: """ Generate Claude Code instruction from Gemini plan. Args: plan: Plan dict from GeminiPlanner Returns: Formatted instruction string for Claude Code """ objective = plan.get("objective", "Unknown objective") approach = plan.get("approach", "") tasks = plan.get("tasks", []) acceptance_criteria = plan.get("acceptance_criteria", []) instruction = f"""## Implementation Task: {objective} ### Approach {approach} ### Tasks to Complete """ for i, task in enumerate(tasks, 1): instruction += f""" {i}. **{task.get("description", "Task")}** - File: `{task.get("file", "TBD")}` - Priority: {task.get("priority", "medium")} """ if task.get("code_hint"): instruction += f" - Hint: {task.get('code_hint')}\n" if task.get("dependencies"): instruction += f" - Depends on: {', '.join(task['dependencies'])}\n" instruction += """ ### Acceptance Criteria """ for criterion in acceptance_criteria: instruction += f"- [ ] {criterion}\n" instruction += """ ### Constraints - Maintain existing API contracts - Follow Atomizer coding standards - Ensure AtomizerSpec v2.0 compatibility - Create README.md for any new study - Use existing extractors from SYS_12 when possible """ return instruction async def execute_plan(self, plan: Dict) -> Dict: """ Execute an implementation plan. Args: plan: Plan dict from GeminiPlanner Returns: Implementation result dict """ instruction = self.create_implementation_session(plan) if self.execution_mode == "cli": return await self._execute_via_cli(instruction, plan) elif self.execution_mode == "api": return await self._execute_via_api(instruction, plan) else: return await self._execute_manual(instruction, plan) async def _execute_via_cli(self, instruction: str, plan: Dict) -> Dict: """Execute through Claude Code CLI.""" start_time = datetime.now() # Write instruction to temp file instruction_file = self.workspace / ".devloop_instruction.md" instruction_file.write_text(instruction) files_modified = [] warnings = [] errors = [] try: # Try to invoke Claude Code CLI # Note: This assumes claude-code or similar CLI is available result = subprocess.run( [ "powershell", "-Command", f"cd {self.workspace}; claude --print '{instruction_file}'", ], capture_output=True, text=True, timeout=300, # 5 minute timeout cwd=str(self.workspace), ) if result.returncode == 0: # Parse output for modified files output = result.stdout for line in output.split("\n"): if "Modified:" in line or "Created:" in line: parts = line.split(":", 1) if len(parts) > 1: files_modified.append(parts[1].strip()) status = "success" else: errors.append(result.stderr or "CLI execution failed") status = "error" except subprocess.TimeoutExpired: errors.append("CLI execution timed out after 5 minutes") status = "error" except FileNotFoundError: # Claude CLI not found, fall back to manual mode logger.warning("Claude CLI not found, switching to manual mode") return await self._execute_manual(instruction, plan) except Exception as e: errors.append(str(e)) status = "error" finally: # Clean up temp file if instruction_file.exists(): instruction_file.unlink() duration = (datetime.now() - start_time).total_seconds() return { "status": status, "files": files_modified, "warnings": warnings, "errors": errors, "duration_seconds": duration, } async def _execute_via_api(self, instruction: str, plan: Dict) -> Dict: """Execute through Anthropic API for code generation.""" if not self.client: return await self._execute_manual(instruction, plan) start_time = datetime.now() files_modified = [] warnings = [] errors = [] try: # Use Claude API for code generation response = self.client.messages.create( model="claude-sonnet-4-20250514", max_tokens=8192, messages=[ { "role": "user", "content": f"""You are implementing code for the Atomizer FEA optimization framework. {instruction} For each file that needs to be created or modified, output the complete file content in this format: ### FILE: path/to/file.py ```python # file content here ``` Be thorough and implement all tasks completely. """, } ], ) # Parse response for file contents content = response.content[0].text # Extract files from response import re file_pattern = r"### FILE: (.+?)\n```\w*\n(.*?)```" matches = re.findall(file_pattern, content, re.DOTALL) for file_path, file_content in matches: try: full_path = self.workspace / file_path.strip() full_path.parent.mkdir(parents=True, exist_ok=True) full_path.write_text(file_content.strip()) files_modified.append(str(file_path.strip())) logger.info(f"Created/modified: {file_path}") except Exception as e: errors.append(f"Failed to write {file_path}: {e}") status = "success" if files_modified else "partial" except Exception as e: errors.append(str(e)) status = "error" duration = (datetime.now() - start_time).total_seconds() return { "status": status, "files": files_modified, "warnings": warnings, "errors": errors, "duration_seconds": duration, } async def _execute_manual(self, instruction: str, plan: Dict) -> Dict: """ Generate manual instructions (when automation not available). Saves instruction to file for human execution. """ start_time = datetime.now() # Save instruction for manual execution output_file = self.workspace / ".devloop" / "pending_instruction.md" output_file.parent.mkdir(parents=True, exist_ok=True) output_file.write_text(instruction) logger.info(f"Manual instruction saved to: {output_file}") return { "status": "pending_manual", "instruction_file": str(output_file), "files": [], "warnings": ["Automated execution not available. Please execute manually."], "errors": [], "duration_seconds": (datetime.now() - start_time).total_seconds(), } async def execute_fix(self, fix_plan: Dict) -> Dict: """ Execute a specific fix from analysis. Args: fix_plan: Fix plan dict from ProblemAnalyzer Returns: Fix result dict """ issue_id = fix_plan.get("issue_id", "unknown") approach = fix_plan.get("approach", "") steps = fix_plan.get("steps", []) instruction = f"""## Bug Fix: {issue_id} ### Approach {approach} ### Steps """ for i, step in enumerate(steps, 1): instruction += f"{i}. {step.get('description', step.get('action', 'Step'))}\n" if step.get("file"): instruction += f" File: `{step['file']}`\n" instruction += """ ### Verification After implementing the fix, verify that: 1. The specific test case passes 2. No regressions are introduced 3. Code follows Atomizer patterns """ # Execute as a mini-plan return await self.execute_plan( { "objective": f"Fix: {issue_id}", "approach": approach, "tasks": [ { "description": step.get("description", step.get("action")), "file": step.get("file"), "priority": "high", } for step in steps ], "acceptance_criteria": [ "Original test passes", "No new errors introduced", ], } ) def get_execution_status(self) -> Dict: """Get current execution status.""" pending_file = self.workspace / ".devloop" / "pending_instruction.md" return { "mode": self.execution_mode, "workspace": str(self.workspace), "has_pending_instruction": pending_file.exists(), "api_available": self.client is not None, } async def verify_implementation(self, expected_files: List[str]) -> Dict: """ Verify that implementation created expected files. Args: expected_files: List of file paths that should exist Returns: Verification result """ missing = [] found = [] for file_path in expected_files: path = ( self.workspace / file_path if not Path(file_path).is_absolute() else Path(file_path) ) if path.exists(): found.append(str(file_path)) else: missing.append(str(file_path)) return { "complete": len(missing) == 0, "found": found, "missing": missing, }