The workflow, as a shape: three separate roles, each checking the last.
Why this exists
This is a workflow I used recently to ship a real production feature — new alphanumeric client/project number support, including several downstream effects on invoicing and reporting that nobody had scoped up front — end to end with Claude Code doing most of the implementation. None of what follows is exotic. It's the same discipline you'd want from a good human team: scope before you build, separate the person writing the code from the person checking it, and verify claims instead of trusting them. The difference with AI agents is that you can run implementer, reviewer, and tester as genuinely separate, cheap, parallel roles instead of hoping one person wears all three hats carefully.
1. Don't start coding until the ticket is actually scoped
The feature arrived as a vague ask. Before writing anything, I pushed back in writing until the real blockers surfaced — payment-reference formats that assumed the old numbering scheme, project-number generation logic that assumed it too, a legacy UI layer nobody had mentioned. Every open question became an explicit, numbered decision, and I got an actual sign-off on all of them before implementation started. It took real back-and-forth, but it meant the build phase had almost no "wait, what did we actually mean by X" moments.
The fastest implementation is the one that never has to backtrack. Spend the first chunk of time turning a vague ask into a checklist someone with authority has actually approved.
2. Split the work by risk and dependency, not by file type
The work split naturally into a small, sequential, high-stakes core (the number-generation logic itself — touches money-adjacent code, every step depends on the last being correct) and a wide, mostly-independent surface (every screen that displays or searches the new field). The core went first, reviewed at every step, run as one continuous thread. Only once it was correct and tested did the wide UI work start — building on a foundation that wouldn't change under it, instead of guessing at behavior that might still move.
3. Implementer → reviewer → tester, as separate roles
For the wide UI work, several implementer agents each took one screen, working from the same spec with no visibility into each other's work. Then a single reviewer — a different model, with zero context on how any of it was written — read every diff against the spec looking specifically for things a same-context self-review would miss. It caught things I wouldn't have: a search filter clause that silently changed unrelated filtering behavior, a double-escaping bug that only broke one specific input shape, and a whole legacy UI code path that had been updated on the frontend but never actually wired up to fetch the new data — nothing the implementer changed could ever have shown up on screen, because the request that would have carried it was never made.
A reviewer who wrote the code will confirm their own assumptions. A reviewer with no stake in the implementation, told specifically what to check, finds a different class of bug than the author ever will in their own code.
The rule of thumb for when this is worth the setup: wide, low-coupling surface area benefits from fanning out implementers in parallel and reviewing once at the end. Narrow, sequential, high-stakes logic doesn't benefit from parallel fan-out — there's nothing to parallelize when every step depends on the last — but still benefits from the same implementer/reviewer separation, just run one step at a time instead of all at once.
4. "No behavior change for anyone else" is a testable claim, not a design intention
The single most important acceptance criterion was that nothing changes for clients not using the new feature — byte for byte, forever. That only stays true if every new code path is gated behind a flag and a per-record condition with no exceptions, and if every touchpoint has an explicit flag-off regression test, not just a flag-on happy-path test. I wrote one for every file touched, and verified each one actually fails if you delete the gate — revert the fix, confirm it goes red, restore it, confirm green. That revert-and-confirm step is cheap, and it's the only way to actually know a test is testing anything.
5. Browser-verify before calling anything done
My implementer agents were upfront that they had no browser tool and couldn't confirm their own UI changes rendered correctly — which is the right thing for an agent to say, not a shortcoming. I closed that gap two ways: Playwright's codegen tool, where a human clicks through the real UI and it generates a runnable test with real selectors live; and screenshots taken mid-script and actually looked at, not just treated as "the test passed." Twice in this piece of work, a passing test turned out to be checking the wrong UI element entirely — only looking at the rendered pixels caught it.
6. Some of the best findings came from just asking "does that actually work?"
A couple of the most valuable fixes came from a plain question, not from process: "shouldn't we be able to search by this new number too?" surfaced that search had never really worked on two separate screens — a pre-existing gap, unrelated to this feature, that a direct question found immediately. "Check every UI, not just the one you tested" found that one search box was doing pure client-side filtering and never touched the backend logic at all, meaning a technically-correct backend fix had almost no real caller.
Don't trust that a fix works because a test passes near it. Ask "does this actually get used" and "did we check every place this needs to hold," out loud, regularly.
7. The collaboration pattern that actually produced these findings
A few things about how the back-and-forth worked, generalized beyond this one feature:
- Raise concerns as plain questions, not instructions. "Wait, could we search by the old number too?" finds more bugs than a longer, more prescriptive spec.
- Every claim gets checked empirically before it's trusted. Not "let me think about whether that's true" — "let me write a two-line script against real data and see." More than once this changed the answer from what seemed obviously true to something more specific and correct.
- Findings get reported with evidence attached, not just a conclusion. "Fixed the search bug" is much weaker than "here's the query before the fix, here's what it returned, here's the same query after — confirmed by reverting and watching the test go red again."
- Scope grows by small increments, not full re-plans. A new finding becomes one more line on an existing sign-off, not a reason to redo the whole plan.
- Decisions that need a human get asked as a real question with real options, at the moment they're needed — not bundled into an end-of-day status update where they're easy to skim past.
- When something is genuinely unverified, say so instead of asserting it. More than once an agent said outright "I have no browser tool, I couldn't check this renders" instead of claiming success — which is exactly what made it obvious where I still needed to look myself.
None of this is about clever prompting. It's treating the agent's output the way you'd treat a junior engineer's pull request — "did you actually check that," "what happens in the case you didn't mention," "show me the evidence" — except in minutes instead of a day, and with several independent checks running in parallel instead of one at a time.
8. "It's green" is a claim about one log, not a fact about the code
Three things worth generalizing from building the permanent end-to-end test suite for this feature: test setup needs the same discipline as the feature itself — no manual database edits, only paths a real integration would actually use, which itself surfaced two previously-invisible gaps in the underlying API. A passing CI job isn't the same claim as "the test passed on its first try" — most CI setups retry a failed test once before marking the job green, which can quietly hide real flakiness behind a "success" label; the only way to catch it is to read the actual run log for retry markers, not just the pass/fail summary. And a heavy local test run on a shared dev machine is not a stand-in for CI's dedicated environment — after a long day of local testing, running the suite locally with concurrent workers produced failures across totally unrelated, already-working tests, which is a sign the machine is under load, not that the tests are wrong.
"It's green" is a claim about a specific log, at a specific concurrency level, in a specific environment — not a fact about the code. Read the actual log before repeating the claim to someone else.
9. Match the verification tool to the actual claim
Later, extending the same feature, browser automation itself became the bottleneck rather than the proof — a UI element intermittently wouldn't register a click, and the dev environment was visibly overloaded from a full day of testing. Rather than keep fighting flaky UI automation, I switched to the backend's own test harness to prove the claim that actually mattered — does the data resolve correctly — directly, without a browser in the loop. It's not a substitute for browser verification; nothing proves "a human can click this and see it" except actually doing that. But for a claim about data correctness, a five-line backend test is both faster and a more precise signal than fighting a flaky UI.
10. Test hypotheses live, don't just read the code
One reported bug turned out to have two independent causes stacked on top of each other — fixing only one would have looked correct in review but silently failed to actually work. Rather than reason abstractly from reading an unfamiliar legacy framework, I tested each hypothesis directly: build the real object the framework constructs, run it against real data, check the real result. The first fix tested as still broken, which is what surfaced the second, independent cause. Reading the code explained why afterward — but testing each candidate fix live is what actually found the second bug.
11. A second, independent reviewer paid off even on work that already felt done
Partway through, I asked a second AI reviewer — a different tool than the one doing the implementation — to review a draft, not-yet-finished pull request. Everything it flagged was real, including a genuine bug that had escaped several rounds of manual review and testing already: a value that happened to be the literal string "0" could have been silently treated as falsy and overwritten. A second, differently-biased reviewer catches a different class of bug than the person who wrote the code, no matter how careful that person was — worth requesting even, especially, on work that already feels finished.
A dedicated review pass done after a feature already seemed finished and browser-verified caught two more real issues later in the same piece of work: an inefficient event-listener pattern that never failed a functional test because it doesn't change what the feature does, only how well it does it; and a permanent automated test that, unlike every other test in its suite, persisted a real setting against a shared account every time it ran — meaning every future test run would have quietly widened that shared account's settings a little more, forever. Neither would have shown up in a demo or a passing test run. "It works" and "the tests are green" are not the same claim as "this is efficient" or "this doesn't leave side effects behind."
12. Root-cause CI-only failures from the actual evidence, not the error message
New tests passed locally every time, then failed in CI on the same step. The error message alone was consistent with several different causes. Rather than guess and add a longer timeout, I pulled the actual CI test-run artifact — the real screenshot/DOM snapshot captured at the moment of failure — and saw the real rendered state: a display setting that happened to be on during local testing but defaults off, which is what CI's fresh environment reflected. The fix followed directly from seeing the real state, not from the error text. "Fails in CI, passes locally" has more than one plausible cause, and the fix looks completely different depending on which one it actually is.
Post-mortem: estimate vs. actual
The original estimate for this feature — written before any real scoping — was about a week, and that estimate was explicitly for the cheapest possible version, not the feature that actually shipped. Actual time logged across the whole piece of work: roughly 28 hours, spread over about two and a half weeks (with a real multi-day pause in the middle for stakeholder scoping — see step 1). What shipped in that time: the new numbering scheme with uniqueness and immutability rules, a working per-record pattern engine, every affected UI surface across old and new frontend layers, several report views with new filters and columns, a follow-up usability feature that the original change made necessary, and full backend plus permanent end-to-end test coverage.
The honest comparison isn't "same scope, less time" — it's "far more scope, in meaningfully less time than the cheap version's own estimate." Edge cases that nobody had even identified as edge cases when the estimate was written only surfaced because the scoping and implementation process kept finding them.
What actually moved the needle wasn't raw typing speed — it was verification-loop speed. Two things did the real work: permanent, browser-driven tests meant I could watch the agent's work happen instead of just reasoning about whether it probably worked, and that kept paying off every time something needed re-checking after a change. And a second, differently-biased reviewer plus a dedicated self-review pass caught real issues that "it works when I click it" never surfaced — those two things don't replace each other; one proves behavior, the other catches things that never fail a test.
What this did not replace: scoping, decisions, and catching wrong assumptions still came from a human, constantly, at almost every step above. The agent didn't run unsupervised — it ran as an implementer that could be trusted to verify its own claims cheaply and repeatedly, which is a more specific thing than just "worked faster."