Agents Write the Code. Your System Has to Prove It.
When a coding agent says it’s done, that’s a claim. Work is done when checks the agent didn’t write and can’t change say the acceptance criteria are met, and the output of those checks is saved where you can look at it.
You have probably seen the failure this guards against. CI goes green. The PR description says every test passes. Two days later the bug is back, and when you open the diff you find the assertion that used to catch it now expects the wrong value. The agent didn’t lie, exactly. It optimized for the signal it was given, and the signal was “tests pass.”
Code generation is no longer the slow part. Verification is. Over the past few weeks I posted a series of short threads on X about how I verify agent-written code, and I walked through the full method live on The Neuron (recording). This post collects all of it in one place, organized the way I think about it: as a pyramid.
None of these ideas are new to software engineering. Oracles, canary releases, contracts, and code review have existed for decades. What changed is that the agent now writes the code and often the tests too, so the person who used to hold the line between “it runs” and “it’s correct” is no longer guaranteed to be in the loop. The answer is not a better prompt. It’s correctness as a property of the system around the model, not something the model reports.
The pyramid of test and verification engineering.
The pyramid
I frame verification as a pyramid of deterministic checks with the model judge at the very top.
- The ground it stands on: objective acceptance criteria and a test contract, reviewed by a person before any code is written.
- Types, lint, and static analysis.
- Unit tests.
- Integration tests across real services.
- End-to-end tests on a running app.
- A model judge, thin, at the top.
The model should use the deterministic layers to guide its judgment. It should never be the other way around. When a team makes an LLM verifier the foundation of its verification workflow, it gets missed checks and regressions, because LLM judges drift toward longer answers and whatever they read first. Every layer below the judge is cheaper, faster, and gives the same answer twice.
The rest of this post walks the pyramid from the ground up, then covers the three places verification usually breaks in practice: debugging, review, and anything that runs outside the repo.
Write the contract before the code
An agent left alone with a ticket starts writing code in the first minute. The most useful thing an engineer can do is decide what “done” means before that happens.
An objective acceptance criterion is a statement of done that someone other than the author could check. A good one names:
- who is acting and the starting state;
- the action they take;
- the result you can check, on screen and in stored state;
- a numeric threshold, where one applies;
- the kind of oracle that checks it (more on oracles below);
- which dependencies have to be real.
For example, from the Subway Surfers-style game I built during the stream: the player is in the middle lane with a train ahead in that lane, presses left, and within a set number of frames is in the left lane, the train passes, and the run continues. That sentence is checkable by a test, a replay, or a person watching the screen.
Then ask the agent for a testing plan, and read it. Make sure each planned test would actually fail if the behavior were wrong. This matters more with agents than with people, for a reason that’s now well documented: LLM test generators tend to write oracles that capture what the code does, not what it’s supposed to do (Konstantinou et al.; Ruberto & Terragni). If the expected value comes from running the new code, the test agrees with the bug. The oracle has to come from the criteria or a reference before the code exists.
Two habits follow from this:
- Read the test diff before the implementation diff. If an assertion got weaker, a case got deleted, or the thing under test got mocked, the green check means nothing.
- Write tests one at a time, not ten at once. When an agent writes a batch of tests up front, the tests take the shape of its guess at the implementation. One failing test through the public interface, the minimum code to pass it, then the next test.
Instructions in AGENTS.md alone don’t hold this line at scale. Good testing needs
judgment, and an agent told to “write unit tests for every change” will write them
mindlessly. Load a TDD skill when the task needs it, instead of bloating AGENTS.md, and
keep the contract review with a person.
Build the foundation from deterministic checks
Run the repo’s own checks first, then static quality gates, and report what passed and what failed. Types and lint catch an entire class of agent mistakes for nearly zero cost.
For code quality beyond lint, I use the Qlty CLI (open source). It parses code into syntax trees and measures complexity, duplication, and structure deterministically, so “this function got too complex” is a number, not an opinion. Treat it as a supplement to your repo’s own check command, not a replacement.
Be wary of coverage as a goal. Agents can drive line coverage up with tests that assert nothing useful. Coverage tells you what ran, not what was checked.
Use oracles the agent can’t game
An oracle is whatever decides pass or fail by checking a result against something you trust: an expected value, a property, a reference implementation, or a person. Example-based asserts are the easiest oracle for an agent to satisfy without being right. These are harder to game, because they check generated or related inputs instead of a few fixed examples:
Property, metamorphic, differential, and mutation testing.
- Property-based tests check an invariant over thousands of generated inputs. “The output is always sorted” is harder to fake than “this one input returns this one list.”
- Metamorphic tests compare related runs. In the game, the same inputs at 30 and 120 frames per second must produce the same outcome.
- Differential tests run two implementations, such as the old one and the new one, or yours and a reference, on the same input and compare.
- Mutation tests inject small faults on purpose. If your tests still pass with a bug planted in the code, they aren’t testing what you think.
- Approved snapshots change only when a person approves the new snapshot.
- Invariants and contracts are pre- and post-conditions checked on every run.
- Seeded replays re-run a recorded session deterministically, which is why I gave the game an FPS counter and run statistics: building the game built the environment to test it.
The agent can write the property tests. You have to decide which of them matter.
Prove the cause before touching the code
Coding agents are bad at debugging. They pattern-match the error message and patch the symptom, which leaves other edge cases open and sometimes introduces new bugs. Good debugging requires proving the cause before touching the code.
Patching the symptom brings the same error back. Proving the cause runs left to right until the same test passes.
- Write a failing test that reproduces the bug before any fix, so you know the fix actually fixed something.
- Run the investigation in a separate debugger subagent, so dead ends and log dumps don’t fill your main session’s context.
- Require the root cause as a file and line number, plus the command and output that show it, so you can check the diagnosis yourself.
- Give the agent the raw error and full stack trace verbatim, not a summary, so it debugs the actual failure instead of its own paraphrase of it. A summarized failure forces the model to re-explore it, which burns more context than the raw log would have. A passing test in context is the opposite case: drop it, because it’s noise.
- Reject fixes that edit, skip, or delete the failing test deterministically, with a hook or CI check instead of a prompt rule, so “green” can’t come from moving the goalposts.
On that last point, be honest about what your tools enforce. Atomic’s debugger subagent is
told never to suppress a failing test, but that is an instruction in its prompt, not a
runtime check. The deterministic version is something you add: Atomic’s tool_call
extension hook can block a tool call before it runs, or a CI step can fail when the test
file changes.
Review with a session that didn’t write the code
Coding agents are bad at reviewing their own code. “Now review your changes” in the same session mostly produces agreement with itself. There’s a simple reason: a model predicts the next token from the tokens before it, so a reviewer that can see the implementer’s reasoning is biased toward continuing it. Separate the context window and that bias goes away.
Same-session review approves its own story. Fresh reviewers throw out an approval that contradicts its own P0 finding, then reach quorum.
- A fresh reviewer session that never saw the implementer’s transcript.
- The reviewer picks its checks from the acceptance criteria before reading the diff. Read the receipts first and you’re grading the story, not the code.
- Every finding is tagged P0–P2, so you can ignore the nitpicks.
- Approval is decided in code. In Atomic’s goal workflow, two independent reviewers must approve, and a reviewer’s approval is thrown out if the same review says the patch is incorrect, says the goal check isn’t satisfied, or lists a blocking finding.
- A cap on review rounds, so it can’t loop forever. When the same blocker repeats three rounds in a row, the run stops as blocked. When the round budget runs out, it hands off to a person.
- Read the test diff yourself before the implementation diff.
Then add one step before anything merges: map every change to the acceptance criterion it satisfies. A change that maps to nothing is scope creep.
Put the model judge on top, not underneath
Some things code can’t check: whether an error message makes sense to a user, or whether a change matches the spirit of the spec. That’s where a model judge earns its place, at the top of the pyramid. Use it like an instrument, not an oracle of last resort.
Four criteria, one fresh judge each, scored on a defined 1–20 scale. The mean passes, but a single “definitely broken” score fails the change. Illustrative scores.
- Run deterministic checks first (types, lint, tests), so the judge never spends tokens on something a compiler or test could have caught.
- Use a model only for what code can’t check.
- Give the judge a checklist and define what each score means (1 = broken, 20 = proven with evidence), so its scores stay consistent and you can tell a real pass from a guess.
- Give each checklist item its own fresh judge, so a strong item can’t pull up a weak one and the judge isn’t swayed by other results.
- Average the scores, but let any single “definitely broken” fail the whole thing, so one confident failure can’t get outvoted by a few lukewarm passes.
Requiring every judge to agree sounds stricter, but it backfires: with K independent judges
that each wrongly reject a good change 10% of the time, a correct change gets bounced
1 − 0.9^K of the time, about 41% at K = 5 (this follows the reference analysis Atomic’s docs cite;
it is not an Atomic measurement). Mean plus a veto holds up better, and every extra judge is
another set of model calls, so K is a budget decision. Atomic’s adversarial-verification
workflow defaults to this shape: a 1–20 scale, accept at a mean of 14 or above, and a veto on
a confident failure.
Before you add a judge, ask whether a test could check the thing instead. Usually it can.
Use the page the way a user would
A screenshot or a video alone is not verification. Coding agents will tell you a UI change works because the component rendered once. Good frontend testing means actually using the page.
The scenario is written first, then driven step by step in a real browser with DOM and network assertions, then the recording goes on the PR. Illustrative.
Before the agent starts, have it write the user scenario in plain steps:
Log in → open settings → change X → see Y.
Read it. That scenario is your test. Then:
- Have the agent drive a real browser and click through the actual flow. I use agent-browser (open source, from Vercel Labs).
- Assert against the DOM and network requests, not pixels alone.
- Use a real test account, not a mocked session.
- Attach the verification recording to the PR.
- Rerun the exact same scenario after every repair.
The same tool measures performance, which agents are just as quick to declare “fine” after a single local load. One run on one machine is not a performance test.
Five runs on main and five on the branch, compared by median against a 2.5s LCP budget on a mobile viewport. Illustrative numbers.
- Measure Core Web Vitals in a real browser (
agent-browser vitals <url> --jsonreturns LCP, FCP, CLS, TTFB, and INP), save DevTools traces, and profile React re-renders. - Run the same scenario on main and on your branch, several runs each, and compare medians.
- Write the budget down before the change, for example LCP under 2.5 seconds on mobile, Google’s “good” threshold.
- Run a mobile viewport too, not just your laptop screen.
No “before,” no claim about the “after.” For every implementation workflow, I require this full sequence to run as verification before a PR is raised.
Test real traffic with a small blast radius
Coding agents can’t verify a deployment from inside the repo. Unit tests pass, prod breaks, because nothing ever hit a real service. Mocks are the reason: a mock only checks the simulation you wrote. My rule is simple. Run databases and brokers for real in a container, use a dev or test environment for services, and inject hard-to-trigger failures, such as timeouts, into the real service in that environment.
A useful first prompt: find every mock, name the real service each one stands in for, and don’t change anything yet. Give me the list and the plan.
Real traffic is only safe with a small blast radius:
The agent calls a narrow CLI. A fleet-wide command stops at a human gate, gets narrowed to the canary, and the choice is logged. Illustrative.
- A canary environment and dedicated test accounts.
- A narrow CLI the agent can call, instead of raw infra access. An allowlist bounds which commands it can run; it does not bound how much an allowed command breaks.
- A human approval gate on any fleet-wide command, showing the literal command.
- Approve, narrow to canary, or decline, with the choice logged for future audit.
- Manual test procedures kept in a workflow shared across your team, not as scripts in the repo.
Before access is granted, have the agent write a plan to test in the canary environment. Better still, build a dedicated environment for your product that mimics your deployments, with a spend limit on its resources so a runaway test doesn’t become a surprise bill. Engineers stay in the loop, and high-risk actions get approved the way pull requests do.
Count it done only when the evidence is saved
“I ran the tests” is a claim. Completion requires saved evidence: the check output, the test diff, the browser recording, the canary log, the review findings. Artifacts like these let an engineer confirm the work without reading every line of code, which no team can keep doing as agent output grows.
Two more principles keep this practical:
- Scale verification to the change. A 200-line fix gets types, unit tests, and one real-platform run. A large migration gets the overnight run with fresh reviewers and a recording. Plan your own time around the long runs instead of watching them.
- Validation stays with people. Verification asks whether the code does what the criteria say. Validation asks whether the criteria describe what users actually need. The second one is judgment, and it doesn’t delegate.
Where this doesn’t apply
This is a lot of machinery for a one-line typo fix or a throwaway prototype. Scale it down: the contract, the repo’s own checks, and a quick look at the diff are enough there.
It also doesn’t make the hard parts go away. Writing good acceptance criteria is still skilled work. Choosing which property tests matter takes taste. Reviewer approval, even with quorum and contradiction checks, is evidence that gates passed, not a proof of correctness. I want to be upfront: Atomic does not fully solve this either. Verification at scale is still an open problem, and the methods above are what’s working for me so far, not a finished answer.
Make it part of your workflows
You don’t need Atomic to use any of this. Every tip here ports to whatever harness you run, and Atomic is open source if you want to see one implementation, or ask it to explain itself.
Agents write the code. The system decides whether it’s done.
References
- The Neuron livestream with Alex Lavaee: Empirical verification and test engineering for agent-written code
- Konstantinou, Degiovanni, Papadakis. Do LLMs generate test oracles that capture the actual or the expected program behaviour? University of Luxembourg.
- Ruberto, Terragni. From Implemented to Expected Behaviors: Leveraging Regression Oracles for Non-regression Fault Detection using LLMs University of Auckland.
- Atomic, an open-source verifiable coding agent runtime and a fork of Pi.
- agent-browser by Vercel Labs.
- Qlty CLI.
- Core Web Vitals thresholds, Google.
Visuals marked Illustrative use invented file names, scores, and numbers to show the mechanism. They are not measurements.
Stay in the loop
New posts delivered to your inbox. No spam, unsubscribe anytime.