You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
146 lines
5.1 KiB
Python
146 lines
5.1 KiB
Python
"""Repair mode: use Claude Code to fix a broken site adapter."""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from loguru import logger
|
|
|
|
from gimme_job.config import GlobalConfig
|
|
from gimme_job.runtime.claude_cli import ClaudeCodeClient, DEFAULT_REPAIR_TOOLS
|
|
|
|
|
|
class RepairMode:
|
|
def __init__(self, global_config: GlobalConfig, claude_client: ClaudeCodeClient):
|
|
self.cfg = global_config
|
|
self.claude = claude_client
|
|
|
|
def repair_site(self, site_id: str) -> bool:
|
|
from gimme_job.db.engine import get_engine, get_session_factory
|
|
from gimme_job.db.repo import SiteConfigRepo, SiteRunRepo
|
|
from gimme_job.utils.paths import project_root, sites_dir
|
|
|
|
from rich.console import Console
|
|
console = Console()
|
|
|
|
logger.info(f"[repair] Starting repair for {site_id}")
|
|
|
|
engine = get_engine()
|
|
factory = get_session_factory(engine)
|
|
|
|
# Load recent failure artifacts
|
|
with factory() as session:
|
|
recent_runs = SiteRunRepo().get_recent_runs(session, site_id, limit=5)
|
|
|
|
# Find last failure with artifacts
|
|
last_run = None
|
|
for run in recent_runs:
|
|
if run.status in ("failed", "repair_needed"):
|
|
last_run = run
|
|
break
|
|
|
|
error_summary = last_run.error_summary if last_run else "No recent failure details"
|
|
failure_classification = last_run.failure_classification if last_run else None
|
|
dom_path = last_run.dom_snapshot_path if last_run else None
|
|
ss_path = last_run.screenshot_path if last_run else None
|
|
trace_path = last_run.trace_path if last_run else None
|
|
|
|
# Format recent run history
|
|
run_history = "\n".join(
|
|
f"- {r.started_at.strftime('%Y-%m-%d %H:%M')} | {r.status} | items={r.items_found}"
|
|
for r in recent_runs[:5]
|
|
) or "No run history"
|
|
|
|
manifest_path = sites_dir() / f"{site_id}.yaml"
|
|
adapter_path = project_root() / "gimme_job" / "adapters" / f"{site_id}.py"
|
|
|
|
if not manifest_path.exists():
|
|
logger.error(f"[repair] Manifest not found: {manifest_path}")
|
|
return False
|
|
|
|
# Build repair prompt
|
|
prompt = self._build_prompt(
|
|
site_id=site_id,
|
|
error_summary=error_summary,
|
|
failure_classification=failure_classification,
|
|
run_history=run_history,
|
|
dom_path=dom_path,
|
|
ss_path=ss_path,
|
|
trace_path=trace_path,
|
|
manifest_path=manifest_path,
|
|
adapter_path=adapter_path,
|
|
)
|
|
|
|
console.print("[cyan]Calling Claude Code for repair...[/cyan]")
|
|
result = self.claude.run_prompt(
|
|
prompt=prompt,
|
|
cwd=project_root(),
|
|
allowed_tools=DEFAULT_REPAIR_TOOLS,
|
|
)
|
|
|
|
if not result.success:
|
|
logger.error(f"[repair] Claude Code failed: {result.error}")
|
|
return False
|
|
|
|
# Run smoke test
|
|
console.print(f"[cyan]Running smoke test...[/cyan]")
|
|
import subprocess
|
|
test_result = subprocess.run(
|
|
["uv", "run", "pytest", f"tests/adapters/test_{site_id}.py", "-v", "--tb=short"],
|
|
cwd=str(project_root()),
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120,
|
|
)
|
|
|
|
if test_result.returncode != 0:
|
|
logger.error(f"[repair] Smoke test failed:\n{test_result.stdout[-500:]}")
|
|
return False
|
|
|
|
# Clear repair_needed flag
|
|
with factory() as session:
|
|
SiteConfigRepo().set_repair_needed(session, site_id, False)
|
|
SiteConfigRepo().reset_failures(session, site_id)
|
|
|
|
# Also update the YAML manifest
|
|
try:
|
|
from gimme_job.config import load_site_manifest
|
|
manifest = load_site_manifest(site_id)
|
|
manifest.repair_needed = False
|
|
manifest.to_yaml(manifest_path)
|
|
except Exception as e:
|
|
logger.warning(f"[repair] Could not update manifest YAML: {e}")
|
|
|
|
logger.info(f"[repair] {site_id} repaired successfully")
|
|
return True
|
|
|
|
def _build_prompt(
|
|
self,
|
|
site_id: str,
|
|
error_summary: str,
|
|
failure_classification: str | None,
|
|
run_history: str,
|
|
dom_path: str | None,
|
|
ss_path: str | None,
|
|
trace_path: str | None,
|
|
manifest_path: Path,
|
|
adapter_path: Path,
|
|
) -> str:
|
|
from jinja2 import Environment, FileSystemLoader
|
|
from gimme_job.utils.paths import project_root
|
|
|
|
prompts_dir = project_root() / "gimme_job" / "prompts"
|
|
env = Environment(loader=FileSystemLoader(str(prompts_dir)))
|
|
template = env.get_template("repair_user.md.j2")
|
|
|
|
return template.render(
|
|
site_id=site_id,
|
|
error_summary=error_summary,
|
|
failure_classification=failure_classification,
|
|
recent_run_history=run_history,
|
|
last_run_dom_snapshot_path=dom_path,
|
|
last_run_screenshot_path=ss_path,
|
|
last_run_trace_path=trace_path,
|
|
current_manifest_path=str(manifest_path),
|
|
current_adapter_path=str(adapter_path),
|
|
)
|