rhodawk-ai-devops-engine / verification_loop.py
Rhodawk AI Patcher
fix: patch all critical bugs and gaps from ProjectKyto deep analysis
ef02f17
Raw
History Blame
5.04 kB
"""
Rhodawk AI β€” Closed Verification Loop Engine
=============================================
This is the core capability that separates Rhodawk from every other AI CI tool.
Standard tools: AI generates fix β†’ open PR (no idea if fix works)
Rhodawk: AI generates fix β†’ re-run tests β†’ if still failing, retry with
new failure context + what was tried β†’ up to MAX_RETRIES rounds
The loop:
1. Run tests β†’ get failure output
2. Dispatch Aider with failure context + memory-retrieved similar fixes
3. Re-run tests on the modified code
4. If GREEN β†’ gate through adversarial review β†’ open PR
5. If STILL RED β†’ append new failure + what was tried β†’ goto 2
6. After MAX_RETRIES β†’ mark as FAILED, escalate
BUG-002 FIX: Removed hardcoded os.getenv("RHODAWK_REPO_DIR") β€” repo_dir is now
passed as a parameter to build_initial_prompt() and build_retry_prompt().
BUG-003 FIX: ADVERSARIAL_REJECTION_MULTIPLIER defaults to 2 (not 0) so adversarial
rejections get extra retry budget beyond MAX_RETRIES.
"""
import os
import time
from dataclasses import dataclass, field
from typing import Optional
from language_runtime import RuntimeFactory
MAX_RETRIES = int(os.getenv("RHODAWK_MAX_RETRIES", "5"))
ADVERSARIAL_REJECTION_MULTIPLIER = int(os.getenv("RHODAWK_ADVERSARIAL_REJECTION_MULTIPLIER", "2"))
RETRY_BACKOFF_SECONDS = 5
@dataclass
class VerificationAttempt:
attempt_number: int
prompt_hash: str
aider_exit_code: int
test_exit_code: int
test_output: str
diff_produced: str
timestamp: str = field(default_factory=lambda: time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
@dataclass
class VerificationResult:
success: bool
attempts: list[VerificationAttempt] = field(default_factory=list)
final_diff: str = ""
final_test_output: str = ""
failure_reason: str = ""
total_attempts: int = 0
def build_retry_prompt(
test_path: str,
src_file: str,
branch_name: str,
original_failure: str,
attempt_history: list[VerificationAttempt],
similar_fixes: list[dict],
repo_dir: str = "/data/repo",
) -> str:
"""
Build an increasingly rich prompt for each retry attempt.
Each retry includes:
- The original failure
- What was tried in previous attempts and why it failed
- Retrieved similar successful fixes from memory
"""
sections = []
sections.append(
f"The pytest test '{test_path}' is STILL FAILING. This is attempt "
f"{len(attempt_history) + 1} of {MAX_RETRIES}.\n"
)
sections.append(
f"ORIGINAL FAILURE:\n```\n{original_failure[:2000]}\n```\n"
)
if attempt_history:
sections.append("PREVIOUS ATTEMPTS THAT DID NOT WORK:")
for a in attempt_history:
sections.append(
f"\nAttempt {a.attempt_number}:\n"
f" Test output after fix:\n```\n{a.test_output[:800]}\n```\n"
f" Diff that was applied:\n```diff\n{a.diff_produced[:600]}\n```"
)
sections.append(
"\nDo NOT repeat the same fix approach. Analyze why previous attempts failed "
"and try a fundamentally different strategy.\n"
)
if similar_fixes:
sections.append("\nSIMILAR FIXES FROM MEMORY (from previously healed tests β€” use as guidance):")
for i, fix in enumerate(similar_fixes[:2], 1):
sections.append(
f"\nSimilar fix {i} (success rate: {fix.get('success_rate', 'unknown')}):\n"
f" Failure pattern: {fix.get('failure_signature', '')[:200]}\n"
f" Fix applied:\n```diff\n{fix.get('fix_diff', '')[:400]}\n```"
)
runtime = RuntimeFactory.for_repo(repo_dir)
sections.append("INSTRUCTIONS:\n" + runtime.get_fix_prompt_instructions(
test_path=test_path,
branch_name=branch_name,
src_hint=src_file,
))
return "\n".join(sections)
def build_initial_prompt(
test_path: str,
src_file: str,
branch_name: str,
failure_output: str,
similar_fixes: list[dict],
repo_dir: str = "/data/repo",
) -> str:
sections = []
sections.append(
f"The pytest test '{test_path}' is failing:\n\n"
f"```\n{failure_output[:3000]}\n```\n"
)
if similar_fixes:
sections.append("RELEVANT FIXES FROM MEMORY (similar past failures that were healed):")
for i, fix in enumerate(similar_fixes[:2], 1):
sections.append(
f"\nSimilar case {i} (success rate: {fix.get('success_rate', 'unknown')}):\n"
f" Failure: {fix.get('failure_signature', '')[:150]}\n"
f" What worked:\n```diff\n{fix.get('fix_diff', '')[:400]}\n```"
)
runtime = RuntimeFactory.for_repo(repo_dir)
sections.append("INSTRUCTIONS:\n" + runtime.get_fix_prompt_instructions(
test_path=test_path,
branch_name=branch_name,
src_hint=src_file,
))
return "\n".join(sections)