Case study
Making an AI agent trustworthy at volume
I gave Claude the keys to a real codebase and built the governance layer that makes its output safe to ship without re-reviewing every line — then instrumented what it cost. Over ~110 supervised hours, Claude shipped 155 reviewed, merged PRs and 26,693 lines of code (~5.3 a day) — an estimated 200–355 hours less than building it by hand. The app Claude built runs an agent of its own — a local model, no cloud key — and instrumenting that one surfaced the number underneath both: across 7 of its runs, human review ran a median of 5.8× longer than the local model, up to 12.1× on one. Whether it's Claude writing the code or a local model drafting a ticket, the model is the cheap part — the human in the loop is the cost, and that's what nobody instruments.
The problem
Every demo shows an agent writing code. None of them show the bill that arrives after. An agent that produces 200 commits of plausible code has produced 200 commits someone now has to trust — and "looks right" scales badly.
For anyone selling or buying agentic tooling, that's the real question: how do you get the volume without inheriting the review burden and the risk of a silent error reaching production? I built a system to answer it end to end — and to measure what it actually costs.
The solution
A governance layer that sits between the agent and the codebase — four guarantees, each trading a little of the agent's speed for something I can point at:
- the agent can only act through typed tools, so it can't corrupt state by improvising;
- the rules are enforced by code, not documentation, so it can't be talked out of them;
- "done" is a gate it can't self-certify past;
- every run's cost is measured, not estimated.
It works. The kanban board this case study is about was built by the agent, operating under these rules, tracked on itself: 264 commits across 23 active days, 235 of them AI-co-authored, 1303 tests.
Verified 2026-07-27, and reproducible from the repo — e.g. git rev-list --count HEAD · npx vitest run.
And because the value was never the board — it was the discipline — I extracted the whole layer into ticket-workflow, a published package that drops the same guardrails into any repo. That's the deliverable: not a clever demo, a repeatable way to make agentic development trustworthy.
What it cost to build (with Claude)
This is the Claude side of the ledger — the cost of Claude Code doing the engineering, separate from the local intake agent the app runs. Volume is only worth having if it's cheap, so I reconstructed the full build economics from Claude Code's own session logs — and committed the script plus a frozen snapshot to the repo, so the method is auditable, not asserted.
The part anyone can check against the git history: 155 reviewed, merged PRs and 26,693 lines of code, about 5.3 merged PRs a day.
Verified 2026-07-21, reproducible from the kanban repo — git log --oneline | grep -Ec '\(#[0-9]+\)'.
The estimate, as a range and a floor: by hand, 155 merged PRs at a deliberately-low 2–3 hours each is 310–465 hours of work — against the ~110 I actually spent supervising, that's ~200–355 hours saved (~5–9 weeks), or roughly $29k–$44k of engineering time avoided at $100/hr, against a measured Claude bill of ~$2,485 — about 12–19× on the spend.
The asterisks, because a number without them is marketing: the dollar cost is self-reported from local session logs — only the PRs, LOC, and velocity are independently reproducible. It's a floor: the CI code-review agent's usage runs on GitHub's servers, not in those logs, so it isn't counted. And the by-hand hours are an estimate — I anchored on merged PRs rather than ticket counts (git-verifiable, and I'd already watched a ticket-based count swing threefold), deduplicated the raw token counts (each streamed response is logged several times), and scoped them to this repo. A case study about honest measurement doesn't get to skip its own footnotes.
Why two models at all? Different jobs. Building the app is a one-time, capability-hungry task where a frontier model earns its keep — the ~$2,485 Claude bill is a rounding error against the time it saved. The intake agent is the opposite: it runs on every report, over operational data you may not want leaving the building — so recurring per-call cost and privacy both point local, and building it that way doubled as a proof that a local-first agent can carry a real feature. Frontier model for the build, local model for the runtime — matching the tool to the job is the point, not a compromise.
Don't take the range on faith — move the assumptions yourself and watch the time, dollars, and ROI recompute.
How it works
Four decisions, each one a guarantee bought by giving up a little speed. The order matters — the later ones only hold because the earlier ones do.
1. Typed tools, not a shell
The board is exposed through a from-scratch MCP server — the agent calls typed functions instead of hand-editing markdown or running grep. Typed tools move the failure from runtime to the boundary: update_ticket validates status against a shared enum, so there's no path where the agent invents status: "in progres" and corrupts the board silently. The intake agent reuses those same handlers as its tools, plus one agent-only tool, search_board, backed by a local retrieval index — the run further down shows it working.
2. Rules enforced by code, not honor-system
The workflow — one ticket, one branch, squash-merged via PR — is enforced by a guard hook that inspects every shell command and blocks the dangerous shapes (git add -A, commits to main, force-push, reset --hard). The insight: an agent that can be talked out of a rule doesn't have a rule. Put it in the prompt and it holds until the context gets long; put it in a hook and it holds because the command doesn't run. GitHub branch protection backstops the same rules server-side.
3. "Done" is a gate, not a claim
The definition of done is a checklist the agent can't self-certify past: typecheck, lint, tests, and a summary that must include a test line — Tests: N added or Tests: none — <reason>. "None" is allowed; silence isn't. Making the skip explicit turns an omission into a decision someone has to defend. Four CI workflows gate every PR.
4. Cost measured, not estimated
The intake agent runs local-first against an OpenAI-compatible endpoint — no cloud key, no per-call charge. Which creates a problem: every "LLM cost" dashboard multiplies tokens by a published price, and locally there is no price. So the cost model reports three categories — measured, assumed, and externalities — and refuses to blur them. That refusal is the whole point, as the numbers below show.
The proof
A governance story is only as good as its instrumentation. Here are two real runs, nothing tidied — first what the agent does with a note, then what a run costs. They're different runs; I'm not going to pretend one recording shows both.
It searched before it wrote
The intake agent's first move on any note is retrieval, not generation. It embeds the note with a local model and ranks it by cosine similarity against every ticket on the board, top five returned. Take one run: someone reported that list_tickets silently truncates at ~440 tickets, and the board already had a ticket for exactly that.
query list_tickets overflow · top 5, by cosine similarity
- 74%list_tickets overflows the tool-output cap at scalebacklog
- 52%Drag-and-drop tickets between columnsbacklog
- 45%Local-first intake agent with retrievaldone
- 42%MCP server exposes the board as typed toolsdone
- 37%Accessibility pass on the board UIbacklog
tool call update_ticket on tkt-list-tickets-output-cap
human gate approved
The agent found the ticket that already existed and called update_ticket on it instead of create_ticket — the anti-duplicate rule working — and a human approved it before anything touched the board. To step through a whole pipeline — the note, every hit and its score, the reasoning, and the gate — walk through three real runs in the replay viewer.
The number that changed my mind
This is the other model — the app's own local intake agent, not Claude. Here's one of its runs, costed — passed back through the same renderSummary() the CLI prints, nothing tidied. The ragged 0.2848888888888889 is why I don't round any of it: real instruments don't round for you.
runId 087f1884-f6af-4689-918d-d05c4c9121d1
at 2026-07-14T16:31:36.484Z
model openai/gpt-oss-20b
calls 2 activeMs 4787 reviewMs 20512
outcome {"created":1,"updated":0,"declined":0,"noProposal":false,"errored":false}
— Measured —
active compute 4787 ms
prompt tokens 2246 tokens
completion tokens 138 tokens
total tokens 2384 tokens
cacheable prefix 89.2 % (estimate (~4 chars/token))
accepted tickets 1 count
— Assumed ($) —
marginal energy notional kWh (notional — set active watts, idle watts)
marginal energy cost notional USD (notional — set active watts, idle watts)
keep-warm energy cost notional USD (notional — set idle watts)
hardware amortization notional USD (notional — set hardware cost)
review time cost 0.2848888888888889 USD (21s x 50/hr)
total run cost notional USD (notional - some cost inputs unset)
manual value (avoided) 4.166666666666666 USD (5min x 50/hr)
cloud-equivalent (openai/gpt-oss-20b) notional USD (notional — no price configured for this model)
— Externalities (report-only) —
water footprint notional L (notional)
carbon footprint notional gCO2e (notional)
— Headline —
cost per accepted ticket notional USD (notional - total cost incomplete)
net savings notional USD (notional - value or cost incomplete)
local vs cloud (saved) notional USD (notional - cloud or local incomplete)The line that reframed the whole project is the timing pair:
4,787ms model computevs20,512ms human review
On this run the human took 4.3× longer than the local model. On the run where I'd have told you the LLM was the expensive part, the local model was 19% of it. A faster model saves ~5 seconds; cutting the review burden — better drafts, a tighter approval UI, fewer decisions — saves 20. The whole industry conversation is about tokens and latency, and for a human-in-the-loop workflow, the human is the cost center. Nobody instruments that, and it was the most useful number I collected.
The caveat, stated plainly: reviewMs is instrumented on 7 of the logged runs — a real sample now, but a small one. Across them the review-to-compute ratio runs from 3.7× to 12.1×, median 5.8×; the run costed in full above, at 4.3×, actually sits near the low end. That's enough to trust the direction — the human dominates the loop — without pretending the exact multiple is a law. A case study about honest measurement doesn't get to round that up.
The win it refused to book
On another run the agent read the note, searched the board, and proposed nothing — an existing ticket already covered it. Watch what the cost model does with that. On the accepted run it books manual value (avoided): 4.166666666666666 USD — the typing the agent saved. Here it books 0: no accepted ticket - nothing avoided. Across 66 runs, 53 earn that credit and 13 don't — $220.83 honest. Booking it unconditionally would read $275.00, a 25% overstatement of the headline from a one-line change nobody would ever see.
If you've ever watched a demo fall apart under a customer's questions, that's the discipline that stops it happening to you. A dashboard that only reports wins isn't measurement, it's marketing — and the first person it fools is the one holding it.
Where it broke
Two things failed, and I'm showing them on purpose.
The seam bugs. The in-app intake feature shipped ~8 real bugs — dropped fields, a silent no-op save, an update misrouted into a duplicate create. Every one survived a green test suite and a per-ticket code review, because they lived in the handoff between modules: each module was correct in isolation, and no single diff-scoped review ever saw the whole path. The fix wasn't more tests — it was one end-to-end round-trip test asserting source input == persisted output across the full chain.
The guard hook failed open. It resolved "what branch am I on?" from the wrong repo, so a genuine direct commit to a sibling's main sailed straight through — the exact class of bug the hook exists to prevent. What caught it, after every unit test passed green, was a suite that drives the real hook against real temporary repos. The same seam bug as the intake feature, in the code I wrote to fix a different one.
Why this belongs in a case study aimed at the people evaluating agentic tooling: the engineer you want in front of your customer is the one who finds the failure before the customer does, and says so out loud. That's the job.
What I'd change
Instrument the human from run one. reviewMs is the most useful number in this study, and it was added late — which is why only 7 runs carry it. The model's tokens were metered from the first call; the human's minutes should have been too.
Write the round-trip test before the feature. Every seam bug above would have died in the end-to-end fidelity test — source input == persisted output — that I only built after they shipped. It's the cheapest test to write before a feature exists and the most expensive to retrofit; I retrofitted.
Make every guard prove it can fail. The guard hook passed a green unit suite while failing open. A green check isn't evidence until you've watched it go red — so now no guarantee counts as one until its test has produced the failure it claims to prevent.
What it means
The takeaway isn't the exact dollar figure — it's that Claude, under a real governance layer, shipped 155 trustworthy, merged PRs in ~110 supervised hours, and I can hand you the receipts. 235 AI-co-authored commits are only worth having if something other than my attention was checking them — the process is what turns volume into something you can actually trust. And the one number I'd still put in front of anyone betting on this space is 5.8× — measured on the app's local agent, but the same lesson holds for Claude: the model is cheap, the human is the cost. Optimize accordingly.