
The bottleneck moved
Generation got fast. Checking did not.
Every other pillar is about steering an agent toward the right answer before it writes a line of code. This one starts after the agent has already run: the code is on a branch, it looks fine, and there are twenty more pull requests waiting behind it.
For most of the history of software teams, the slowest step was writing the code. By the time a pull request appeared, the hard part was over. The author had already run it in their head and thought through the edge cases along the way. Review came last, and it was never the thing holding everything up.
That balance has flipped. A well-prompted agent turns out a working pull request in minutes, and the code still has to land safely. But it now arrives in one drop, so the checking happens afterwards, on a pile that keeps growing, reviewed by a team exactly the size it was last year.

Why manual review does not scale
Eliyahu Goldratt's Theory of Constraints says every system has exactly one bottleneck that caps its output. Speed up anything that is not the bottleneck and the system moves not one bit faster. In software delivery today, that constraint is human code review.
Picture a team whose PR volume jumps from 5 a day to 25 the month they pick up AI tools. At 5 a day, careful review is easy. Run the same process against five times the volume and something has to give. It is almost always quality. Approvals get faster. Comments get shorter. The LGTM rate creeps up. A review that used to take an hour now takes five minutes.
None of that is laziness, just the only sane response to a workload that cannot be done properly. But the final safety check everyone is quietly counting on has turned into a rubber stamp, which gives you the reassurance of a safety net and none of the protection.
Four automated layers, and you
Think of verification as machinery: a pipeline that catches defects and shrinks the blast radius of the ones it misses, running mostly on its own. You are the person standing over it, deciding what to build and making the one call it cannot.
The layers are a stack of filters, each leaning on the ones before it. The early ones are cheap and fast and catch the bulk of the problems. The later ones are slower and pricier, and exist for the handful of things only a live system can reveal.
1 · Deterministic guardrails
Linters, type checkers, security scanners and tests. Hard yes-or-no, no judgment required. Fast and cheap. Runs before merge.
2 · LLM-based review
An AI reviewer that reads the diff for what rules cannot encode: architecture fit, spec alignment, security intent, test quality. Also before merge.
3 · Deploy-time safety
Feature flags, canaries and blue-green strategies that limit the blast radius of anything that slips through. Runs after merge.
4 · Runtime safety
Observability, anomaly detection and AI-driven incident analysis, for what only real production traffic reveals. Also after merge.
You sit outside the stack and above it, doing the work no layer can: setting the intent the pipeline builds toward, deciding which changes the machinery may merge on its own, and judging the few it routes up. The machinery settles whether the code is correct. You answer the question it cannot. Is this the right thing to build, built the right way?
The agent's own first review
This is the feedback half of harness engineering. The other pillars show you how to build a harness that guides an agent. This one shows you how the agent uses that same harness to check its own work before a human ever sees the PR.
An agent works in a loop: reason, act, observe, adjust. The observe step is what closes it. When you run the agent's new code through your tests, a failure becomes an observation the agent can act on. It reads the failure, works out what broke, and tries again. That is why a test suite is the main way an agent can tell whether its own changes actually work.
A good harness pushes this earlier still. The agent runs the whole local gate set (lint, type check, tests, build) before it ever opens a pull request. If something fails, the branch does not get pushed. The agent fixes the error and runs the gates again, and again, until everything is green.
It does not stop at unit tests. Hand the agent a headless browser through an MCP server and it can launch the app, click through a real user flow and look at what actually renders. It writes an end-to-end scenario, runs it, reads the failure, fixes the code, and runs it again. At that point the agent is driving the thing it built and repairing what breaks.
Layer 1 · Deterministic guardrails
Cheap, fast, and not up for debate
Deterministic checks give the same answer every time with no opinion attached. The tests pass or they do not. The code type-checks or it does not. Because they cost almost nothing and leave no room for argument, they should run on every PR before a human or an LLM lays eyes on it.
If you already have a CI pipeline, none of this is new. The only thing that changed is how hard you now have to lean on it, because the volume of code pouring through it just multiplied.
Code health
Linting and formatting
Prettier, gofmt, ESLint, Ruff, golangci-lint. Agents write in subtly different styles from one prompt to the next. A formatter normalises the output the instant it lands, so reviewers spend zero attention on layout.
Dead code and unused deps
Knip reads the project as a graph and reports unused files, exports and dependencies in one pass. Agents leave a trail of abandoned work behind them: an approach they switched away from, a dependency added to test an idea. None of it fails a test.
Type checks and compilation
The highest-signal cheap gate you have, full stop. Models rarely get the shape of the code wrong. They get the APIs wrong, calling a nonexistent method or inventing a field, and a strict check turns that into a red build in seconds.
The test suite
Still the backbone. But when one agent writes both the code and its tests, "all green" is worth less than it used to be. Stop reading coverage as a quality score. A PR can hit 100% and verify nothing.
Test quality
Mutation testing
How you catch tests that lie. The tool breaks working code on purpose, flipping a > to >= or a true to false, then reruns your suite. Tests that stay green never checked that logic. It is slow, so run it on touched files and hold a score on payments, auth and core logic. Stryker, PIT, mutmut.
Property-based testing
Instead of one example, you state a rule that must hold for any input (decode-then-encode returns the original, money is conserved across a transfer) and the tool throws hundreds at it. Built for the edges an agent misses: the empty list, the negative number, the value sitting exactly on the boundary. fast-check, Hypothesis.
Security and supply chain
SAST
Semgrep, SonarQube. Reads source for known bad shapes: SQL built by concatenation, unescaped input, unsafe deserialization, weak crypto. A model trains on a mountain of public code, much of it insecure, so it will repeat an insecure pattern straight back to you with total confidence.
Dependency and container scanning
Run Trivy over your lockfile and your built image on every pull request. Ask an agent for a date parse and you get whatever package it remembers being popular, which may be stale, abandoned or carrying a known hole. A quarterly audit gives that import three months of cover.
Secrets scanning
Gitleaks, as a precommit hook and a CI gate, because hooks can be skipped and CI cannot. An agent stuck on missing env variables will paste the values straight into the code to get unblocked and prove the thing works, then never clean them up.
Infrastructure as code
Checkov, or Trivy's IaC mode. A public bucket behaves perfectly in testing and in the demo. Nothing breaks, and it is one config line from a data breach. Infrastructure mistakes have the widest blast radius of anything, and agents write infrastructure now too.
Supply-chain integrity
An SBOM lists what is inside the artifact, SLSA records how it was built, Sigstore signs it. Earns its place once AI adds dependencies nobody read: the agent pulls in a package, that package pulls in ten more, and one of the ten is the real risk. Skip it for a small internal app.
Dependency updates
Dependabot or Renovate, opening small steady PRs through the same gate set. This is how you close the gap when your scanner reports a CVE, without a human nursing every bump. Group the low-risk ones and automerge what passes clean.
Design and performance
Architecture fitness functions
Tests that assert the shape of the system. The domain layer must not import infrastructure, no circular dependencies, service A never calls service B directly. An agent cares about the change in front of it and feels no loyalty to your layers. Dependency-cruiser, ArchUnit.
Performance and bundle budgets
A hard ceiling on a number that likes to creep. No single AI PR makes the app noticeably slower, it is "only 5 kb more" every time, but agents add code far more readily than they delete it. Set the budget where you stand today, so it fires on regressions. Lighthouse CI.
Query and data-access performance
Where AI-generated code fails most predictably and most expensively. Agents write N+1 queries by reflex, forget indexes, and write joins that are fine on ten rows and melt on ten million, because they see the code and not the shape of your production data. Add an N+1 detector and a slow-query check against a production-sized dataset.
Contract gates
Pact, or OpenAPI diffing. Agents love to "improve" signatures. Renaming a field reads locally as the cleaner design, with no clue that a mobile app is parsing that exact field in production. The change looks like an improvement and lands like an outage.
Accessibility and i18n
Frontend AI output has two reliable gaps: missing labels, weak contrast, keyboard traps and absent alt text; and hardcoded English strings. Both are painful to retrofit. By the time anyone complains, the problem is smeared across hundreds of components. Axe, plus a lint rule for hardcoded strings.
PR size and scope
Small PRs get reviewed and big ones get skimmed, which is why this gate protects all the others. Agents love a giant PR. Ask for one feature and get a 2,000-line change that also refactors three unrelated files. Flag anything over a threshold and ask the author to split it or defend it.
Layer 2 · LLM-based review
Well-formed is not the same as right
A linter tells you the style is clean. A type checker tells you the APIs exist. The suite tells you the tests pass. None of them touches the questions a thoughtful reviewer actually asks, so a PR can sail through every deterministic gate and still be the wrong code, written cleanly.
That gap is what LLM review fills. An LLM can read the diff the way a person would, weigh it against the intent and the code around it, and form a judgment. Unlike the deterministic gates it does not hand you a hard yes or no. It gives an opinion, and like any opinion it can be wrong. What it can do is reason about the questions no deterministic check reaches.
The review can run in three places, and they do not compete:
Locally
On the PR
In the cloud
Most mature setups run more than one. The local pass kills the obvious problems before they are ever published. The PR-stage reviewer then gives the team a shared, visible second opinion.
Review adversarially, in a fresh session
The agent that just wrote a piece of code is a terrible judge of that code. It is attached to its own answer. It reasoned its way there two minutes ago, so when you ask 'is this correct?' in the same session it mostly agrees with itself.
That is the sycophancy problem pointed at review: models tend to agree with what is already in front of them. So do the opposite. Open a fresh session with no memory of writing the code, give it only the diff and the context, and hand it a hostile brief.
A fresh context with that framing turns up a surprising number of first-draft mistakes: the unhandled null, the off-by-one, the case nobody put in the original prompt. One more tweak probably helps, though treat it as a rule of thumb rather than a proven result. Run the pass on a different model. Reviewing code with the model that wrote it is like proofreading your own writing.
Two ways to organise the review
Once you decide an LLM should review your PRs, the next question is how. There are two common shapes, and you can mix them.

