feat: Add DevLoop automation and HTML Reports

## 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
This commit is contained in:
2026-01-24 21:18:18 -05:00
parent a3f18dc377
commit 3193831340
24 changed files with 6437 additions and 0 deletions

View File

@@ -0,0 +1,392 @@
"""
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,
}