Oleg Vasienin logoOleg Vasienin

Verification Over Trust: Making AI Agents Prove Their Work

ai-agentsai-code-reviewreliabilityquality-gates

An agent told me four merges were green. I checked. All four were red.

Same afternoon, same day: four "done" notifications. Typechecks pass, tests pass, ready to ship. The director agent didn't believe any of them. It re-ran the typechecks itself, ran the tests itself, grepped the actual diffs to confirm the changes were even there. Four out of four were broken. None reached production, and here's the part that matters: nothing went wrong that day. The system worked exactly as designed.

This is the part of running autonomous AI agents that gets less airtime than prompting or orchestration and matters more than either: verification. AI code review, quality gates, and the discipline of treating everything an agent says as a claim until the system has checked it against reality. What follows is the full map of that layer in my agent org: how it's built, what it has caught, and the incident pattern that convinced me it can never come out. (How the org itself is structured is a separate article.)

"Done" is a claim, not a fact

An agent saying it finished is a sentence, not a state of the world. It's confident, it's plausible, and it's wrong often enough that believing it on faith is how broken code ships on a Friday.

The instinct is to fix this with words. Tell the agent to be careful. Add "always verify your work" to the prompt. It doesn't hold, and it's worth being precise about why: instructions are themselves something the agent remembers, and memory is exactly the thing that drifts. You can't patch a trust problem with more text to trust.

So I stopped trying to make the agents honest. Honesty isn't a setting. The alternative is structural: make the claim cheap and the proof expensive, then only count the proof.

Status nobody can type

In my org, no agent can set a task to "done." There is no writable field for it. Lifecycle status is computed from files on disk: a work marker means in-progress, a completion report means needs-review, a completion report carrying a verification stamp means done, a blocker file means blocked. A guard rejects any attempt to write the status directly.

The two-phase split is what does the real work. The builder writes the completion report. That's a claim, and it only moves the task to needs-review. Then a different actor, the director, verifies the work on disk and leaves the stamp. Only the stamp completes the task. The one who claims and the one who confirms are separated by construction, not by a policy anyone can forget under a long context window.

Why so rigid? Because every status tracker you've ever used rots the same way. Someone hand-edits a label, the label drifts from reality, and you get the card that's been "90% done" for a month. A tracker you can edit is a tracker that will eventually be wrong. Mine is generated from the territory: you want the status to change, you ship the artifact the status is reading.

A fact isn't durable until it's committed

The registry taught me a second lesson the hard way. One night we lost verification stamps, and the forensics showed nothing had deleted them. They'd been written to disk but never committed, and another agent ran a perfectly legitimate git reset --hard in the shared checkout for its own reasons. The stamps didn't get destroyed. They just stopped having ever happened.

In a single-operator repo, uncommitted state is a non-issue; you know what you left there. With multiple agents running git concurrently, uncommitted state is data loss on a timer. The rule since: a marker write and its commit are one operation, a small scoped commit of exactly that file in the same action that writes it. State either exists in history or it doesn't exist. Durability isn't where you wrote the data. It's where the data survives everyone else's legitimate operations.

The verify gate: AI code review against reality, not assertions

Between a green test suite and the final merge sits a separate verifying agent whose only job is to prove the work does what was claimed, on the live artifact, not in the test harness.

Two catches from a single week make the case better than any argument.

First: tests green, merge clean, and the gate bounced it anyway. A convenience re-export in a barrel file was dragging node:crypto into the client bundle. The unit tests exercised the crypto helper server-side, where it belongs, and passed honestly. Nobody had written an assertion about bundle composition, because nobody ever does. That's the kind of miss that surfaces as a broken production build days later, far from the commit that caused it.

Second: a cross-merge regression. Two branches, each green in isolation, and one quietly broke a query helper the other had just changed. Both suites passed. The combination didn't.

Neither of these is a testing failure, exactly. They're scope failures. A test suite checks the code against its own expectations; a verifier checks the claim against the world. Build the real bundle. Run the merged whole. Read what actually comes out.

The economics changed here too. A dedicated skeptic per merge used to be a luxury reserved for release day. Agents made it affordable to run one on every single merge.

Green tests are the work agreeing with itself. Verification is the work agreeing with reality.

A month of reliability incidents, one root cause

