## DevLoop - Closed-Loop Development System - Orchestrator for plan → build → test → analyze cycle - Gemini planning via OpenCode CLI - Claude implementation via CLI bridge - Playwright browser testing integration - Test runner with API, filesystem, and browser tests - Persistent state in .devloop/ directory - CLI tool: tools/devloop_cli.py Usage: python tools/devloop_cli.py start 'Create new feature' python tools/devloop_cli.py plan 'Fix bug in X' python tools/devloop_cli.py test --study support_arm python tools/devloop_cli.py browser --level full ## HTML Reports (optimization_engine/reporting/) - Interactive Plotly-based reports - Convergence plot, Pareto front, parallel coordinates - Parameter importance analysis - Self-contained HTML (offline-capable) - Tailwind CSS styling ## Playwright E2E Tests - Home page tests - Test results in test-results/ ## LAC Knowledge Base Updates - Session insights (failures, workarounds, patterns) - Optimization memory for arm support study
393 lines
12 KiB
Python
393 lines
12 KiB
Python
"""
|
|
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,
|
|
}
|