File size: 5,041 Bytes
c32102b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef02f17
 
 
 
 
c32102b
 
 
 
 
 
ffaeef0
c32102b
 
ef02f17
c32102b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef02f17
c32102b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef02f17
ffaeef0
 
 
 
 
c32102b
 
 
 
 
 
 
 
 
 
ef02f17
c32102b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef02f17
ffaeef0
 
 
 
 
c32102b
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""
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)