Coding & Code-Review Agent
The fastest-growing enterprise use case: an agent that reviews pull requests for real bugs and style issues, explains CI failures, and proposes fixes as commits — grounded in your repo's conventions. It reads code, reasons about it, and comments like a senior engineer, with a human always merging.
What this project teaches you to design
- A repo-aware review agent that cites specific files and lines.
- The propose-don't-push safety model (PR comments & suggested commits, human merges).
- Tools for reading a diff, searching the repo, running tests, and posting review comments.
- Code-review evals that reward recall without drowning humans in false positives.
The brief advanced
"Catch bugs and convention violations before a human reviewer spends time — and before they reach production." Human review is a bottleneck and inconsistent. An agent that does a thorough first pass on every PR — flagging real issues with file/line references and suggested fixes — makes human review faster and catches more.
1 · Discovery — where does review time go? advanced
| Where time goes | Agent leverage |
|---|---|
| Reading a diff to understand what changed | ⭐⭐⭐ high — summarize the change |
| Spotting bugs, edge cases, security issues | ⭐⭐⭐ high — the core value |
| Checking style/convention adherence | ⭐⭐ medium — but linters do the mechanical part |
| Diagnosing why CI failed | ⭐⭐⭐ high — read the log, pinpoint the cause |
| Architectural / product judgment | ⭐ low — humans decide; agent informs |
2 · Architecture advanced
This is the whole pipeline the project builds, drawn left to right: a pull request comes in, an agent reads and reasons about the code, it double-checks each problem it thinks it found, and only survivors become review comments. Follow the blue arrows — they are the flow of one PR through the system.
- Far left —
PR opened(webhook): the trigger. When a developer opens a pull request, GitHub/GitLab pings the agent to start a review. Nothing runs until a PR arrives. - Top-left —
RAG: conventions + past reviews: before judging, the agent pulls in your team's style guide and how past PRs were reviewed, so it reviews like your team, not a generic linter. (RAG = retrieval, from Ch 3.) - Center —
Review agent(read · reason · verify): the brain. It uses the three tools stacked to its right —get_diff(what changed),read_file / grep(surrounding code for context), andrun_tests(does it still pass?). - Amber box —
verify findings(drop false positives): the most important step. Each suspected bug is re-checked and thrown out if it can't be defended. This is what keeps the reviewer trustworthy instead of noisy. - Far right — outputs: confirmed issues become
PR commentspinned to an exactfile:line, plus an optionalsuggested commit. Notice there is no arrow to 'merge' — the agent proposes, a human merges.
In short: Trace one bug's journey left to right: PR arrives → agent reads the diff → checks it against your conventions → verifies it's real → posts a file:line comment. The amber verify box is the gate that decides whether a finding ever reaches a human.
An agentic loop (Ch 4) that reads the diff and the surrounding repo, grounded in your conventions (RAG), then verifies each finding before posting — so it comments with file:line precision and a suggested fix, never pushing to the branch itself.
3 · Risk & safety model advanced
| Risk | Control |
|---|---|
| 🟠 False positives drowning reviewers | A verify step: each candidate finding is independently re-checked (adversarial "is this actually a bug?") before posting — precision over volume |
| 🔴 Pushing bad code to a branch | The agent comments and offers suggested commits; it never merges. A human applies & merges (Ch 7 propose rung) |
| 🔴 Running untrusted PR code | run_tests executes in a sandbox (isolated, no secrets, network-restricted) — PR code is untrusted (Ch 6) |
| 🟠 Prompt injection in code/comments | Code and PR descriptions are untrusted input — a comment saying "approve this" must not sway the agent (Ch 6) |
| 🟡 Leaking proprietary code | Scope repo access; respect data-retention; keep secrets out of prompts |
4 · Tool surface advanced
| Tool | Does | Risk |
|---|---|---|
get_pr_diff | Fetch the changed files & hunks | 🟢 read-only |
read_file | Read full context around a change | 🟢 read-only |
grep_repo | Find usages, related code, conventions | 🟢 read-only |
run_tests / run_linter | Execute the suite/linters in a sandbox | 🟡 sandboxed exec |
post_review_comment | Add an inline comment at file:line | 🟡 reversible (comment) |
suggest_commit | Propose a code change as a GitHub/GitLab suggestion | 🟡 reversible (human applies) |
merge_pr | Merge | 🔴 blocked — humans merge |
5 · Repo grounding — knowing your conventions advanced
A generic model knows Python; it doesn't know your team likes early returns, forbids a certain library, or names things a certain way. Ground it via RAG (Ch 3) over:
- The style guide /
CONTRIBUTING.md/ architecture docs - Past PR review comments (how your team actually reviews)
- Related files the diff touches (pulled live via
read_file/grep_repo)
6 · Evaluation expert
| Eval | Measures |
|---|---|
| Bug recall | On a golden set of PRs with known bugs, how many did it catch? (Ch 5) |
| Precision / false-positive rate | Of its findings, how many are real? (the metric that decides adoption) |
| Citation accuracy | Does every finding point at the correct file:line? |
| Convention adherence | Do suggestions follow the team's documented style? (LLM-judge) |
| No-hallucinated-code (deterministic) | Every file/function it references actually exists in the repo |
7 · Phased rollout expert
Skills & course map expert
| Skill | Learn it in |
|---|---|
| Agentic loop reading diffs/files with tools | Ch 4 |
| Repo/convention grounding (RAG) | Ch 3 |
| Verify-before-report (adversarial check) | Ch 5 |
| Sandboxed test execution, propose-via-PR, secrets | Ch 6 + Ch 8 safety model |
| Structured findings (file, line, severity, fix) | Python P4 (Pydantic) |
| The propose-don't-act discipline | Ch 7 FDE |
Build-along plan expert
- Start read-only (Ch 4): tools to fetch a diff and read files from a local repo; produce a change summary.
- Add findings as structured output (Pydantic):
[{file, line, severity, issue, suggested_fix, confidence}]. - Add the verify step (Ch 5 idea): re-check each finding; drop the ones it can't defend — this is what makes it usable.
- Ground in conventions (Ch 3): RAG over the style guide + past review comments.
- Wire to a real repo: post comments via the GitHub/GitLab API; run tests in a sandbox; measure precision/recall on a golden set of historical PRs.
Learning objectives
- Produce structured findings (file, line, severity, fix, confidence).
- Build the two-pass review→verify pipeline (the precision lever).
- Measure recall on known bugs and keep precision high.
What you'll build expert
A reviewer that reads a PR diff, proposes candidate findings, then independently re-checks each one and drops the indefensible — reporting only confirmed issues with file:line and a fix. It never merges; a human does.
llm-course-starter/coding-agent/. The rationale is in the design chapter.Step 1 · The findings schema expert
Setup to run this snippet
from pydantic import BaseModel
from pydantic import Field
from typing import Literalagent/schemas.pyclass Finding(BaseModel):
file: str
line: int
severity: Literal["low", "medium", "high"]
issue: str
suggested_fix: str
confidence: float = Field(ge=0, le=1)
class Review(BaseModel):
summary: str
findings: list[Finding] = Field(default_factory=list)
class Verdict(BaseModel): # the verifier's judgement
is_real: bool
reason: str
Before writing any logic, we declare the exact shape of the data we want back from the model. These are Pydantic models — think of each as a form with typed blanks. If the model returns something that doesn't fit the form, Pydantic rejects it, so bad data never flows downstream. This is the project's contract.
class Finding(BaseModel)is one reported problem. Each field is a labelled blank with a type:file: str,line: int, anissuedescription, and asuggested_fix.severity: Literal["low", "medium", "high"]means severity must be one of exactly those three words — nothing else is accepted. That's how we stop the model inventing junk categories.confidence: float = Field(ge=0, le=1)is a number the model must keep between 0 and 1 (ge= greater-or-equal,le= less-or-equal) — a 0-to-1 sureness score.class Reviewbundles a plain-Englishsummarywith afindingslist;default_factory=listjust means "start empty if there are none".class Verdictis the verifier's yes/no answer later:is_realplus areason.
What the output means: Nothing prints — this file only defines the shapes. Their payoff shows up when .parse(...) forces the model's reply into these exact forms.
Try this: Imagine the model returns severity: "urgent". Because it isn't in the Literal list, Pydantic raises a ValidationError — the schema catching bad data for you, before it can cause a bug.
Step 2 · The mock diff (with real bugs) expert
repo/sample_diff.py is a plain-text diff containing three planted bugs (missing amount check, a TypeError, a missing None-check) plus KNOWN_ISSUES the eval scores against. In production this comes from git diff / the PR API.
repo/sample_diff.pyDIFF = '''...
+ gateway.charge(user.card, amount) # BUG: no positive-amount check
+ log.info("charged " + amount) # BUG: TypeError, amount is a float
...'''
KNOWN_ISSUES = ["amount", "TypeError", "None"]
To build and test a review agent you need something for it to review. Rather than wire up a live repo, this file hard-codes a small diff (the text showing what a PR changed) with three bugs deliberately planted in it — so we always know the right answer to check the agent against.
DIFFis a multi-line string (the'''...'''triple quotes let it span lines). Lines starting with+are newly added code — exactly what a realgit diffshows.- The
# BUG:comments mark the three planted problems: charging with no positive-amount check, aTypeErrorfrom adding a float to a string, and (further down) using a value that might beNone. KNOWN_ISSUES = ["amount", "TypeError", "None"]is the answer key: three short keywords, one per planted bug. Step 6's eval later checks the agent's findings mention each of these.
What the output means: This file just holds data — the diff to review and the list of bugs it contains. In production this text would come from git diff or the PR API instead.
Try this: Add a fourth planted bug to DIFF and a matching keyword to KNOWN_ISSUES. Now the eval expects the agent to catch four — a quick way to make the test harder.
Step 3 · The review pass expert
Uses the client / objects set up earlier in this lesson, and needs ANTHROPIC_API_KEY set. Run the earlier blocks first.
agent/engine.pyREVIEW_SYSTEM = ("You are a senior code reviewer... Report everything you "
"suspect — a separate verification step will filter.")
def review_pr(diff):
return client.messages.parse(model=MODEL, max_tokens=1500,
system=REVIEW_SYSTEM, output_format=Review,
messages=[{"role":"user","content":f"Diff:\n{diff}"}]).parsed_output
This is the first of two model calls: the review pass. Its job is to cast a wide net — surface every problem it suspects. We deliberately tell it to over-report, because a second, stricter pass (Step 4) will throw out the false alarms. Recall first, precision later.
REVIEW_SYSTEMis the system prompt — the reviewer's standing instructions. Notice it literally says "Report everything you suspect — a separate verification step will filter", setting up the two-pass design.def review_pr(diff):takes the diff text and returns findings. It callsclient.messages.parse(...)— the.parsemethod (not plain.create) forces the reply into a shape.output_format=Reviewis the key line: it tells the model "answer as aReviewobject" (the schema from Step 1), so you get a summary and a typed list of findings, not free-form prose..parsed_outputat the end hands back that validatedReviewobject directly — ready to loop over, no manual parsing.
What the output means: A Review object: a one-line change summary plus a list of Findings (file, line, severity, issue, fix) — likely including some over-eager guesses that the next step will prune.
Try this: Read the two-sentence REVIEW_SYSTEM aloud. That single instruction — "report everything, something else will filter" — is the whole philosophy of the review half of the pipeline.
Step 4 · The verify pass (the precision lever) expert
Uses the client / objects set up earlier in this lesson, and needs ANTHROPIC_API_KEY set. Run the earlier blocks first.
agent/engine.pyVERIFY_SYSTEM = ("You are a skeptical reviewer... decide if it is a genuine, "
"defensible bug. Default to is_real=false if unsure. Precision, not volume.")
def verify(diff, finding):
v = client.messages.parse(model=MODEL, max_tokens=400, system=VERIFY_SYSTEM,
output_format=Verdict, messages=[{"role":"user","content":
f"Diff:\n{diff}\n\nClaimed issue: {finding.issue}\n\nReal bug?"}])
return v.parsed_output.is_real
def review_and_verify(diff):
review = review_pr(diff)
confirmed = [f for f in review.findings if verify(diff, f)] # the filter
return review.summary, confirmed
terminalpython agent/engine.py # needs key
SUMMARY: Refactors charge() and refund()...
3 confirmed finding(s):
payments.py:4 [high] charge() no longer checks amount > 0...
payments.py:5 [medium] string concatenation with a float raises TypeError...
payments.py:9 [high] refund() dereferences a possibly-None txn...
This is the second model call — the verify pass — and it's the heart of the project. A brand-new, skeptical reviewer looks at each suspected bug on its own and answers one question: is this a real, defensible bug? Only survivors are kept. This is the precision lever that stops the tool from becoming noise.
VERIFY_SYSTEMis a deliberately harsh prompt: "Default to is_real=false if unsure. Precision, not volume." When in doubt, it drops the finding — the opposite bias from the review pass.def verify(diff, finding):asks the model about one finding at a time.output_format=Verdictforces a clean yes/no answer, and.parsed_output.is_realpulls out just theTrue/False.def review_and_verify(diff):wires the two passes together. Firstreview = review_pr(diff)gets all the candidates.confirmed = [f for f in review.findings if verify(diff, f)]is the filter (the# the filtercomment): it keeps a finding only ifverify(...)returnsTrue. This is a list comprehension — a compact for-loop that builds a new, shorter list.
What the output means: The SUMMARY line, then 3 confirmed finding(s) — each printed as file:line [severity] description. The false positives from Step 3 have been silently dropped; only defensible bugs remain.
Try this: Count the calls: one review call, then one verify call per finding. That's the cost of precision. If you find the reviewer too noisy, the fix is almost always to make VERIFY_SYSTEM stricter — not to touch the review pass.
5 · Tests (no key) expert
terminalpython -m pytest tests/ -v
test_finding_schema_validates PASSED
test_finding_rejects_bad_severity PASSED
test_diff_contains_the_known_bugs PASSED
3 passed
These tests check the parts that don't need the model at all — the schema and the fixture — so they run instantly with no API key. Fast, free tests like these guard the foundations every other step stands on.
python -m pytest tests/ -vruns the test suite.-v(verbose) lists each test by name withPASSEDorFAILED.test_finding_schema_validatesbuilds a well-formedFindingand confirms Pydantic accepts it — the happy path works.test_finding_rejects_bad_severityfeeds a severity outside theLiterallist and confirms Pydantic rejects it — proving the guard from Step 1 actually bites.test_diff_contains_the_known_bugschecks the fixture from Step 2 really holds the planted bugs, so the eval has something real to catch.
What the output means: 3 passed — all three green. Because none of these call the API, they pass the same way on any machine, key or no key.
Try this: Temporarily change a severity in schemas.py to allow "urgent" and re-run — test_finding_rejects_bad_severity will fail, showing you the test is genuinely enforcing the rule.
| Test | Proves |
|---|---|
| schema validates | findings are well-formed |
| rejects bad severity | the Literal guard works (no junk severities) |
| fixture has known bugs | the eval has real bugs to catch |
6 · Evals (needs key) expert
terminalpython evals.py
confirmed findings: 3
[HIT] known issue: amount
[HIT] known issue: TypeError
[HIT] known issue: None
recall on known bugs: 100%
✅ evals passed
Recall is measured against KNOWN_ISSUES; the eval fails if it misses most bugs and warns if it over-reports (a sign the verify step isn't filtering).
The eval answers the question that decides whether this agent is worth shipping: does it actually catch the bugs? It runs the full review→verify pipeline on the planted diff and scores the confirmed findings against the KNOWN_ISSUES answer key from Step 2. This one needs an API key because it calls the model.
python evals.pyruns the whole pipeline once and then grades it.confirmed findings: 3is how many bugs survived the verify filter. Each[HIT] known issue: ...line means a planted bug (amount / TypeError / None) was matched by a confirmed finding.recall on known bugs: 100%means it found all three planted bugs. Recall = of the bugs that exist, what fraction did we catch — the headline quality number.- The note explains the two failure modes the eval watches for: it fails if it misses most bugs (recall too low) and warns if it reports far more than three (a sign the verify step has stopped filtering).
What the output means: A per-bug hit list, a recall percentage, and ✅ evals passed. Together with Step 5's schema tests, this is your evidence the pipeline both works and stays precise.
Try this: After adding a fourth bug in Step 2's 'Try this', re-run — recall is now out of four. This is exactly how you'd grow a real golden set: every bug the agent misses becomes a new case in the eval.
Troubleshooting expert
| Symptom | Fix |
|---|---|
| Too many findings (noisy) | Strengthen VERIFY_SYSTEM ("default to is_real=false"); the verify step is your filter |
| Misses obvious bugs | Make the review prompt over-report; raise effort; ensure the diff is fully in the prompt |
| Wrong line numbers | Include line context in the diff; ask the model to cite the hunk header |
| Verify drops real bugs | Verifier too strict — soften slightly, or pass more surrounding code as context |
ValidationError on severity | The model returned a severity outside the Literal — that's the schema doing its job; the SDK retries |
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: A code-review agent should start with zero blast radius: read-only tools while you build the reasoning, write tools gated behind human merge only later. A reviewer also can't judge a change it can't see.
Your task: Implement read-only tools (get_pr_diff, read_file) as offline fakes and a summarize_diff that reports files and line churn.
Requirements:
get_pr_diffandread_fileas offline fakes (real API stubbed)summarize_diffreports files touched and lines added/removed- It parses a unified diff's
+++/+/-markers - No tool in this milestone can modify the repo
- Demonstrate the summary on a sample diff
💡 Hint: Walk the diff lines: +++ names a file, a leading +/- counts an added/removed line.
Show solution
Start read-only — zero blast radius while you build the reasoning. Offline fakes stand in for the API:
DIFF = '''+++ b/app.py
+def add(a, b): return a - b
-def add(a, b): return a + b
+++ b/util.py
+import os'''
def get_pr_diff(pr_id): return DIFF # real: GitHub/GitLab API
def read_file(path): return "..." # real: repo checkout
def summarize_diff(diff):
files, added, removed = set(), 0, 0
for line in diff.splitlines():
if line.startswith("+++"): files.add(line.split("b/")[-1])
elif line.startswith("+"): added += 1
elif line.startswith("-"): removed += 1
return {"files": sorted(files), "added": added, "removed": removed}
print(summarize_diff(get_pr_diff(1)))
# {'files': ['app.py', 'util.py'], 'added': 3, 'removed': 1}
Read-only tools let the agent gather context safely. Write tools (posting comments) come later, gated behind human merge — never let the first version touch the repo.
Context: Freeform review text isn't actionable. A typed schema is the contract that lets you pin each comment to file:line and filter by confidence downstream.
Your task: Define Pydantic Finding and Review schemas and a labelled messages.parse call that fills them.
Requirements:
Findinghas file, line, severity, issue, suggested_fix, and a 0–1 confidence- Severity is constrained (e.g. a Literal of low/medium/high)
Reviewholds a summary and a list of findings- A hand-built review validates the schema offline
- The model-fill step is a labelled
messages.parsewithoutput_format
💡 Hint: Every finding must carry a line to anchor to and a confidence to threshold on — that's what makes the posting step trivial later.
Show solution
Structured findings are what let you pin comments to file:line and filter by confidence later:
from pydantic import BaseModel, Field
from typing import Literal
class Finding(BaseModel):
file: str
line: int
severity: Literal["low", "medium", "high"]
issue: str
suggested_fix: str
confidence: float = Field(ge=0.0, le=1.0)
class Review(BaseModel):
summary: str
findings: list[Finding]
# offline: a hand-built review validates the schema
r = Review(summary="1 bug", findings=[Finding(file="app.py", line=2,
severity="high", issue="add() subtracts", suggested_fix="return a + b",
confidence=0.95)])
print(r.findings[0].file, r.findings[0].severity) # app.py high
# --- needs API key: model fills the schema ---
# r = client.messages.parse(model="claude-opus-4-8", output_format=Review,
# system="Review this diff. Emit findings with file:line and a fix.",
# messages=[{"role":"user","content":diff}]).parsed_output
The typed schema is the contract: every finding must have a line to anchor to and a confidence to threshold on, so the posting step downstream is trivial.
Context: A single review pass over-reports. A second, skeptical verify pass drops indefensible findings — trading a little recall for a lot of precision, because a bot that cries wolf gets muted.
Your task: Add a verify pass that re-checks each finding and keeps only the defensible ones, modelled offline.
Requirements:
- Each finding is re-checked independently and dropped unless defensible
- A verdict carries whether it's real and a reason
- The offline heuristic keeps high-confidence findings with a concrete fix
- Weak / no-fix findings are dropped
- Demonstrate that a noisy review shrinks to the defensible findings
💡 Hint: The real version is a second messages.parse asking 'is this a real bug, and why?'; offline, gate on confidence plus a non-empty fix.
Show solution
The verify pass is a second, skeptical look that drops indefensible findings — the precision lever:
class Verdict(BaseModel):
is_real: bool
reason: str
def verify(finding):
# --- real: a second messages.parse asking "is this a real bug? why?" ---
# offline heuristic: high-confidence + concrete fix survives
real = finding.confidence >= 0.7 and bool(finding.suggested_fix.strip())
return Verdict(is_real=real,
reason="defensible" if real else "low-confidence/no fix")
def two_pass(review):
kept = [f for f in review.findings if verify(f).is_real]
return Review(summary=review.summary, findings=kept)
noisy = Review(summary="", findings=[
Finding(file="a", line=1, severity="high", issue="bug",
suggested_fix="fix it", confidence=0.9),
Finding(file="b", line=2, severity="low", issue="maybe?",
suggested_fix="", confidence=0.4)])
print(len(two_pass(noisy).findings)) # 1 -> the weak finding is dropped
Reviewers who cry wolf get muted. The verify pass keeps only findings the agent can defend, so the comments developers see are worth reading — precision matters more than recall for a bot.
Context: A generic reviewer flags style the team allows and misses the house rules. Retrieving the team's own conventions lets a finding cite the actual rule, which is far more likely to be actioned than a generic warning.
Your task: Retrieve relevant convention snippets for the changed files and inject them so findings cite the actual rule.
Requirements:
- A small store of team conventions (logging, errors, SQL, etc.)
- Retrieval returns the conventions relevant to signals present in the diff
- Results are capped at top-k
- A finding can cite the rule name it violates
- Demonstrate retrieval on a diff that trips two rules
💡 Hint: Match diff content against a per-rule signal (e.g. an f-string into SQL, a bare print() so the reviewer speaks the team's language.
Show solution
Grounding in retrieved conventions turns opinion into "this violates rule X":
CONVENTIONS = {
"logging": "Use structured logging via log.info(event=..., **kw); no print().",
"errors": "Raise typed exceptions; never bare except:.",
"sql": "Parameterize queries; never f-string user input into SQL.",
}
def retrieve_conventions(diff_text, k=2):
hits = []
for name, rule in CONVENTIONS.items():
signal = {"logging":"print(", "errors":"except:", "sql":"f\""}[name]
if signal in diff_text:
hits.append((name, rule))
return hits[:k]
diff = 'query = f"select * from t where id={uid}"\nprint("done")'
for name, rule in retrieve_conventions(diff):
print(f"[{name}] {rule}")
# [sql] Parameterize queries; ...
# [logging] Use structured logging ...
A finding that cites the team's own rule ("violates conventions/sql: parameterize queries") is far more likely to be actioned than a generic "possible SQL injection". Retrieval makes the reviewer speak the team's language.
Context: Before wiring to a live repo, a bot reviewer must earn trust with numbers — and precision first, because a reviewer that's right half the time gets muted while one that's right 90% gets read.
Your task: Score findings against a golden set of known bugs, report precision and recall, and gate rollout on a precision bar.
Requirements:
- A golden set of known real bugs keyed by (file, line)
- Compute precision and recall from true/false positives
- Report both plus the raw TP / FP counts
- Gate rollout on a precision bar (e.g. 0.8), failing below it
- Optimize precision before recall in the framing
💡 Hint: Precision is TP over everything you flagged; recall is TP over the known bugs — a change that lifts recall by tanking precision must fail the gate.
Show solution
A bot reviewer must earn trust with numbers — high precision first, so developers don't learn to ignore it:
GOLD = {("app.py", 2), ("db.py", 40)} # known real bugs (file,line)
def score(found, gold):
found = set(found)
tp = len(found & gold)
precision = tp / (len(found) or 1)
recall = tp / (len(gold) or 1)
return {"precision": round(precision,2), "recall": round(recall,2),
"tp": tp, "fp": len(found - gold)}
found = [("app.py", 2), ("app.py", 9)] # one real, one false positive
m = score(found, GOLD)
print(m) # precision 0.5, recall 0.5
PRECISION_BAR = 0.8
raise SystemExit(0 if m["precision"] >= PRECISION_BAR else 1) # gate rollout
Optimize precision before recall: a reviewer that's right 90% of the time gets read; one that's right 50% gets muted. Track both each run so a prompt change that boosts recall by tanking precision is caught.
Context: The agent advises; humans decide. On real PRs it posts file:line comments (or suggests commits) but never crosses the line of merging — that button belongs to a person.
Your task: Wire the write tools (post_review_comment, optional suggest_commit) with a hard rule that the agent never merges.
Requirements:
post_review_commentsupports a dry-run before real posting- The real posting path is labelled (needs API key + repo token)
- A
mergeattempt raises a safety error — always - The agent may suggest a commit but never merges
- Demonstrate posting the verified findings and the blocked merge
💡 Hint: Default posting to dry-run so you can eyeball comments first; make merge raise unconditionally as the line the agent never crosses.
Show solution
Writes are gated and merge is off-limits — the agent advises, humans decide:
class SafetyError(PermissionError): pass
def post_review_comment(pr_id, file, line, body, dry_run=True):
if dry_run:
return f"[dry-run] {file}:{line} -> {body[:40]}"
# --- needs API key + repo token: GitHub/GitLab review comment ---
# gh.pulls.create_review_comment(pr=pr_id, path=file, line=line, body=body)
return f"posted {file}:{line}"
def merge(*a, **kw):
raise SafetyError("agent must never merge; humans merge only")
for f in two_pass(noisy).findings:
print(post_review_comment(1, f.file, f.line,
f"{f.issue}\n```suggestion\n{f.suggested_fix}\n```"))
try: merge(pr=1)
except SafetyError as e: print("blocked:", e)
Start every PR in dry_run to eyeball the comments, then flip it on for real posting. The merge guard is the line the agent never crosses — it can suggest a commit, but a human owns the merge button.
✓ Checkpoint — done when…
- The review pass produces structured findings with file:line.
- The verify pass drops indefensible findings.
- Schema tests pass with no key.
- The eval catches the known bugs without a flood of false positives.
| Dimension | Meets the bar | Above the bar (staff-level) |
|---|---|---|
| Recall on real bugs | Recall is measured against a known-bug set; the agent flags the seeded defects with file:line references. | Recall holds across bug classes (logic, security, resource leaks), not just the easy ones, and misses are triaged rather than ignored. |
| False-positive rate / precision | The two-pass review→verify pipeline drops indefensible findings; precision is measured, and noise is low enough that a human trusts the output. | Precision is tuned so reviewers act on nearly every comment; the verify pass is independent (fresh context), not the same call rationalising itself. |
| Safety of any auto-action | The agent proposes — PR comments and suggested commits — and never merges; a human is always the gate to main. | Suggested edits are minimal-diff and reversible; nothing touches CI secrets or runs untrusted code from the diff during review. |
| Grounding to the diff | Findings reference actual files and lines in the PR, not hallucinated code; comments quote the offending lines. | The agent distinguishes changed-line issues from pre-existing debt and scopes comments to the diff so it doesn't relitigate the whole repo. |
| Eval & regression gate | Precision and recall are computed by an eval harness, not by demo vibes; the numbers gate changes to the reviewer. | A regression suite fails CI if precision or recall drops, so a prompt tweak can't silently make the reviewer noisier. |
| Cost & latency per PR | Per-PR token/dollar cost and wall-clock are tracked and stay within a per-PR budget. | Cost scales with diff size, not repo size (retrieval is scoped); large PRs degrade gracefully instead of timing out or blowing budget. |
Score each row 0 (missing) / 1 (meets) / 2 (above). A passing review agent is 9+/12 with safety-of-auto-action at 2 — any path that can merge or mutate main without a human is an automatic fail, and a build whose false-positive rate trains humans to ignore it has failed its one job regardless of recall.
Knowledge check check yourself
Why does the review agent optimize for precision (a verify step that drops findings it can't defend) rather than maximizing recall?
Show answer
Why is merge_pr blocked and why must run_tests run in a sandbox?