I logged a month of incidents from the org. Different days, different subsystems. Lined up, they were the same bug wearing different clothes.

  • An agent reported a "done" that wasn't. It hadn't checked the disk.
  • An agent summarized a source it never actually read.
  • The director reported the 7-day budget at 35% when the live figure was 41%. It wasn't lying, it was remembering: keeping a running estimate in its head and incrementing it cycle by cycle, while the true number sat one tool call away in a snapshot it already reads. Every budget-gated decision was keyed to a figure the agent had invented about itself. (More on the budget machinery in the cost article.)
  • Workers got spawned against a config cached at launch, not the version on disk.
  • My migration tool reported nine unapplied migrations sitting in production. All nine were already in the database, applied off the books months earlier; the tool computed "pending" from its own ledger and never once looked at the actual schema. The map had drifted from the territory, and the tool that trusts the map confidently showed me a road that wasn't there.

One thread runs through all of it: an autonomous system will trust its own claim, memory, or snapshot over ground truth, and it will do it with total confidence. A stale guess and a verified fact come out in the same calm, fluent voice. You cannot tell them apart by listening.

So the fix is never words. It's shape. Take the agent's memory out of the loop for any fact the system already knows. A number that gates a decision gets fetched at decision time, never remembered. Ask the schema whether a migration ran, not the migration log. Decide off the territory, not the map.

Even the watchers need watching

The verification layer is code too, and it inherits the same disease.

At 3am my phone lit up: wrapper alive, process dead. I got up. The process was fine. The zombie detector had learned a rule from the majority of my services (a wrapper spawns a child, so wrapper-without-child means dead) and applied it to the one service that is a single Go binary with no child by design. The monitor was hallucinating corpses, with total confidence, about the healthiest process in the fleet. A health check encodes a belief about how the world is built, and the day reality doesn't match the belief, it pages you anyway. The deeper failure wasn't even the false positive; it was that the noise reached my phone instead of dying at the org.

Later, a cleanup routine nearly reaped a lead agent whose squad was writing code at that exact moment. I had equated "directive empty" with "idle," and in my architecture the first arrives minutes before the second. My favorite part is the fix. I handed a worker the task with a ready hypothesis, gate the reaping on heartbeat, and it came back and proved me wrong: the heartbeat fires every 60 seconds for the life of the process, busy or not, so it distinguishes live from dead, which wasn't my problem. The signal it found on its own was file modification times in the lead's worktree. A busy squad keeps touching files. An idle one leaves the worktree quiet. Trust the evidence of work, not the evidence of existence. And let the agent closest to the code argue with you; that night it was right and I wasn't.

Turning the skeptic on yourself

The same verification muscle points outward.

Before exposing an internal API gateway to the internet for the first time, I aimed an agent at my own surface with a hostile brief: assume you want in, find what's exposed, ground every claim in the live config, no guessing. The findings came back with one line rated critical. The auto-generated REST layer in front of the database had write permissions granted to the anonymous role on the tasks table. No key, no auth: anyone who could reach the endpoint could insert rows. I had reviewed that stack multiple times, as a builder, walking the paths I designed and checking that the intended doors lock. The anonymous grant wasn't on any intended path. The attacker mindset isn't a rare talent anymore. It's a role you can spawn.

Backups get the same treatment. Every disaster-recovery run ends with a restore drill: archive, encrypt, ship off-box to two storage providers, then download the artifact back, decrypt it, unpack it, and count what's inside. The most recent drill restored 164 files, 738,681 bytes, verified. "Backup succeeded" describes the writing. The count describes the only thing that matters. A backup you've never restored from is a hope with a filename.

The playbook: quality gates that hold

If you run agents that write code, the transferable rules are short:

  1. Separate the claimer from the verifier, by construction. The builder never stamps its own work.
  2. Compute status from artifacts. No writable "done" field, and a guard that rejects hand edits.
  3. Verify the live artifact, not the harness. Build the real bundle, run the merged whole.
  4. Commit facts in the same operation that writes them. Uncommitted state is data loss on a timer.
  5. Fetch ground truth at decision time. Any number an agent can remember, it will eventually prefer to the truth.
  6. Audit the watchers. Monitors and trackers encode assumptions, and assumptions drift.
  7. Drill, don't ritualize. A safety mechanism that has never fired in anger is a hypothesis.

None of this needs a framework. Mine is marker files, git, one guard, and a director with standing orders to be suspicious.

The payoff isn't paranoia. It's the opposite: I sleep while the org merges code, precisely because I know what stands between a confident claim and production.

A green checkmark is a hypothesis. Done is what's left after you try to falsify it.