Team of specialists
Several agents, each with one narrow job (security, performance, architecture, tests, docs) going over the same PR. A coordinator gathers what they found, strips duplicates, ranks what is left and posts one clean review. Sharp, but you are now running and coordinating a fleet.
Single reviewer with a checklist
One agent handed an explicit list: go through each of these, tell me pass, fail or not applicable, and why. Cheaper, easier to maintain, easier to reason about. Start here. Graduate to specialists only when one area, usually security, deserves its own heavily tuned agent.
What to look for
Working code is the floor here, not the goal. The value of LLM review lives in the questions that go past 'does it run?', and they fall into three clusters.
Does the change fit?
Architecture
Spec alignment
Test quality
Is the change risky?
Security
Privacy and PII
Cost
Is the change finished?
Documentation
Leftovers
Tune the reviewer to your codebase
A reviewer given generic instructions gives you generic comments: textbook advice that may not apply, nitpicks about a style your formatter already owns, the same five suggestions on every PR. Prime it with your context and it starts to feel like a senior teammate.
A well-tuned reviewer carries your conventions, your past incidents, your glossary, and a flat list of files it must never comment on, which is context engineering turned onto the reviewer itself. The shape matters more than the exact words:
# Role
You are a senior backend reviewer for our payments service.
Review the PR diff below. Be concise. Only raise issues that matter.
If the PR is solid, say so and stop. Do not invent problems.
# Our conventions
- All money values are integer cents, never floats. Flag any float math on money.
- Every external API call must have a timeout and a retry policy.
- Database access goes through the repository layer only. No raw SQL in handlers.
# Past incidents to watch for
- INC-204: a missing idempotency key caused double charges. Flag any new
payment write that is not idempotent.
- INC-181: an unbounded query took down the DB. Flag queries without a limit.
# Do not review
- Anything under /generated or /vendor
- Snapshot test files (*.snap)
# Output
For each finding: file and line, severity (blocker / warning / nit), and a
one-sentence fix. End with a one-line verdict: APPROVE or REQUEST_CHANGES.
The conventions are concrete enough to check a diff against. Two real incidents become specific things to watch for, which beats "check for bugs" every time. The prompt says plainly what to ignore, killing a whole class of noise, and it pins down the output format so the result is quick to scan. Every time the reviewer gets something wrong, you edit this file and it sharpens.
Layer 3 · Safe deployment
Treat the deploy as a controlled experiment
The first two layers share one hard limit. They both run before the code meets reality, and some bugs do not exist until production: the race condition that needs real concurrency, the query that only chokes on the full dataset, the integration that behaves one way against a mock and another against the live API.
This was always true. What changed is the volume. Ship 25 AI-generated PRs a day instead of five and your pre-merge layers catch a bigger share than they ever did, while the raw number that slip through still rises.
So the deploy itself has to become a safety layer. None of these techniques is new. Feature flags, blue-green, canaries and automated rollback are well-worn DevOps practice. What AI changes is the stakes. When more code ships faster with less human attention on each change, they go from being nice to have on the scary releases to being the layer that holds up the whole delivery process.
Feature flags
The default for every nontrivial change. A flag splits deployment from release: merge at 10am, let the code sit dark, flip it on at 4pm when you are watching. A kill switch beats a rollback, because a rollback is a whole new deploy with its own risk and its own lag.
Canary deployments
Ship to 1% of traffic, then 5%, 25%, everyone, watching error rates and latency at each step. For changes you cannot fully test ahead of production, which in an AI-native workflow is a big category. The blast radius is 1% instead of 100%.
Blue-green
Two identical environments. Deploy to the idle one, swing all traffic across in one move, swing it back the same way. For high-stakes changes where you want zero downtime and a guaranteed clean exit. It is all-or-nothing, though. Everyone moves at once, so a subtle bug reaches all of them the second you flip.
Shadow traffic
Run the new code against real production traffic and throw its response away. The safest way to test a risky change against real input, and the prime case is an AI rewrite, where you want proof the new version matches the old before you trust it. Only works when the new path has no side effects.
Progressive delivery by segment
Slice by who rather than by percentage: internal users, then a beta cohort, then one region, then everyone. Buys you control over who takes the risk. A teammate hitting a bug is a Slack message. A key enterprise customer hitting it is an uncomfortable phone call.
Automated rollback
Every strategy above assumes a human is watching. At AI volume that assumption snaps. Wire the deploy system to your SLOs so that when a deploy threatens the error budget, it reverts on its own, before anyone is paged.
State is the exception
Flags, canaries and blue-green all lean on the same assumption: rolling back means switching to the old code, at which point the problem vanishes. State breaks that assumption, and it is where teams get burned the most.
No feature flag can undrop a column. No blue-green swap unsends the duplicate emails your retrying worker already fired off. No canary uncharges the customers your non-idempotent payment job billed twice. Application code can usually be rolled back. State usually cannot.
An agent writing a migration sees the code, not the operational world around it. It does not know this table has half a billion rows, so it writes a migration that locks it for an hour. It does not know this consumer can receive the same message twice but must not act on it twice. It produces a change that is correct in isolation and dangerous in production, without a flicker of doubt. So stateful changes need more human attention, not less, even while the rest of the pipeline gets more automated.
Any change that touches state needs a plan
- Backward-compatible migrations: old and new code both work during the transition
- Separate the deploy, the schema change and the backfill into steps you can check one at a time
- Expand and contract: add the new structure, deploy code that uses it, drop the old one later
- Idempotent jobs and consumers, because retries happen
And an operational runbook
- Safe consumer rollout: offsets, ordering, poison messages, dead-letter queues, replay
- Backfill observability: progress, error counts, rate limits, an obvious stop button
- Roll-forward plans, since reverting data may be impossible or make things worse
- Treat a migration on a big table as a planned operation, however tidy the diff looks
Layer 4 · Runtime safety and AI ops
Observability is the price of admission
Layer 3 caps the blast radius of a bad change. This layer is about noticing it in the first place, and about how much of that noticing an agent can now do for you.
Observability rests on three kinds of signal. Logs are timestamped records of discrete events. Metrics are numbers tracked over time. Traces follow one request through all your services. Those three are the raw material, and that has a blunt consequence. You cannot skip the instrumentation. An AIOps agent that promises to find your bugs is only as good as the signals it can read.
OpenTelemetry
Instrument once against an open, vendor-neutral standard and ship the data wherever you like. It matters more now, because AIOps tools are at their best reading across all your signals at once, tying a latency spike to a specific error and a slow span. That only works if the signals share a format.
SLOs and error budgets
"The site should feel fast" becomes "99.5% of requests complete under 500ms". The leftover failure percentage is your budget, which turns reliability into a resource you spend. Budget to spare, ship fast. Budget gone, fix stability first. Its burn rate is what trips automated rollback.
Alerts worth waking for
Symptom-based alerts page on what the user feels, not on CPU at 80%. Burn-rate alerts distinguish a slow leak from a five-alarm fire. If a page is not worth waking someone for, it belongs on a dashboard.
The AI ops loop: detect, analyse, triage, fix
What is new is what you can stack on top once an agent can read every one of those signals. This category already has a name, the AI SRE, and as of 2026 you can build one, buy one from an observability vendor, or host an open-source one.
- 01
Anomaly detection
Models that learn the normal shape of your traffic, latency and error rates, including daily and weekly rhythms, and flag outliers without you hand-tuning a number per metric. The most mature step in the loop. It deals in probabilities, though, so let it surface candidates for a human to look at and keep irreversible actions off it.
- 02
Post-deploy analysis
A sidecar agent wakes on every deploy, compares signals before and after, and pins a regression on the PR that just shipped. Then it either trips the automated rollback or files a bug with the evidence attached. Flagging is safe and valuable. Acting on a fuzzy hunch with no human is where to tread carefully.
- 03
Root-cause analysis
When an alert fires at 3am, the on-call engineer burns the first miserable stretch just gathering context. An agent pulls the traces, recent deploys, error stacks and dashboards and drafts a first cut of the timeline. Draft is the operative word. The agent assembles the evidence and proposes a cause, and the engineer confirms or corrects it.
- 04
Bug resolution
An agent watches errors landing in your tracker, deduplicates against existing tickets, opens one with reproduction steps and a stack trace, and routes it to the team that owns the code. The same pipeline handles human reports. Only the trigger differs. Precision decides whether it helps or just recreates alert fatigue one floor up.
- 05
Back to the coding agent
The ticket goes to a coding agent, which opens a draft PR with a candidate fix. That PR gets no special treatment whatsoever. It re-enters the exact pipeline above, from deterministic gates through LLM review to a flagged, canaried deploy, with a human approving at the gate.

