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.
Files changed (45) hide show
  1. mr_overkill/__init__.py +3 -0
  2. mr_overkill/__main__.py +116 -0
  3. mr_overkill/agents.py +497 -0
  4. mr_overkill/budget/__init__.py +109 -0
  5. mr_overkill/budget/claude.py +353 -0
  6. mr_overkill/budget/codex.py +118 -0
  7. mr_overkill/budget_report.py +98 -0
  8. mr_overkill/classify.py +63 -0
  9. mr_overkill/cli.py +643 -0
  10. mr_overkill/data/.refactorsuggestrc.example +50 -0
  11. mr_overkill/data/.reviewlooprc.example +36 -0
  12. mr_overkill/data/__init__.py +0 -0
  13. mr_overkill/data/prompts/active/claude-fix-execute.prompt.md +13 -0
  14. mr_overkill/data/prompts/active/claude-fix.prompt.md +20 -0
  15. mr_overkill/data/prompts/active/claude-refactor-fix-execute.prompt.md +22 -0
  16. mr_overkill/data/prompts/active/claude-refactor-fix.prompt.md +27 -0
  17. mr_overkill/data/prompts/active/claude-refactor-full.prompt.md +115 -0
  18. mr_overkill/data/prompts/active/claude-refactor-layer.prompt.md +115 -0
  19. mr_overkill/data/prompts/active/claude-refactor-micro.prompt.md +116 -0
  20. mr_overkill/data/prompts/active/claude-refactor-module.prompt.md +114 -0
  21. mr_overkill/data/prompts/active/claude-review.prompt.md +72 -0
  22. mr_overkill/data/prompts/active/claude-self-review.prompt.md +66 -0
  23. mr_overkill/data/prompts/active/codex-refactor-full.prompt.md +115 -0
  24. mr_overkill/data/prompts/active/codex-refactor-layer.prompt.md +115 -0
  25. mr_overkill/data/prompts/active/codex-refactor-micro.prompt.md +116 -0
  26. mr_overkill/data/prompts/active/codex-refactor-module.prompt.md +114 -0
  27. mr_overkill/data/prompts/active/codex-review.prompt.md +72 -0
  28. mr_overkill/git_ops.py +258 -0
  29. mr_overkill/init.py +142 -0
  30. mr_overkill/json_extract.py +205 -0
  31. mr_overkill/loop_engine.py +659 -0
  32. mr_overkill/models.py +293 -0
  33. mr_overkill/refactor_suggest.py +426 -0
  34. mr_overkill/reporting.py +203 -0
  35. mr_overkill/resume.py +143 -0
  36. mr_overkill/retry.py +302 -0
  37. mr_overkill/review_loop.py +56 -0
  38. mr_overkill/self_review.py +345 -0
  39. mr_overkill/time_utils.py +38 -0
  40. mr_overkill/two_step_fix.py +112 -0
  41. overkill-0.2.1.dist-info/METADATA +405 -0
  42. overkill-0.2.1.dist-info/RECORD +45 -0
  43. overkill-0.2.1.dist-info/WHEEL +4 -0
  44. overkill-0.2.1.dist-info/entry_points.txt +2 -0
  45. overkill-0.2.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """Mr. Overkill — AI-powered code review loop."""
2
+
3
+ __version__ = "0.2.1"
@@ -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)