Skip to main content

I Gated My AI Agents. Now Review Is the Bottleneck.

·10 min read·
Khmer decorative icon

In March I wrote about the agent loop: I watched a planner and a reviewer go back and forth on my plan until the reviewer approved it. That loop was the start. Then Opus 5 landed in late July, and the PRs my agents produced got so verbose and unhelpful that I started adding mechanical gates to cut the noise, so I could keep shipping. I wanted better code, and I didn't want my colleagues spending their review time on AI slop.

The gates helped the code. They didn't help me keep track. I'm usually diving into multiple repos at any one time, and it's hard to hold everything at once. With agents doing part of the work in each of them, I kept coming back to the same four questions:

  • What's in flight, and in which repo?
  • Where did I leave off on each one?
  • What did the agents finish while I was away?
  • What's waiting on me?

For someone who loves checklists and todos, the answer had to be a list I could trust. I arrived at it later than I expected. It's a JSON file per feature, and one shell script that's allowed to change it.

At the heart of it is one idea: gate the decisions where it matters, and keep a human in the loop for those. I get to make the decisions. The AI drafts the ticket before anything is filed. I read what it plans to say in a review before it posts.

Since mid-September, plan review has sent the plan back on 10 of 11 features before any code existed. On the Supertopics PR, the gates did enough of the grinding that my teammates approved it in one pass. All of that also moved the bottleneck somewhere I didn't expect.

Khmer decorative icon

i've always loved a good plan

In February I wrote about planning as leverage. That hasn't changed. Planning still gets most of my attention, because it's where I come in. It's the part of being a software engineer I enjoy most: designing how things work together, or taking apart a piece of code until I understand it well enough to build on top of it. My plans are detailed. Each part of a plan is one commit. Each part says what a person can do once it lands. Anything I haven't decided stays marked open.

That last rule matters more than it looks. The design doc puts it this way: "An invented decision in a plan is worse than a gap, because it arrives wearing the format's credibility." A gap gets noticed. A confident sentence in a well-formatted plan gets built.

A good plan tells the agents what to do. It doesn't tell me where they are.

a json file and one script

Every feature I'm working on gets a state file. It records which phase the feature is in, what I've approved, and which review verdicts apply to which commit. One shell script is the only thing allowed to change it. Agents never write to it. They hand their results back, and the script decides whether the state moves.

A simplified sketch of one, with made-up values (the real file carries more):

{
  "feature": "remove-entitlement-gate",
  "phase": "code-review",
  "approvals": {
    "plan": { "digest": "digests/plan.md" }
  },
  "verdicts": {
    "security": { "verdict": "approved", "commit": "a1b2c3d" },
    "tests": { "verdict": "revise", "commit": "a1b2c3d" }
  }
}

That one file answers all four. What's in flight: one file per feature. Where I left off: the phase. What the agents finished: the results they handed back. What's waiting on me: the approval the feature needs.

It's also where the gates live. A gate is a move the script refuses until there's evidence for it: an approval, a review verdict, a receipt from the agent that did the work. Verdicts are pinned to the commit they reviewed, so new code makes them stale on its own.

One line in the design doc sums it up: "Enforcement over memory." I don't have to remember where a feature is, and neither does an agent deep in a long session. The file knows.

the digest

The plan approval in that file points at a digest. Every time the pipeline needs me, an agent shows me one: what it found, what it proposes, what it's unsure about. I reply yes, or I name the point to change. The approval is recorded against the digest I actually saw.

That gives me a decision record without writing one. It's the decision log from an artifact in the last post, moved into the pipeline. Later I can see what I agreed to and what was in front of me.

ticket or no ticket

The last post ended at the ticket, so here's what happens next. If there's a ticket with acceptance criteria, the work goes through the pipeline. If there isn't a ticket, the problem is bounded, and the session already has the context I need from diagnosing the issue, I "vibe code" it with Opus 5.5 or Fable. Acceptance criteria are what the gates hold the work to, and without them a plan and a plan review would be ceremony.

Both paths end in the same place. Whatever I vibe code still goes through the pipeline's review council before anyone else sees it. Review is the one gate nothing skips.

Inside the pipeline, the stages are tier, plan, plan review, ticket, build, code review, compose the PR, and comments. I approve at the plan, where a wrong answer costs a conversation instead of a rewrite, and anywhere the work goes public: filing the ticket, posting the PR, replying to reviewers. Nothing leaves my machine with my name on it unless I said yes. Build and code review run without me, behind machine checks.

An overview map of the pipeline: tier, plan, plan review, ticket, build, code review, compose PR, and comments. Stages are marked YOU where a person approves and AUTO where a machine check decides. Dashed loops show code review sending work back to the build, plan defects sending the build back to the plan, and teammate comments sending a posted PR back to the build. A blocked build escalates to a person, and comments end in a merge. Underneath sits one state file per feature with one writer, which sdev reads but never writes.
The pipeline at a glance. YOU marks where I approve, AUTO marks a mechanical gate, usually a shell script, and the orange loops are where work goes back, including from teammates after it's posted.

The tier at the start comes from measuring what each feature spends. Routine fits most features. Heavy is for changes that touch customer data, span several repos, or introduce new arithmetic. Heavy is the default, and choosing the lighter one is always my call, never an agent's.

Each agent has a job, and the roles got sharper when I defined them by what they never do. The plan writer never makes a design decision. The implementer never works around a wrong plan, widens scope, or pushes. Reviewers never edit. When the implementer finds the plan is wrong, it says so with PLAN_DEFECT and stops, and the plan gets fixed and reviewed again before the build continues. An implementer that quietly adapts erases the correction trail, and nothing fixes the next plan.