Running the whole loop hands-off, with no human at the gate, is still aspirational. A few teams are stitching the pieces together. Almost nobody lets it run end to end unattended, and for anything that matters you should not want to yet. Build it anyway, one step at a time. Each piece is useful on its own, long before the full loop exists.
The human in the loop
Deciding what needs you
Every layer above lowers the cost of being wrong. But cheaper is not free. A bug in an internal dashboard is an annoyance. A bug in the payment path is lost money and lost trust. A mistake in a migration can be the kind you never fully recover from.
Your judgment is the scarcest, most expensive thing in the system, and the machinery's job is to spend it well. It should route to you only the changes where your attention changes the outcome, and let everything else flow through. Putting a human in front of every PR again would throw away everything the four layers just bought you.
This is the routing question, and it is the narrow end of a wider one. Human-in-the-loop collaboration covers the rest of it: which artifacts are worth reviewing, when an agent should stop and ask rather than wait to be checked, and how to trade speed against accuracy without guessing.

A deterministic gate
One file of path patterns. If any file in the diff matches, human review is mandatory. No AI, no judgment, no exceptions. Its power is in how boring it is.
An AI risk scorer
For every change that does not trip the gate, the scorer reads the diff and proposes automerge or human review, with a plain-language note on what could go wrong.
The gate covers the cases where you already know a human has to look. The scorer takes the long, ambiguous middle, where the honest answer is "it depends" and you want a fast, cheap recommendation instead of a hand-maintained rule.
The gate: changes that always need a human
A single file of path patterns, the same idea as CODEOWNERS or .gitignore. Short enough to read on one screen, and stable enough that you touch it a few times a year rather than a few times a sprint.
# Any change to these paths requires human review.
services/payments/**
**/auth/**
**/*crypto*
**/migrations/**
infra/**
**/*.tf
.github/workflows/**
These are the spots where one bad merge will lose money, leak data or take production down. You can push the same idea further and match on the content of the diff as well as the path: a SQL DELETE or DROP, a call to a known dangerous function, the shape of a hardcoded credential, a feature flag being deleted instead of disabled. Still deterministic, still boring, which is precisely why it belongs in the gate and not the scorer. A destructive SQL statement is not a judgment call.
Deterministic matching only ever catches the patterns you can name in advance. It will never catch the subtle logic error, or the architecturally wrong but syntactically spotless refactor. That gap is what the scorer is for.
The scorer has to fail safe
You are using AI to decide when not to trust AI. That recursion is only safe under one condition.
When the scorer is unsure, it must escalate to a human and never wave the change through. A scorer that fails open, defaulting to "looks fine" when it is confused, is worse than no scorer at all. It hands you false confidence at the exact moment the change was strange enough to confuse it.
You can stand one up today with a prompt:
# Role
You are a release risk assessor. You do NOT decide style or correctness.
You decide how much human review this change needs before it merges.
(The deterministic path gate has already run; you only see changes that
cleared it, so your job is the ambiguous middle.)
# Method
1. Read the file paths first. They tell you where the change lives
(a service, a script, a config) and how much is at stake.
2. Weigh what the change DOES. Consider: blast radius, reversibility,
test coverage of the touched code, diff size, and whether it touches
a hot or fragile file.
3. If you are uncertain, round UP. Default to requiring a human.
# Output (JSON)
{
"risk": "low | medium | high",
"recommendation": "auto_merge | one_human | multiple_humans",
"reviewer_domain": "e.g. payments, auth, frontend, or null",
"what_could_go_wrong": "one or two plain sentences",
"reason": "why this risk level, referencing the diff"
}
Deciding that a human is needed is only half the job. The other half is picking which human. Handing a payments change to someone who has never touched payments is a review in name only. A CODEOWNERS file maps path patterns to owners, and the reviewer_domain field above covers the cases where no ownership file applies.
What is left for the human
By the time a change reaches a person, nearly everything that can be checked mechanically already has been. So the human stops reading line by line.
Hunting for the missing null check or the off-by-one is no longer the point, because the machine does that better and faster than a tired reviewer at 5pm. The human does the thing only a human can do, which is judging intent and fit. Does this match the spec we agreed on? Does it move the system where we actually want it to go? Is this even the right thing to build?
That last question is the one no automated gate can answer, because it rests on goals that live outside the code. It is a better use of a senior engineer's time, and far harder to fake your way through than a line-by-line pass, which is part of why it is the right job to leave in human hands.
Running the layers as one system
Each layer needs the ones under it
Every layer above is useful on its own, and not one of them was designed to run alone. The order matters as much as the parts.
The cheap gates only earn their keep if nothing expensive runs ahead of them. An LLM reviewer needs the deterministic gates reliably green before it has a clean baseline to work from, so that it never burns its judgment on code that will not compile. Put a risk scorer on top of layers that do not yet catch the mechanical problems and you are just shipping bugs faster. And the AIOps loop wants instrumentation, clear SLOs and a safe deploy path underneath it before it can do anything except page you.
Assembling that, and knowing when your team is ready for the next layer, is its own body of work. It did not fit in an essay.