Two Agents, One Repo
The coder doesn't get to grade its own homework. A working method for putting AI on a production codebase — one agent that plans and reviews, one that writes, and durable state that lives in files instead of the chat.

The dangerous thing about an AI coding agent isn't that it writes bad code. It writes good code more often than most people expect. The dangerous thing is that it will tell you the work is done — confidently, in a tidy summary with checkmarks — when it isn't. A skipped test reported as a passing one. A "readiness summary" that says PASS while the build actually died. A cleanup that quietly overwrote the very log that would have shown the failure.
In the last article I argued that "no bugs found" is only trustworthy if the process behind it was built to draw blood. This is the same argument aimed one level up — at the process itself. If you're going to put AI on a codebase you care about, especially one you inherited, the single most important design decision isn't the model. It's who's allowed to say the work is finished, and where that claim gets written down.
The method that's held up for me has two rules. Split the planner from the coder. And keep the state that matters in files, not the chat.
The coder doesn't grade its own homework
When one agent both writes the code and decides it's complete, you've handed the same actor the pen and the red marker. It will optimize for the thing you rewarded — done — not the thing you wanted — correct. This isn't a knock on the model; it's true of humans too, which is exactly why code review exists as a separate seat.
So I run two roles, and I keep them genuinely separate:
- The PM. Plans the work, writes the brief, sets the branch, and — the part that matters — is the only one who can approve. It reviews against the source, not against the coder's summary. It never writes feature code.
- The worker. Codes only when dispatched with an exact, bounded brief. It implements one slice, reports what it did with evidence, and stops. It doesn't decide what's next, and it doesn't get to declare victory.
The PM's verdict is binary: approved or rejected. There is no "looks good to me," no "mostly there." A slice is approved only after the PM independently re-verifies the claims — re-runs the greps, re-reads the changed files, checks the parity rows the worker marked complete against the actual code on both sides. If the proof is missing or wrong, the row goes back to red and the slice is rejected until it's fixed. And the rule that makes the whole thing hold: the worker does not start the next slice until the current one is approved or deliberately rejected. No pipelining half-finished work into a growing pile of "probably fine."
That sounds bureaucratic. In practice it's the opposite — it's what lets you move fast without accumulating a debt of unverified "done"s that you'll pay back as a mysterious production fire three weeks later.
Chat is the wrong place to store anything you need to be true
Here's the part people skip, and it's the part that actually makes the two-agent split work: the conversation is not durable state.
A chat history gets summarized and compacted as it grows. Constraints stated forty messages ago get silently dropped from the working context. A crash, a new session, a handoff to a second agent — and everything that lived only in the thread is gone. If a rule you care about exists only as a sentence someone typed once, it will eventually stop being enforced, and nobody will notice the moment it does.
So the durable state lives in the repo, as files with names and jobs:
qa/agent-current-task.md— the brief. The PM writes it before any code: the decision and its rationale, the branch to cut, an enumerated list of fix items, and pointers to the reference implementation. It's specific enough that a fresh agent with no memory of the conversation could pick it up cold.qa/agent-current-progress.md— the worker's running log while it's mid-slice.qa/agent-current-report.md— the completion report, filed only when the work is actually done, with the evidence attached.qa/agent-needs-pm.md— the escalation channel. Blocked workers write here instead of guessing; the PM answers here instead of in a thread that'll evaporate.- A living audit table — for a migration, every screen or behavior as a row, marked by the worker and independently re-verified by the PM. The table is the source of truth for what's real, not anyone's recollection of it.
None of these are exotic. They're just the discipline of writing down what's true where it can be diffed, grepped, code-reviewed, and survive the process that created it.
This site is its own example. When I moved codebyramiz.com off a dead analytics beacon and onto GA4, the PM decision — replace it entirely, analytics on by default with a per-browser opt-out, and never hardcode the measurement ID — went into a task file first, with the exact files to create and the sibling repo to port from. The worker came back with a report that didn't say "done." It said which files it created, which it edited, and — the part I care about — the two grep commands proving the old beacon was gone from both the source and the built output, and that the ID appeared nowhere in the tree. I didn't have to trust that. I could replay it.
The failure mode this actually prevents
The point of all this isn't process for its own sake. It's a specific, recurring failure: an agent that generates a plausible claim of completion that doesn't survive contact with the evidence.
I've watched a worker add a caveat to a piece of published work — a confident, reader-facing "note" — that had never been approved and wasn't true. Not out of malice; it filled a gap the way these models fill gaps, with something that reads well. In a chat-only workflow, that sails straight through, because the only gate was "the agent said so." With an explicit approval seat and a file that records exactly what was signed off, it doesn't: reader-facing claims are their own line item, and if the PM didn't approve it, it isn't in.
The same discipline catches the quieter stuff. An outreach system I run has a hard rule that every batch is committed to a ledger the moment it's sent — because the send generates state (message IDs, who's now been contacted and must be suppressed) that exists nowhere else. Skip the commit "just this once" and that history is gone the next time the process restarts, and you email someone twice. State that lives only in the run doesn't survive the run.
What to demand if you're putting AI on your codebase
If you're bringing an AI-assisted workflow onto an app that matters — or hiring someone who says they do — four questions separate a real method from a fast way to generate confident nonsense:
- Who approves? If the same agent writes the code and declares it done, there is no review — there's a narrator. Insist on a separate seat that can only say approved or rejected.
- Where does "done" get proved? Ask for the evidence — the failing-then-passing test, the grep, the diff — not the summary paragraph. A report is a claim; proof is replayable.
- What survives a new session? If the constraints live in the chat, they'll be dropped the moment it compacts. The rules you need enforced belong in files.
- What happens when it's blocked? A workflow with no escalation channel produces a worker that guesses. A good one has a place to stop and ask, in writing.
This is the operational spine under everything else I've written about rescuing apps. Diagnosing before you touch anything and proving every bug with a failing test only mean something if the process running them can't quietly lie about the results. AI makes that process faster than it's ever been — and makes the honesty of it matter more than it ever has. The speed is worth having. Just don't let the same agent hold both the pen and the red marker.
Ramiz Raja is a senior mobile developer and technical lead with 12+ years shipping and rescuing production apps on Android, iOS, and Flutter. More at codebyramiz.com.