""" CLI Bridge - Execute AI tasks through Claude Code CLI and OpenCode CLI. Uses your existing subscriptions via CLI tools: - Claude Code CLI (claude.exe) for implementation - OpenCode CLI (opencode) for Gemini planning No API keys needed - leverages your CLI subscriptions. """ import asyncio import json import logging import os import subprocess import tempfile from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional, Tuple import re logger = logging.getLogger(__name__) @dataclass class CLIResult: """Result from CLI execution.""" success: bool output: str error: str duration_seconds: float files_modified: List[str] class ClaudeCodeCLI: """ Execute tasks through Claude Code CLI. Uses: claude.exe --print for non-interactive execution """ CLAUDE_PATH = r"C:\Users\antoi\.local\bin\claude.exe" def __init__(self, workspace: Path): self.workspace = workspace async def execute( self, prompt: str, timeout: int = 300, model: str = "opus", ) -> CLIResult: """ Execute a prompt through Claude Code CLI. Args: prompt: The instruction/prompt to execute timeout: Timeout in seconds model: Model to use (opus, sonnet, haiku) Returns: CLIResult with output and modified files """ start_time = datetime.now() # Build command cmd = [ self.CLAUDE_PATH, "--print", # Non-interactive mode "--model", model, "--permission-mode", "acceptEdits", # Auto-accept edits prompt, ] logger.info(f"Executing Claude Code CLI: {prompt[:100]}...") try: # Run in workspace directory result = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout, cwd=str(self.workspace), env={**os.environ, "TERM": "dumb"}, # Disable colors ) output = result.stdout error = result.stderr success = result.returncode == 0 # Extract modified files from output files_modified = self._extract_modified_files(output) duration = (datetime.now() - start_time).total_seconds() logger.info( f"Claude Code completed in {duration:.1f}s, modified {len(files_modified)} files" ) return CLIResult( success=success, output=output, error=error, duration_seconds=duration, files_modified=files_modified, ) except subprocess.TimeoutExpired: return CLIResult( success=False, output="", error=f"Timeout after {timeout}s", duration_seconds=timeout, files_modified=[], ) except Exception as e: return CLIResult( success=False, output="", error=str(e), duration_seconds=(datetime.now() - start_time).total_seconds(), files_modified=[], ) def _extract_modified_files(self, output: str) -> List[str]: """Extract list of modified files from Claude Code output.""" files = [] # Look for file modification patterns patterns = [ r"(?:Created|Modified|Wrote|Updated|Edited):\s*[`'\"]?([^\s`'\"]+)[`'\"]?", r"Writing to [`'\"]?([^\s`'\"]+)[`'\"]?", r"File saved: ([^\s]+)", ] for pattern in patterns: matches = re.findall(pattern, output, re.IGNORECASE) files.extend(matches) return list(set(files)) async def execute_with_context( self, prompt: str, context_files: List[str], timeout: int = 300, ) -> CLIResult: """ Execute with additional context files loaded. Args: prompt: The instruction context_files: Files to read as context timeout: Timeout in seconds """ # Build prompt with context context_prompt = prompt if context_files: context_prompt += "\n\nContext files to consider:\n" for f in context_files: context_prompt += f"- {f}\n" return await self.execute(context_prompt, timeout) class OpenCodeCLI: """ Execute tasks through OpenCode CLI (Gemini). Uses: opencode run for non-interactive execution """ OPENCODE_PATH = r"C:\Users\antoi\AppData\Roaming\npm\opencode.cmd" def __init__(self, workspace: Path): self.workspace = workspace async def execute( self, prompt: str, timeout: int = 180, model: str = "google/gemini-3-pro-preview", ) -> CLIResult: """ Execute a prompt through OpenCode CLI. Args: prompt: The instruction/prompt timeout: Timeout in seconds model: Model to use Returns: CLIResult with output """ start_time = datetime.now() # Build command cmd = [self.OPENCODE_PATH, "run", "--model", model, prompt] logger.info(f"Executing OpenCode CLI: {prompt[:100]}...") try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout, cwd=str(self.workspace), env={**os.environ, "TERM": "dumb"}, ) output = result.stdout error = result.stderr success = result.returncode == 0 duration = (datetime.now() - start_time).total_seconds() logger.info(f"OpenCode completed in {duration:.1f}s") return CLIResult( success=success, output=output, error=error, duration_seconds=duration, files_modified=[], # OpenCode typically doesn't modify files directly ) except subprocess.TimeoutExpired: return CLIResult( success=False, output="", error=f"Timeout after {timeout}s", duration_seconds=timeout, files_modified=[], ) except Exception as e: return CLIResult( success=False, output="", error=str(e), duration_seconds=(datetime.now() - start_time).total_seconds(), files_modified=[], ) async def plan(self, objective: str, context: Dict = None) -> Dict: """ Create an implementation plan using Gemini via OpenCode. Args: objective: What to achieve context: Additional context Returns: Plan dict with tasks and test scenarios """ prompt = f"""You are a strategic planner for Atomizer, an FEA optimization framework. ## Objective {objective} ## Context {json.dumps(context, indent=2) if context else "None provided"} ## Task Create a detailed implementation plan in JSON format with: 1. tasks: List of implementation tasks for Claude Code 2. test_scenarios: Tests to verify implementation 3. acceptance_criteria: Success conditions Output ONLY valid JSON in this format: ```json {{ "objective": "{objective}", "approach": "Brief description", "tasks": [ {{ "id": "task_001", "description": "What to do", "file": "path/to/file.py", "priority": "high" }} ], "test_scenarios": [ {{ "id": "test_001", "name": "Test name", "type": "filesystem", "steps": [{{"action": "check_exists", "path": "some/path"}}], "expected_outcome": {{"exists": true}} }} ], "acceptance_criteria": [ "Criterion 1" ] }} ``` """ result = await self.execute(prompt) if not result.success: logger.error(f"OpenCode planning failed: {result.error}") return self._fallback_plan(objective, context) # Parse JSON from output try: # Find JSON block in output output = result.output if "```json" in output: start = output.find("```json") + 7 end = output.find("```", start) json_str = output[start:end].strip() elif "```" in output: start = output.find("```") + 3 end = output.find("```", start) json_str = output[start:end].strip() else: # Try to find JSON object directly match = re.search(r"\{.*\}", output, re.DOTALL) if match: json_str = match.group() else: return self._fallback_plan(objective, context) plan = json.loads(json_str) logger.info(f"Plan created with {len(plan.get('tasks', []))} tasks") return plan except json.JSONDecodeError as e: logger.error(f"Failed to parse plan JSON: {e}") return self._fallback_plan(objective, context) def _fallback_plan(self, objective: str, context: Dict = None) -> Dict: """Generate a fallback plan when Gemini fails.""" logger.warning("Using fallback plan") return { "objective": objective, "approach": "Fallback plan - manual implementation", "tasks": [ { "id": "task_001", "description": f"Implement: {objective}", "file": "TBD", "priority": "high", } ], "test_scenarios": [], "acceptance_criteria": [objective], } async def analyze(self, test_results: Dict) -> Dict: """ Analyze test results using Gemini via OpenCode. Args: test_results: Test report from dashboard Returns: Analysis with issues and fix plans """ summary = test_results.get("summary", {}) scenarios = test_results.get("scenarios", []) if summary.get("failed", 0) == 0: return { "issues_found": False, "issues": [], "fix_plans": {}, "recommendations": ["All tests passed!"], } failures = [s for s in scenarios if not s.get("passed", True)] prompt = f"""Analyze these test failures for Atomizer FEA optimization framework: ## Test Summary - Total: {summary.get("total", 0)} - Passed: {summary.get("passed", 0)} - Failed: {summary.get("failed", 0)} ## Failed Tests {json.dumps(failures, indent=2)} ## Task Provide root cause analysis and fix plans in JSON: ```json {{ "issues_found": true, "issues": [ {{ "id": "issue_001", "description": "What went wrong", "severity": "high", "root_cause": "Why it failed" }} ], "fix_plans": {{ "issue_001": {{ "approach": "How to fix", "steps": [{{"action": "edit", "file": "path", "description": "change"}}] }} }}, "recommendations": ["suggestion"] }} ``` """ result = await self.execute(prompt) if not result.success: return self._fallback_analysis(failures) try: output = result.output if "```json" in output: start = output.find("```json") + 7 end = output.find("```", start) json_str = output[start:end].strip() else: match = re.search(r"\{.*\}", output, re.DOTALL) json_str = match.group() if match else "{}" return json.loads(json_str) except: return self._fallback_analysis(failures) def _fallback_analysis(self, failures: List[Dict]) -> Dict: """Generate fallback analysis.""" issues = [] fix_plans = {} for i, failure in enumerate(failures): issue_id = f"issue_{i + 1}" issues.append( { "id": issue_id, "description": failure.get("error", "Unknown error"), "severity": "medium", "root_cause": "Requires investigation", } ) fix_plans[issue_id] = { "approach": "Manual investigation required", "steps": [], } return { "issues_found": len(issues) > 0, "issues": issues, "fix_plans": fix_plans, "recommendations": ["Review failed tests manually"], } class DevLoopCLIOrchestrator: """ Orchestrate DevLoop using CLI tools. - OpenCode (Gemini) for planning and analysis - Claude Code for implementation and fixes """ def __init__(self, workspace: Path = None): self.workspace = workspace or Path("C:/Users/antoi/Atomizer") self.claude = ClaudeCodeCLI(self.workspace) self.opencode = OpenCodeCLI(self.workspace) self.iteration = 0 async def run_cycle( self, objective: str, context: Dict = None, max_iterations: int = 5, ) -> Dict: """ Run a complete development cycle. Args: objective: What to achieve context: Additional context max_iterations: Maximum fix iterations Returns: Cycle report """ from .test_runner import DashboardTestRunner start_time = datetime.now() results = { "objective": objective, "iterations": [], "status": "in_progress", } logger.info(f"Starting DevLoop cycle: {objective}") # Phase 1: Plan (Gemini via OpenCode) logger.info("Phase 1: Planning with Gemini...") plan = await self.opencode.plan(objective, context) iteration = 0 while iteration < max_iterations: iteration += 1 iter_result = {"iteration": iteration} # Phase 2: Implement (Claude Code) logger.info(f"Phase 2 (iter {iteration}): Implementing with Claude Code...") impl_result = await self._implement(plan) iter_result["implementation"] = { "success": impl_result.success, "files_modified": impl_result.files_modified, } # Phase 3: Test (Dashboard) logger.info(f"Phase 3 (iter {iteration}): Testing...") test_runner = DashboardTestRunner() test_results = await test_runner.run_test_suite(plan.get("test_scenarios", [])) iter_result["test_results"] = test_results # Check if all tests pass summary = test_results.get("summary", {}) if summary.get("failed", 0) == 0: logger.info("All tests passed!") results["iterations"].append(iter_result) results["status"] = "success" break # Phase 4: Analyze (Gemini via OpenCode) logger.info(f"Phase 4 (iter {iteration}): Analyzing failures...") analysis = await self.opencode.analyze(test_results) iter_result["analysis"] = analysis if not analysis.get("issues_found"): results["status"] = "success" results["iterations"].append(iter_result) break # Phase 5: Fix (Claude Code) logger.info(f"Phase 5 (iter {iteration}): Fixing issues...") fix_result = await self._fix(analysis) iter_result["fixes"] = { "success": fix_result.success, "files_modified": fix_result.files_modified, } results["iterations"].append(iter_result) if results["status"] == "in_progress": results["status"] = "max_iterations_reached" results["duration_seconds"] = (datetime.now() - start_time).total_seconds() logger.info(f"DevLoop cycle completed: {results['status']}") return results async def _implement(self, plan: Dict) -> CLIResult: """Implement the plan using Claude Code.""" tasks = plan.get("tasks", []) if not tasks: return CLIResult( success=True, output="No tasks to implement", error="", duration_seconds=0, files_modified=[], ) # Build implementation prompt prompt = f"""Implement the following tasks for Atomizer: ## Objective {plan.get("objective", "Unknown")} ## Approach {plan.get("approach", "Follow best practices")} ## Tasks """ for task in tasks: prompt += f""" ### {task.get("id", "task")}: {task.get("description", "")} - File: {task.get("file", "TBD")} - Priority: {task.get("priority", "medium")} """ prompt += """ ## Requirements - Follow Atomizer coding standards - Use AtomizerSpec v2.0 format - Create README.md for any new study - Use existing extractors from optimization_engine/extractors/ """ return await self.claude.execute(prompt, timeout=300) async def _fix(self, analysis: Dict) -> CLIResult: """Apply fixes using Claude Code.""" issues = analysis.get("issues", []) fix_plans = analysis.get("fix_plans", {}) if not issues: return CLIResult( success=True, output="No issues to fix", error="", duration_seconds=0, files_modified=[], ) # Build fix prompt prompt = "Fix the following issues:\n\n" for issue in issues: issue_id = issue.get("id", "unknown") prompt += f""" ## Issue: {issue_id} - Description: {issue.get("description", "")} - Root Cause: {issue.get("root_cause", "Unknown")} - Severity: {issue.get("severity", "medium")} """ fix_plan = fix_plans.get(issue_id, {}) if fix_plan: prompt += f"- Fix Approach: {fix_plan.get('approach', 'Investigate')}\n" for step in fix_plan.get("steps", []): prompt += f" - {step.get('description', step.get('action', 'step'))}\n" return await self.claude.execute(prompt, timeout=300) async def step_plan(self, objective: str, context: Dict = None) -> Dict: """Execute only the planning phase.""" return await self.opencode.plan(objective, context) async def step_implement(self, plan: Dict) -> CLIResult: """Execute only the implementation phase.""" return await self._implement(plan) async def step_analyze(self, test_results: Dict) -> Dict: """Execute only the analysis phase.""" return await self.opencode.analyze(test_results)