A chart of six pipeline roles, each with a NEVER line: the plan writer never makes a design decision, plan reviewers never edit anything, the implementer never works around a wrong plan, widens scope, or pushes, code reviewers never block on taste, ticket and PR writers never file or post, and the main session, the only thing that updates the state file, never lets agents write state themselves. Below it, the implementer's four returns: DONE, DONE_WITH_CONCERNS, PLAN_DEFECT, and BLOCKED.

Every role, and the line it never crosses. The four returns are how the implementer talks back.

what review catches

In September, Supertopics went to every customer. Shipping it meant removing a gate. On paper that's deleting one check. The PR touched 19 files.

Code review ran four rounds and sent the work back to the build twice. The fixes closed permission gaps. The create action had to be gated on the write permission rather than a broader manage permission. A reveal of shared content needed a gate of its own. A scan kept running after its data was gone. Then the work bounced once more at the PR step, because the description didn't match the diff. That's the same kind of noise that started all this, caught before anyone had to read it.

My teammates approved it in one pass. One wrote that the gate was removed cleanly, with no dangling references, and that the create action was gated on the write permission at both places it could fire. By the time a person looked, the gates had done the grinding.

That grinding is code review, which looks at a diff from several angles: security, logic, tests, UX, and the conventions I've written down. What the diff touches decides which run. One more always runs, and that's evidence: does each claim in the work point at something real? Taste isn't a reason to send work back.

The event log I can query starts in mid-September, and in that window it carried 11 features. Code review sent work back to the build 13 times, across 7 features, and most of those came from the evidence checks rather than security. I gave 81 approvals across the 11. It's a small sample, and it's one person's.

what teammates still catch

The document page route went the other way. It passed every gate, and I posted it. Teammate review still sent it back to the build twice.

They caught a hook nothing used, guarded by nine tests. A second hand-written copy of a pattern our shared component library already wraps. Sorting that pushed every sort click onto the browser's Back history, so Back walked through your sort choices instead of leaving the page. A hand-built link that dropped the account ID. Label wording that didn't match between two places on the page.

The gates let all of that through. One reviewer also wrote, about how a failed fetch reads on the page:

Love that a failed collections fetch reads as "Collections not checked" … That's exactly the right call.

The gates caught real problems. Teammates still found the things only people who know the product and the codebase would. The gates just leave them less noise to get through.

what i look at now

I read every digest and every PR. What changed is where my attention goes. The agents do a good job digging through the code, so I've leaned toward QA. Does the flow make sense for the user? Does the PR actually deliver what the ticket asked for? Those are the harder things AI doesn't get right yet.

I'd hope reviewers do the same with my PRs: run our review skill alongside whatever flow they already use, then QA it by hand. Testing by hand matters because AI still doesn't do it well, even with Playwright in the loop. For bigger features, I run the thing by hand at least once, on other people's PRs and on my own. I've caught plenty of design drifts that way.

where it falls short

This is my personal workflow, built for my quirks. It isn't applied to the team. Everyone has their own way of producing code, and this is mine. It produces a lot less AI slop than vibing alone, but some still gets through. That's why I lean so hard on the human parts, and why I keep adapting it, building smarter ways to verify and evaluate what comes out.

It isn't adversarial-proof either. The design doc says the gates defend "against drift and amnesia, not a malicious agent." What they reliably stop is the ordinary failure: a long session where a rule got forgotten, or a plan that quietly stopped matching the code.

One loop isn't closed yet. The design doc's last principle is: "The system learns the way it works." Every recurring failure is supposed to become a gate, a script check, or a line in a conventions pack, never only a conversation. Teammate comments should feed that. The comments stage is designed to sort each catch: was it already flagged, should a review lens have caught it, or should a check have? Recording that consistently is next, and I should do a better job of it. When the same feedback keeps showing up in PR comments, the common themes become PRs of their own, to my process and to my review agents.

It also costs something. Every gate is time, and every review is tokens.

review is the bottleneck now

I built all of this to put a human where it matters, and it worked well enough that review is now the slow part.

Code now comes out faster than people can review it, and PRs pile up at the review gate. That gate is the one nothing skips, and it's made of people.

That isn't unique to us. A lot of companies are hitting a different wall at a different stage of working out AI, and this is ours. If you lead a team, I'd ask what happens to your PR queue when every engineer has a pipeline producing PRs.

Review also runs on trust. Everyone on the team is building a system of their own, with their own quirks, and no single setup fits everyone. I think that's where working with AI is heading: customized workflows. So review has to hold up across all of them.

The goal is to be honest about what actually needs fixing. Ideally the nits go away, and what's left comes from someone who knows the codebase, how the data flows, or what their own agents caught. A shared review tool with input from different engineers is the long-term version of that: room for everyone's own way of working, and checks objective enough to keep code shipping.

That points at the next routing problem. The pipeline already decides which work gets gates. Review needs the same kind of fork: what can ship without heavy input from developers, and what needs the customized review that builds on each engineer's experience. We're still working out where that line is.

The Claude Code team has shared some of what they've learned, and I'm sure others will too. This process will keep changing. We'll keep adapting, and find creative ways through each bottleneck as it comes.

Khmer decorative icon

The answer to "where did I leave off?" lives in a file now. I wanted to see it without opening one. That's sdev, and it's the next post.

This post went through the same loop it describes: I talked it through, Claude drafted from my notes, the pipeline's design doc and event log, and the PR record, and I edited by hand until it read like me.

Related

Khmer decorative icon