overkill 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mr_overkill/__init__.py +3 -0
- mr_overkill/__main__.py +116 -0
- mr_overkill/agents.py +497 -0
- mr_overkill/budget/__init__.py +109 -0
- mr_overkill/budget/claude.py +353 -0
- mr_overkill/budget/codex.py +118 -0
- mr_overkill/budget_report.py +98 -0
- mr_overkill/classify.py +63 -0
- mr_overkill/cli.py +643 -0
- mr_overkill/data/.refactorsuggestrc.example +50 -0
- mr_overkill/data/.reviewlooprc.example +36 -0
- mr_overkill/data/__init__.py +0 -0
- mr_overkill/data/prompts/active/claude-fix-execute.prompt.md +13 -0
- mr_overkill/data/prompts/active/claude-fix.prompt.md +20 -0
- mr_overkill/data/prompts/active/claude-refactor-fix-execute.prompt.md +22 -0
- mr_overkill/data/prompts/active/claude-refactor-fix.prompt.md +27 -0
- mr_overkill/data/prompts/active/claude-refactor-full.prompt.md +115 -0
- mr_overkill/data/prompts/active/claude-refactor-layer.prompt.md +115 -0
- mr_overkill/data/prompts/active/claude-refactor-micro.prompt.md +116 -0
- mr_overkill/data/prompts/active/claude-refactor-module.prompt.md +114 -0
- mr_overkill/data/prompts/active/claude-review.prompt.md +72 -0
- mr_overkill/data/prompts/active/claude-self-review.prompt.md +66 -0
- mr_overkill/data/prompts/active/codex-refactor-full.prompt.md +115 -0
- mr_overkill/data/prompts/active/codex-refactor-layer.prompt.md +115 -0
- mr_overkill/data/prompts/active/codex-refactor-micro.prompt.md +116 -0
- mr_overkill/data/prompts/active/codex-refactor-module.prompt.md +114 -0
- mr_overkill/data/prompts/active/codex-review.prompt.md +72 -0
- mr_overkill/git_ops.py +258 -0
- mr_overkill/init.py +142 -0
- mr_overkill/json_extract.py +205 -0
- mr_overkill/loop_engine.py +659 -0
- mr_overkill/models.py +293 -0
- mr_overkill/refactor_suggest.py +426 -0
- mr_overkill/reporting.py +203 -0
- mr_overkill/resume.py +143 -0
- mr_overkill/retry.py +302 -0
- mr_overkill/review_loop.py +56 -0
- mr_overkill/self_review.py +345 -0
- mr_overkill/time_utils.py +38 -0
- mr_overkill/two_step_fix.py +112 -0
- overkill-0.2.1.dist-info/METADATA +405 -0
- overkill-0.2.1.dist-info/RECORD +45 -0
- overkill-0.2.1.dist-info/WHEEL +4 -0
- overkill-0.2.1.dist-info/entry_points.txt +2 -0
- overkill-0.2.1.dist-info/licenses/LICENSE +21 -0
mr_overkill/__init__.py
ADDED
mr_overkill/__main__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Entry point for ``python -m mr_overkill``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
"""Dispatch subcommands."""
|
|
11
|
+
logging.basicConfig(
|
|
12
|
+
level=logging.INFO,
|
|
13
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
14
|
+
datefmt="%H:%M:%S",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if len(sys.argv) >= 2 and sys.argv[1] in ("-V", "--version"):
|
|
18
|
+
from mr_overkill import __version__
|
|
19
|
+
|
|
20
|
+
print(f"overkill {__version__}")
|
|
21
|
+
sys.exit(0)
|
|
22
|
+
|
|
23
|
+
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
|
|
24
|
+
print(
|
|
25
|
+
"Usage: overkill <command>\n\n"
|
|
26
|
+
"Commands:\n"
|
|
27
|
+
" init Initialize .review-loop/ in a project\n"
|
|
28
|
+
" review-loop Run AI review-fix loop\n"
|
|
29
|
+
" refactor-suggest Run AI refactoring suggestions\n"
|
|
30
|
+
" check-budget Show token budget usage summary",
|
|
31
|
+
file=sys.stderr,
|
|
32
|
+
)
|
|
33
|
+
sys.exit(0 if len(sys.argv) >= 2 else 1)
|
|
34
|
+
|
|
35
|
+
command = sys.argv[1]
|
|
36
|
+
sys.argv = sys.argv[1:] # Shift so argparse sees correct prog name
|
|
37
|
+
|
|
38
|
+
if command == "init":
|
|
39
|
+
import argparse
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
from mr_overkill.init import init_project
|
|
43
|
+
|
|
44
|
+
p = argparse.ArgumentParser(
|
|
45
|
+
prog="overkill init",
|
|
46
|
+
description="Initialize .review-loop/ in a project",
|
|
47
|
+
)
|
|
48
|
+
p.add_argument("target", nargs="?", default=".", help="Target directory")
|
|
49
|
+
init_args = p.parse_args()
|
|
50
|
+
init_project(Path(init_args.target).resolve())
|
|
51
|
+
sys.exit(0)
|
|
52
|
+
elif command == "review-loop":
|
|
53
|
+
from mr_overkill.cli import parse_review_loop_args
|
|
54
|
+
from mr_overkill.review_loop import run
|
|
55
|
+
|
|
56
|
+
config = parse_review_loop_args()
|
|
57
|
+
sys.exit(run(config))
|
|
58
|
+
elif command == "refactor-suggest":
|
|
59
|
+
from mr_overkill.cli import parse_refactor_suggest_args
|
|
60
|
+
from mr_overkill.refactor_suggest import run as refactor_run
|
|
61
|
+
|
|
62
|
+
config, extra = parse_refactor_suggest_args()
|
|
63
|
+
exit_code = refactor_run(
|
|
64
|
+
config, config.scope or "auto", create_pr=extra.create_pr,
|
|
65
|
+
)
|
|
66
|
+
if extra.with_review and exit_code == 0 and not config.dry_run:
|
|
67
|
+
import subprocess
|
|
68
|
+
|
|
69
|
+
ahead = subprocess.run(
|
|
70
|
+
["git", "rev-list", "--count",
|
|
71
|
+
f"{config.target_branch}..{config.current_branch}"],
|
|
72
|
+
capture_output=True, text=True, check=False,
|
|
73
|
+
)
|
|
74
|
+
if ahead.returncode != 0:
|
|
75
|
+
logging.getLogger(__name__).error(
|
|
76
|
+
"Failed to count commits: %s", ahead.stderr.strip(),
|
|
77
|
+
)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
if int(ahead.stdout.strip() or "0") == 0:
|
|
80
|
+
logging.getLogger(__name__).info(
|
|
81
|
+
"No refactor commits — skipping chained review-loop.",
|
|
82
|
+
)
|
|
83
|
+
sys.exit(exit_code)
|
|
84
|
+
|
|
85
|
+
from mr_overkill.cli import parse_review_loop_args
|
|
86
|
+
from mr_overkill.review_loop import run as review_run
|
|
87
|
+
|
|
88
|
+
review_config = parse_review_loop_args([
|
|
89
|
+
"-t", config.target_branch,
|
|
90
|
+
"-n", str(extra.review_loops),
|
|
91
|
+
"--reviewer-backend", config.reviewer_backend,
|
|
92
|
+
])
|
|
93
|
+
exit_code = review_run(review_config)
|
|
94
|
+
sys.exit(exit_code)
|
|
95
|
+
elif command == "check-budget":
|
|
96
|
+
import argparse
|
|
97
|
+
|
|
98
|
+
from mr_overkill.budget_report import print_budget_report
|
|
99
|
+
|
|
100
|
+
p = argparse.ArgumentParser(
|
|
101
|
+
prog="overkill check-budget",
|
|
102
|
+
description="Show token budget usage summary",
|
|
103
|
+
)
|
|
104
|
+
p.add_argument(
|
|
105
|
+
"--json", action="store_true", dest="json_mode",
|
|
106
|
+
help="Output machine-readable JSON",
|
|
107
|
+
)
|
|
108
|
+
budget_args = p.parse_args()
|
|
109
|
+
sys.exit(print_budget_report(json_mode=budget_args.json_mode))
|
|
110
|
+
else:
|
|
111
|
+
print(f"Unknown command: {command}", file=sys.stderr)
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
main()
|
mr_overkill/agents.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
"""Agent abstractions for reviewer/coder backends.
|
|
2
|
+
|
|
3
|
+
Provides ABC classes and concrete implementations for review, fix, and
|
|
4
|
+
self-review agents. Factory functions create the right agent based on
|
|
5
|
+
backend/variant parameters, replacing the closure-based factories that
|
|
6
|
+
were previously duplicated across review_loop.py and refactor_suggest.py.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import string
|
|
14
|
+
import subprocess
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from mr_overkill.budget.claude import claude_budget_sufficient
|
|
19
|
+
from mr_overkill.budget.codex import codex_budget_sufficient
|
|
20
|
+
from mr_overkill.models import (
|
|
21
|
+
BudgetCheckFn,
|
|
22
|
+
BudgetScope,
|
|
23
|
+
BudgetTimeoutError,
|
|
24
|
+
LoopConfig,
|
|
25
|
+
RetryFn,
|
|
26
|
+
WorktreeSnapshot,
|
|
27
|
+
)
|
|
28
|
+
from mr_overkill.retry import (
|
|
29
|
+
retry_claude_cmd,
|
|
30
|
+
retry_codex_cmd,
|
|
31
|
+
wait_for_budget,
|
|
32
|
+
)
|
|
33
|
+
from mr_overkill.self_review import self_review_subloop
|
|
34
|
+
from mr_overkill.two_step_fix import claude_two_step_fix
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── Budget / retry helpers (moved from review_loop.py) ───────────────
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _budget_check(
|
|
43
|
+
tool: str, scope: BudgetScope, max_wait: int
|
|
44
|
+
) -> bool:
|
|
45
|
+
"""Direct budget check without waiting."""
|
|
46
|
+
if tool == "claude":
|
|
47
|
+
return claude_budget_sufficient(scope)
|
|
48
|
+
if tool == "codex":
|
|
49
|
+
return codex_budget_sufficient(scope)
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _BudgetFn:
|
|
54
|
+
"""Budget-wait callable bound to config defaults.
|
|
55
|
+
|
|
56
|
+
Implemented as a class to satisfy the ``BudgetCheckFn`` Protocol.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, config: LoopConfig) -> None:
|
|
60
|
+
self._config = config
|
|
61
|
+
|
|
62
|
+
def __call__(
|
|
63
|
+
self, tool: str, scope: BudgetScope, max_wait: int
|
|
64
|
+
) -> bool:
|
|
65
|
+
actual = (
|
|
66
|
+
max_wait if max_wait > 0 else self._config.retry_max_wait
|
|
67
|
+
)
|
|
68
|
+
return wait_for_budget(_budget_check, tool, scope, actual)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _make_budget_fn(config: LoopConfig) -> BudgetCheckFn:
|
|
72
|
+
"""Create a budget-wait function bound to config defaults."""
|
|
73
|
+
return _BudgetFn(config)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _RetryFn:
|
|
77
|
+
"""Retry callable bound to config settings.
|
|
78
|
+
|
|
79
|
+
Implemented as a class to satisfy the ``RetryFn`` Protocol.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, config: LoopConfig) -> None:
|
|
83
|
+
self._config = config
|
|
84
|
+
|
|
85
|
+
def __call__(
|
|
86
|
+
self,
|
|
87
|
+
output_path: Path,
|
|
88
|
+
label: str,
|
|
89
|
+
cmd_args: list[str],
|
|
90
|
+
**kw: object,
|
|
91
|
+
) -> bool:
|
|
92
|
+
stdin = kw.get("stdin")
|
|
93
|
+
return retry_claude_cmd(
|
|
94
|
+
output_path,
|
|
95
|
+
label,
|
|
96
|
+
cmd_args,
|
|
97
|
+
stdin=str(stdin) if stdin is not None else None,
|
|
98
|
+
max_wait=self._config.retry_max_wait,
|
|
99
|
+
initial_wait=self._config.retry_initial_wait,
|
|
100
|
+
diagnostic_log=self._config.diagnostic_log,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _make_retry_fn(config: LoopConfig) -> RetryFn:
|
|
105
|
+
"""Create a retry function bound to config settings."""
|
|
106
|
+
return _RetryFn(config)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── ABC definitions ──────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ReviewAgent(ABC):
|
|
113
|
+
"""Abstract base for agents that run code review."""
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
def __call__(self, output_path: Path, iteration: int) -> bool: ...
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class FixAgent(ABC):
|
|
120
|
+
"""Abstract base for agents that apply fixes."""
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def __call__(
|
|
124
|
+
self, review_json: str, label: str, **kw: object
|
|
125
|
+
) -> bool: ...
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class SelfReviewAgent(ABC):
|
|
129
|
+
"""Abstract base for agents that run self-review sub-loops."""
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def __call__(
|
|
133
|
+
self,
|
|
134
|
+
pre_fix_snapshot: list[WorktreeSnapshot],
|
|
135
|
+
max_subloop: int,
|
|
136
|
+
log_dir: Path,
|
|
137
|
+
iteration: int,
|
|
138
|
+
review_json_str: str,
|
|
139
|
+
) -> str: ...
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ── Concrete implementations ────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class CodexReviewAgent(ReviewAgent):
|
|
146
|
+
"""Codex-based reviewer for the standard review-loop."""
|
|
147
|
+
|
|
148
|
+
def __init__(self, config: LoopConfig) -> None:
|
|
149
|
+
self._config = config
|
|
150
|
+
self._budget_fn = _make_budget_fn(config)
|
|
151
|
+
|
|
152
|
+
def __call__(self, output_path: Path, iteration: int) -> bool:
|
|
153
|
+
config = self._config
|
|
154
|
+
prompt_file = config.prompts_dir / "codex-review.prompt.md"
|
|
155
|
+
if not prompt_file.is_file():
|
|
156
|
+
logger.error("Review prompt not found: %s", prompt_file)
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
tmpl = string.Template(
|
|
160
|
+
prompt_file.read_text(encoding="utf-8")
|
|
161
|
+
)
|
|
162
|
+
prompt_text = tmpl.safe_substitute({
|
|
163
|
+
"CURRENT_BRANCH": config.current_branch,
|
|
164
|
+
"TARGET_BRANCH": config.target_branch,
|
|
165
|
+
"ITERATION": str(iteration),
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
if not self._budget_fn("codex", config.budget_scope, 0):
|
|
169
|
+
raise BudgetTimeoutError(
|
|
170
|
+
f"Codex budget timeout (iteration {iteration})."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
stderr_path = output_path.with_suffix(".stderr")
|
|
174
|
+
return retry_codex_cmd(
|
|
175
|
+
stderr_path,
|
|
176
|
+
"Codex review",
|
|
177
|
+
[
|
|
178
|
+
"codex", "exec", "--sandbox", "read-only",
|
|
179
|
+
"-o", str(output_path), prompt_text,
|
|
180
|
+
],
|
|
181
|
+
max_wait=config.retry_max_wait,
|
|
182
|
+
initial_wait=config.retry_initial_wait,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class CodexRefactorReviewAgent(ReviewAgent):
|
|
187
|
+
"""Codex-based reviewer for scope-specific refactor analysis."""
|
|
188
|
+
|
|
189
|
+
def __init__(self, config: LoopConfig, scope: str) -> None:
|
|
190
|
+
self._config = config
|
|
191
|
+
self._scope = scope
|
|
192
|
+
self._budget_fn = _make_budget_fn(config)
|
|
193
|
+
|
|
194
|
+
def __call__(self, output_path: Path, iteration: int) -> bool:
|
|
195
|
+
config = self._config
|
|
196
|
+
scope = self._scope
|
|
197
|
+
|
|
198
|
+
# Refresh source file list each iteration
|
|
199
|
+
source_files_path = config.log_dir / "source-files.txt"
|
|
200
|
+
result = subprocess.run(
|
|
201
|
+
["git", "ls-files"],
|
|
202
|
+
capture_output=True,
|
|
203
|
+
text=True,
|
|
204
|
+
check=False,
|
|
205
|
+
)
|
|
206
|
+
source_files_path.write_text(result.stdout)
|
|
207
|
+
|
|
208
|
+
prompt_file = (
|
|
209
|
+
config.prompts_dir / f"codex-refactor-{scope}.prompt.md"
|
|
210
|
+
)
|
|
211
|
+
if not prompt_file.is_file():
|
|
212
|
+
logger.error("Prompt not found: %s", prompt_file)
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
tmpl = string.Template(
|
|
216
|
+
prompt_file.read_text(encoding="utf-8")
|
|
217
|
+
)
|
|
218
|
+
prompt_text = tmpl.safe_substitute({
|
|
219
|
+
"CURRENT_BRANCH": config.current_branch,
|
|
220
|
+
"TARGET_BRANCH": config.target_branch,
|
|
221
|
+
"ITERATION": str(iteration),
|
|
222
|
+
"SOURCE_FILES_PATH": str(
|
|
223
|
+
config.log_dir / "source-files.txt"
|
|
224
|
+
),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
if not self._budget_fn("codex", config.budget_scope, 0):
|
|
228
|
+
raise BudgetTimeoutError(
|
|
229
|
+
f"Codex budget timeout (iteration {iteration})."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
stderr_path = output_path.with_suffix(".stderr")
|
|
233
|
+
return retry_codex_cmd(
|
|
234
|
+
stderr_path,
|
|
235
|
+
"Codex analysis",
|
|
236
|
+
[
|
|
237
|
+
"codex", "exec", "--sandbox", "read-only",
|
|
238
|
+
"-o", str(output_path), prompt_text,
|
|
239
|
+
],
|
|
240
|
+
max_wait=config.retry_max_wait,
|
|
241
|
+
initial_wait=config.retry_initial_wait,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class ClaudeReviewAgent(ReviewAgent):
|
|
246
|
+
"""Claude-based reviewer for the standard review-loop."""
|
|
247
|
+
|
|
248
|
+
def __init__(self, config: LoopConfig) -> None:
|
|
249
|
+
self._config = config
|
|
250
|
+
self._budget_fn = _make_budget_fn(config)
|
|
251
|
+
self._retry_fn = _make_retry_fn(config)
|
|
252
|
+
|
|
253
|
+
def __call__(self, output_path: Path, iteration: int) -> bool:
|
|
254
|
+
config = self._config
|
|
255
|
+
prompt_file = config.prompts_dir / "claude-review.prompt.md"
|
|
256
|
+
if not prompt_file.is_file():
|
|
257
|
+
logger.error("Review prompt not found: %s", prompt_file)
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
tmpl = string.Template(
|
|
261
|
+
prompt_file.read_text(encoding="utf-8")
|
|
262
|
+
)
|
|
263
|
+
prompt_text = tmpl.safe_substitute({
|
|
264
|
+
"CURRENT_BRANCH": config.current_branch,
|
|
265
|
+
"TARGET_BRANCH": config.target_branch,
|
|
266
|
+
"ITERATION": str(iteration),
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
if not self._budget_fn("claude", config.budget_scope, 0):
|
|
270
|
+
raise BudgetTimeoutError(
|
|
271
|
+
f"Claude budget timeout (iteration {iteration})."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return self._retry_fn(
|
|
275
|
+
output_path,
|
|
276
|
+
"Claude review",
|
|
277
|
+
[
|
|
278
|
+
"claude", "-p", "-",
|
|
279
|
+
"--allowedTools", "Bash,Read,Glob,Grep",
|
|
280
|
+
],
|
|
281
|
+
stdin=prompt_text,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class ClaudeRefactorReviewAgent(ReviewAgent):
|
|
286
|
+
"""Claude-based reviewer for scope-specific refactor analysis."""
|
|
287
|
+
|
|
288
|
+
def __init__(self, config: LoopConfig, scope: str) -> None:
|
|
289
|
+
self._config = config
|
|
290
|
+
self._scope = scope
|
|
291
|
+
self._budget_fn = _make_budget_fn(config)
|
|
292
|
+
self._retry_fn = _make_retry_fn(config)
|
|
293
|
+
|
|
294
|
+
def __call__(self, output_path: Path, iteration: int) -> bool:
|
|
295
|
+
config = self._config
|
|
296
|
+
scope = self._scope
|
|
297
|
+
|
|
298
|
+
# Refresh source file list each iteration
|
|
299
|
+
source_files_path = config.log_dir / "source-files.txt"
|
|
300
|
+
result = subprocess.run(
|
|
301
|
+
["git", "ls-files"],
|
|
302
|
+
capture_output=True,
|
|
303
|
+
text=True,
|
|
304
|
+
check=False,
|
|
305
|
+
)
|
|
306
|
+
source_files_path.write_text(result.stdout)
|
|
307
|
+
|
|
308
|
+
prompt_file = (
|
|
309
|
+
config.prompts_dir / f"claude-refactor-{scope}.prompt.md"
|
|
310
|
+
)
|
|
311
|
+
if not prompt_file.is_file():
|
|
312
|
+
logger.error("Prompt not found: %s", prompt_file)
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
tmpl = string.Template(
|
|
316
|
+
prompt_file.read_text(encoding="utf-8")
|
|
317
|
+
)
|
|
318
|
+
prompt_text = tmpl.safe_substitute({
|
|
319
|
+
"CURRENT_BRANCH": config.current_branch,
|
|
320
|
+
"TARGET_BRANCH": config.target_branch,
|
|
321
|
+
"ITERATION": str(iteration),
|
|
322
|
+
"SOURCE_FILES_PATH": str(
|
|
323
|
+
config.log_dir / "source-files.txt"
|
|
324
|
+
),
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
if not self._budget_fn("claude", config.budget_scope, 0):
|
|
328
|
+
raise BudgetTimeoutError(
|
|
329
|
+
f"Claude budget timeout (iteration {iteration})."
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return self._retry_fn(
|
|
333
|
+
output_path,
|
|
334
|
+
"Claude analysis",
|
|
335
|
+
[
|
|
336
|
+
"claude", "-p", "-",
|
|
337
|
+
"--allowedTools", "Bash,Read,Glob,Grep",
|
|
338
|
+
],
|
|
339
|
+
stdin=prompt_text,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class ClaudeFixAgent(FixAgent):
|
|
344
|
+
"""Claude-based fixer using configurable two-step fix prompts."""
|
|
345
|
+
|
|
346
|
+
def __init__(
|
|
347
|
+
self,
|
|
348
|
+
config: LoopConfig,
|
|
349
|
+
*,
|
|
350
|
+
opinion_prompt: str = "claude-fix.prompt.md",
|
|
351
|
+
execute_prompt: str = "claude-fix-execute.prompt.md",
|
|
352
|
+
) -> None:
|
|
353
|
+
self._config = config
|
|
354
|
+
self._retry_fn = _make_retry_fn(config)
|
|
355
|
+
self._budget_fn = _make_budget_fn(config)
|
|
356
|
+
self._opinion_prompt = opinion_prompt
|
|
357
|
+
self._execute_prompt = execute_prompt
|
|
358
|
+
|
|
359
|
+
def __call__(
|
|
360
|
+
self, review_json: str, label: str, **kw: object
|
|
361
|
+
) -> bool:
|
|
362
|
+
config = self._config
|
|
363
|
+
log_dir = config.log_dir
|
|
364
|
+
|
|
365
|
+
return claude_two_step_fix(
|
|
366
|
+
review_json=review_json,
|
|
367
|
+
opinion_file=log_dir / f"opinion-{label}.md",
|
|
368
|
+
fix_file=log_dir / f"fix-{label}.md",
|
|
369
|
+
label=label,
|
|
370
|
+
retry_fn=self._retry_fn,
|
|
371
|
+
budget_fn=self._budget_fn,
|
|
372
|
+
prompts_dir=config.prompts_dir,
|
|
373
|
+
current_branch=config.current_branch,
|
|
374
|
+
target_branch=config.target_branch,
|
|
375
|
+
budget_scope=config.budget_scope,
|
|
376
|
+
budget_max_wait=config.retry_max_wait,
|
|
377
|
+
opinion_prompt=self._opinion_prompt,
|
|
378
|
+
execute_prompt=self._execute_prompt,
|
|
379
|
+
fix_history=str(kw.get("fix_history", "")),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class ClaudeSelfReviewAgent(SelfReviewAgent):
|
|
384
|
+
"""Claude-based self-review agent wrapping self_review_subloop."""
|
|
385
|
+
|
|
386
|
+
def __init__(
|
|
387
|
+
self,
|
|
388
|
+
config: LoopConfig,
|
|
389
|
+
fixer: FixAgent,
|
|
390
|
+
) -> None:
|
|
391
|
+
self._config = config
|
|
392
|
+
self._fixer = fixer
|
|
393
|
+
self._retry_fn = _make_retry_fn(config)
|
|
394
|
+
self._budget_fn = _make_budget_fn(config)
|
|
395
|
+
|
|
396
|
+
def __call__(
|
|
397
|
+
self,
|
|
398
|
+
pre_fix_snapshot: list[WorktreeSnapshot],
|
|
399
|
+
max_subloop: int,
|
|
400
|
+
log_dir: Path,
|
|
401
|
+
iteration: int,
|
|
402
|
+
review_json_str: str,
|
|
403
|
+
) -> str:
|
|
404
|
+
config = self._config
|
|
405
|
+
fixer = self._fixer
|
|
406
|
+
|
|
407
|
+
def fix_fn(
|
|
408
|
+
review_json: str, label: str, **kw: object
|
|
409
|
+
) -> bool:
|
|
410
|
+
return bool(fixer(review_json, label, **kw))
|
|
411
|
+
|
|
412
|
+
return self_review_subloop(
|
|
413
|
+
pre_fix_snapshot=pre_fix_snapshot,
|
|
414
|
+
max_subloop=max_subloop,
|
|
415
|
+
log_dir=log_dir,
|
|
416
|
+
iteration=iteration,
|
|
417
|
+
review_json_str=review_json_str,
|
|
418
|
+
retry_fn=self._retry_fn,
|
|
419
|
+
budget_fn=self._budget_fn,
|
|
420
|
+
fix_fn=fix_fn,
|
|
421
|
+
prompts_dir=config.prompts_dir,
|
|
422
|
+
current_branch=config.current_branch,
|
|
423
|
+
target_branch=config.target_branch,
|
|
424
|
+
budget_scope=config.budget_scope,
|
|
425
|
+
dry_run=config.dry_run,
|
|
426
|
+
fix_nits=config.fix_nits,
|
|
427
|
+
original_review_json=json.loads(review_json_str),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ── Factory functions ────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def create_review_agent(
|
|
435
|
+
config: LoopConfig,
|
|
436
|
+
*,
|
|
437
|
+
scope: str | None = None,
|
|
438
|
+
) -> ReviewAgent:
|
|
439
|
+
"""Create the appropriate review agent.
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
config : LoopConfig
|
|
444
|
+
Loop configuration.
|
|
445
|
+
scope : str, optional
|
|
446
|
+
Refactor scope (e.g. "micro", "module"). When provided,
|
|
447
|
+
returns a refactor-specific reviewer; otherwise returns the
|
|
448
|
+
standard review-loop reviewer.
|
|
449
|
+
"""
|
|
450
|
+
backend = config.reviewer_backend
|
|
451
|
+
if scope is not None:
|
|
452
|
+
if backend == "claude":
|
|
453
|
+
return ClaudeRefactorReviewAgent(config, scope)
|
|
454
|
+
return CodexRefactorReviewAgent(config, scope)
|
|
455
|
+
if backend == "claude":
|
|
456
|
+
return ClaudeReviewAgent(config)
|
|
457
|
+
return CodexReviewAgent(config)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def create_fix_agent(
|
|
461
|
+
config: LoopConfig,
|
|
462
|
+
*,
|
|
463
|
+
variant: str = "review",
|
|
464
|
+
) -> FixAgent:
|
|
465
|
+
"""Create the appropriate fix agent.
|
|
466
|
+
|
|
467
|
+
Parameters
|
|
468
|
+
----------
|
|
469
|
+
config : LoopConfig
|
|
470
|
+
Loop configuration.
|
|
471
|
+
variant : str
|
|
472
|
+
``"review"`` for the standard review-loop fixer,
|
|
473
|
+
``"refactor"`` for the refactor-specific fixer.
|
|
474
|
+
"""
|
|
475
|
+
if variant == "refactor":
|
|
476
|
+
return ClaudeFixAgent(
|
|
477
|
+
config,
|
|
478
|
+
opinion_prompt="claude-refactor-fix.prompt.md",
|
|
479
|
+
execute_prompt="claude-refactor-fix-execute.prompt.md",
|
|
480
|
+
)
|
|
481
|
+
return ClaudeFixAgent(config)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def create_self_review_agent(
|
|
485
|
+
config: LoopConfig,
|
|
486
|
+
fixer: FixAgent,
|
|
487
|
+
) -> SelfReviewAgent:
|
|
488
|
+
"""Create a self-review agent.
|
|
489
|
+
|
|
490
|
+
Parameters
|
|
491
|
+
----------
|
|
492
|
+
config : LoopConfig
|
|
493
|
+
Loop configuration.
|
|
494
|
+
fixer : FixAgent
|
|
495
|
+
The fix agent to use for re-fix attempts during self-review.
|
|
496
|
+
"""
|
|
497
|
+
return ClaudeSelfReviewAgent(config, fixer)
|