In the past year my team shipped software written almost entirely by AI agents. We shipped plenty of bad features, and some code I’m not proud of, but so far nothing the agents wrote has taken production down. Some of that is luck. Most of it is that we never give the agents the chance to be untrustworthy.
For scale: this is one small team, one product, about a year of working this way. That’s the entire evidence base, so price everything below accordingly.
This post is a map of “never give them the chance.” I’ve written about how my day-to-day work changed when agents got good; this is the system underneath that change. I call the system the constraint spectrum: at every phase of the software lifecycle you already know, there is a menu of constraints running from soft nudges to hard guarantees, and you dial each one by the cost of being wrong. The subtitle of this post is borrowed from a Thinking Machines essay I loved, which describes building technology “that lets the born and the made walk the road together,”1 and trust is what has to be earned for that walk to work.
The asymmetry

AI can produce work faster than you can check it, and the problem isn’t even new. Brandolini’s law, the “bullshit asymmetry principle,” says the energy needed to refute nonsense is an order of magnitude bigger than the energy needed to produce it.2 An unreliable narrator is exhausting to argue with: they’ve got infinite nonsense, you’ve got a handful of facts to check it against, and they don’t beat you on the merits, they just bury you. An LLM used carelessly is an unlimited-nonsense machine, and a human reviewer is exactly the kind of overwhelmed fact-checker that loses.
Computer science has its own version of this. Hand me a program in a normal language and, honestly, I usually can’t tell you what it does without running it; that’s the halting problem sitting underneath. Shrink the language down far enough, into a domain-specific one, and I can tell it’s correct just from how it’s built. Unmesh Joshi made this argument well on Martin Fowler’s site, and his sharpest line applies to everything below: “The problem is not merely code generation complexity, but verification complexity.”3
But there’s an underlooked fact on the other side of the ledger. The same models that produce slop are very good at spotting patterns, because pattern recognition is close to what they are. This year one of these models found 271 security vulnerabilities in Firefox, including a bug that had been sitting in the code for twenty years.4 So an LLM can produce nonsense at scale, and a separate LLM with a clean context and a clean goal can spot nonsense at scale. Bounce them off each other, stack them on the tooling we’ve had for decades, and by the time a human enters the picture, most of the work is already done.
Bun showed what this looks like at scale. In May 2026 its runtime got rewritten from Zig to Rust, about a million lines in eleven days, by 64 agents working as implementers and reviewers, with the old TypeScript test suite as the judge.5 It shipped with 19 known regressions and caught flak for leaning on unsafe, and that’s the trade in plain sight: at a scale no human could have reviewed, the test suite held back the catastrophic breakage, let nineteen smaller bugs through, and those got found and fixed in the open.
METR measures the length of task a model can complete on its own, and that horizon has been doubling roughly every seven months since 2019, faster since 2023.6 Every doubling means more work happening between human check-ins, more surface nobody has read. The constraints are coming either way; the question is whether you install them before the failure or after it.
One note on the word you might have expected here: harness. A harness is one agent’s bundle of picks from these menus. The spectrum is the menu itself.

Planning: the seed
Everything downstream of your idea is capped by the quality of your idea. The maker and checker agents can refine and polish, but the model will only follow the train of thought you gave it; if the thought is really bad the model might push back, but most of the time your input quality is the ceiling of your output quality, and that doesn’t usually change.
So the first constraints apply to the seed. I dictate my intent as a long, detailed rant rather than a one-line ticket, and the rant is literal: my typing never kept up with my thinking, so my prompts start as speech, through a dictation app called Handy.7 A spoken rant carries the context a ticket strips out, and this phase is entirely about context.
I keep taste artifacts where agents can read them: examples of what good looks like to me, in writing, in design, in code. The sparser your instructions, the more a model regresses to the average of everything it was trained on; examples pull it toward yours, and they work at the moment of inference, no fine-tuning required.8
And knowledge lives where agents can grep it. Agents are very good at finding context if you let them, and letting them search plain files usually beats configuring a connector into every tool. We built an open-source tool, notion-sync, that syncs Notion to the file system in both directions, so the documents humans edit in Notion sit in the repo as plain files that any agent can search like code.
The quality gates9 at this phase exist to stop weak seeds. Our spec-writing agent refuses to start if the tools and context it needs are not enabled, because we once watched it write a spec anyway with half its context missing. And it is instructed to grill the user: keep asking questions until you fully understand what needs to be done. A vague idea gets interviewed, not accepted at face value.

Requirements and design: the spec
This is where we run our purest maker-checker setup,10 and the design that matters most is who gets to see what.
The maker is a skill any developer invokes on their machine. It interviews you, researches the knowledge base, and writes the spec. Every spec ships with an HTML mockup, because we found it hard to visualize what a spec meant from words alone. The mockup is dummy HTML with mock data, built on our design system so the UI looks consistent, and it doubles as a light prototyping stage: anyone can pull it, run it, and argue about the actual thing instead of arguing about sentences.
The checker exists only in the CI pipeline. There is no way to chat with it and no way to influence it.11 It never sees my original rant, only the maker’s output. The maker, in turn, knows only a paraphrased version of the checker’s rubric, so it can’t write to the test. Think of preparing for an exam: the maker has the practice tests, so it knows roughly what’s expected, but it is not the examiner, and the checker is the real exam. You know the guidelines; you don’t get to grade yourself.
And because it all runs through GitHub, the checker sees the diff of each new revision sitting right next to the verdict it gave last round. Models aren’t deterministic: give the same spec to the same checker twice and the notes come back slightly different each run. Without that memory a fresh checker basically re-argues the whole spec every time; with it, each round actually goes somewhere.
The score comes back broken out by dimension instead of one lump number, so you can see where the spec is strong and where it’s thin, not just how it did overall. And there’s a hard cap: three rounds of maker-checker, then humans have to step in and brainstorm. It trips maybe once a month, usually when the feature is genuinely complicated, and that is the system working as intended. The machine loop hands the hard ones back.
Maker-checker isn’t just for specs, though. It’s a horizontal wrapper you can apply at almost any phase: plans, code in the middle of a milestone before a pull request even exists, tests. It burns more tokens on purpose (the internet calls that token maxxing12), and it’s a slider you set by how much the work matters, with a soft ceiling on what more checking can buy.
Implementation: the build
The dial for the code itself runs from soft to hard. Prompts and few-shot examples sit at the soft end. Then self-documenting code, with one rule I insist on: agents must document the why, not the what. The code already tells you what it’s doing, and three months later the why is the only thing you’ll wish you had.
Then lint rules and type checks, which hard-fail the pipeline. We use OXLint and write our own custom rules, and the best part of owning the rules is who writes them now: when a model messes something up, we ask the model to write the lint rule that makes its own mistake impossible to repeat. Our custom rules read like a diary of model habits: one bans re-validating request parameters by hand inside a route handler, because models kept adding belt-and-braces checks for things the route’s schema already guarantees, and duplicated checks drift. Another stops imports from reaching deep into another module’s internals, because models will happily couple to anything they can see.
Then schemas and structured outputs, where the model’s answer must parse or it doesn’t exist.
The shape of the work is a constraint too. Big features get built as vertical slices that fit together like Tetris, each slice independently testable, rather than as a waterfall where everything collapses together at the end. Each slice arrives as a draft pull request that I review as a human, a loop I walked through, flowchart included, in the previous post.
The far end of this dial is a DSL, a language constrained enough to be correct by construction. I don’t write DSLs for my day job and I’m not telling you to; it’s a reference point that shows which direction “harder” points. But we borrow the idea where it pays. In our production systems, agents never see database UUIDs: a wrapper converts every UUID to a short reference on the way in, and every reference back to a UUID on the way out. I built it after watching models invent identifiers with total confidence, and it’s the purest example I have of the hard end of the spectrum: not a guardrail that catches the failure, but a constraint that makes the failure impossible, because the vocabulary the failure needs has been removed. That single wrapper is a large part of why a harness can make a small model reliable, and why we can run much less intelligent models than we otherwise could.
Testing: the proof
Unit tests and smoke tests carry over from the old world unchanged. Property-based testing gets more useful, not less. You tell it the shape of an input and let it hammer at that shape.13 Say something’s supposed to be an email: it’ll fling a hundred thousand ugly emails at your parser and find out whether it actually holds, which is exactly the beating AI-written code should take before a user gives it one.
Evals are the newer member of the family, and ours run every day, which turns model drift from a vague worry into an alarm that fires. They also chase a goal I’ll write about separately. Evals are mostly discussed as a way to compare models against each other, and that’s fine if you’re the one producing the models, but maybe 90% of the conversation belongs to the producers. We’re consumers, and a consumer’s eval has a different job: minimize the variation in your product across models, and tune for the least intelligent model that still clears the bar, because a customer arriving with a weaker model shouldn’t get a subpar product. Getting there is hard work. A less intelligent model has less autonomy, so you chew the food for it, the way birds do for their chicks: take the big workflow an intelligent agent could figure out on its own, break it into small, heavily constrained slices, and chain them.
The checker agents work at this phase too: one AI impartially catches issues and another fixes them, in a recursive loop, stacked on all the static tooling above.
This stack is what made the biggest refactor I did this year boring, in the best sense. We migrated our entire Vue component library from .vue single-file components to TypeScript TSX, using agents plus static migration tools I built: the static tools converted what they could mechanically, and the agents took care of the rest. Because the test coverage was good going in, around 95% of the work landed without a single bug. What actually bit us were migration artifacts, global Vue and Nuxt configuration changes, and some minor class-ordering issues. Almost everything else worked out of the box.
And the human samples. Review depth scales with stakes: business-critical code gets every line read, normal code gets sampled, isolated throwaway code sometimes gets no reading at all, because realistically, nobody honestly reads every line anyway. What agents change is that you can choose the sample. On one large dataset-building job, I have a committee of agents judge the items and route the ones where they disagree into my review pile, thirty at a time. Machine-learning researchers named this pattern query by committee three decades ago;14 the difference now is that the committee costs pennies. Sometimes I flip the strategy, because the committee itself might carry biases, and take twenty purely random items instead. My comments on those samples then become the golden examples that future agents get prompted with, so each round of human judgment compounds back into the machine.

Deployment: the blast door
Nothing here makes the work any less wrong. What it does is make being wrong cheap, so a mistake costs you a rollback instead of a night.
Big changes live on lab branches with preview deployments, so they never touch main until they’ve been walked through. Features ship behind flags that default to off. Releases are reversible. And the approval model is inverted: approved by default, veto as the action. If someone wants to be unblocked, they are unblocked; if someone disagrees, they can step in at any time and say no, let’s change this. That flip is a big reason we’ve been able to grow the team fast: nobody sits around waiting to be let through a gate, and someone steps in only when they spot a real problem. Google’s whitepaper describes a cousin of this idea as “conditional LGTM”;15 ours goes further, because rejection is treated as the exception rather than approval as the gate.
The inversion is only safe because every constraint earlier in the pipeline has already done its job. You could be shipping something dangerous, and because the guardrails are in place, through process and through technology, you’re safe.
Acceptance and maintenance: where reality answers
The first constraint here technically straddles the line between acceptance and maintenance, and I don’t much care where it gets filed, because it’s the only rule in this post with no exceptions: a human actually uses the feature. Whatever the AI built, someone on the team uses it the way a customer would and checks that it makes sense and that it’s tastefully done. I’ve called this the taste test before: someone made food, and you taste it.
The second is the audit trail. Every AI call our agents make, system prompt, messages, tool calls, and tool outputs, is stored in a plain Postgres table. That table is what makes our agents durable in the first place: load the rows and you can replay any conversation and continue it. The same table is how we debug, because “the agent botched this tool call four times this week” becomes a query instead of a hunch, and bad tools get found and fixed instead of quietly retried forever. It’s where we mine golden examples for future prompts, and it’s raw material for evals, because the agent’s own best runs are a test set nobody had to write. When something misbehaves in production, and for us it has mostly been infrastructure rather than agent output, you can trace almost every single thing that was done.
The capability envelope
Everything so far constrains what agents did. One layer constrains what they can do, and it’s the reason my honest answer to “tell me about a time an agent did something it shouldn’t have been able to” is that it has never happened, or at least that nothing of the sort has ever surfaced in the audit trail, and not because the models are polite: we don’t leave the option open.
The envelope bounds what an agent can be tricked into doing, not whether it can be tricked. Prompt injection that stays inside granted permissions is a real risk, and it belongs to the checker’s half of the house, which I’ll get to.
On a developer’s machine, the agent is an extension of the developer. It executes with your credentials, so whatever you cannot do, it cannot do either; if you don’t have admin on the cloud account, neither does your agent. It’s a very natural sandbox, and it means the blast radius of an agent is never bigger than the blast radius of its human.
In production the sandbox is explicit. Tools are JSON schemas and structured API calls, and when an agent must run code, it runs inside just bash or QuickJS in WebAssembly, where it can compute without touching anything else. Permissions stay bound to the invoking user: if an intern uses our production agents, the agent holds the intern’s tokens and nothing more. You cannot convince an agent to delete something you aren’t allowed to delete. It will try, and it will get permission denied.
And every major provider lets you set budget caps at the organization level. We use them quite literally; they’re what lets us go to bed knowing we will not wake up broke.
Gradients and booleans
A mindset runs underneath all of this, and it took me a while to say it plainly: don’t think about any of it in terms of pass and fail. I never say this maker-checker run worked and that one failed. Real life is way too messy for that. It’s almost always about conveniences and inconveniences, about having a day where most things were efficient, and failing is a very strong word for what actually happens. Outcomes live on a gradient, and the more time you spend at the better end of the gradient, the better your life becomes.
The tripwires are different. The checker either has its tooling or it rejects; the flag is off until someone turns it on; the third round either ends with agreement or with humans in a room. Outcomes I judge on a sliding scale. The tripwires don’t slide; they either fire or they don’t.
Choosing your point
No two tasks deserve the same dial settings, and the dial is the cost of being wrong. Estimation changed shape here too: days-based estimates stopped making sense when things started happening in hours, but t-shirt sizing works again. A rough size, a criticality call (an experimental feature, by default, cannot be critical), and a sense of how many decisions are hiding inside the feature: those knobs pick the path.
A throwaway internal tool barely staffs the counters. I’ve shipped things where I wrote zero lines and reviewed zero lines, and only sampled the output (the taste test still happened; using the thing is not the same as reading its code). The real product staffs every station. Business-critical work reaches for the hard end of each column, up to reading every line, and that tier keeps shrinking, because the guardrails are what shrink it. The full decision tree, with the flowchart, is in the previous post.
What constraints can’t buy
There is a boundary, and it’s worth being honest about it: constraints buy correctness, and correctness is not taste.
I can force an agent onto a style guide, and it will sometimes violate it. I can force it onto a design system where it may only use components that exist, which is about as close to a DSL as design gets. What comes out is correct, and it still isn’t pleasant. Models choke on the judgment calls of good interfaces: when not to show a button, what to hide behind progressive disclosure, when less is more. You can often tell which model built a design, because designs from the same model share the DNA of its training. With today’s models, no matter how many constraints you put on one, you can get a correct UI, but you can’t get a pleasant UI. For now, that part stays with the born.
A note from the maker’s side
One admission for a post that leans this hard on maker-checker: I’ve written all of it as a maker, a developer talking about shipping. The checker’s half of the house, supply-chain provenance, prompt-injection hardening, data privacy, the licensing of generated code, deserves its own field guide from someone wearing that hat, and its reading list is already excellent.16 17 18
Where to start on Monday
If you adopt one constraint per phase this month, these are the ones I’d pick, cheapest first:
- Planning: write your next feature request as a spoken rant instead of a one-line ticket, and make your docs greppable from the repo.
- Requirements: before anything gets built, have one agent write the spec and a second agent with a fresh context review it. Two sessions, zero tooling.
- Implementation: take the last mistake an AI made in your codebase and have the AI write the lint rule against it. Then tell your agents to document why, never what.
- Testing: point property-based testing at your most fragile parser or transform.
- Deployment: put your next feature behind a flag that defaults to off.
- Maintenance: log every agent call somewhere you can query. A single database table is enough.
None of these need a platform or a budget, and each one is a click of the ratchet, which is the last thing I want to tell you about.

The ratchet
Everything above fits on one page:
None of the quality gates in that diagram were designed up front. Every gate began life as a failure.
The spec writer and the spec checker started out as two skills on the same machine, and one day someone accidentally invoked the checker inside the maker’s chat. The output got drastically worse, because a model that can see the goal will bend its judgment toward it; the checker turned agreeable. That failure is why the checker now lives only in the CI pipeline, where nobody can talk to it.
The refuse-to-start gate exists because a developer once ran the spec writer without the right tooling connected, and it happily wrote a spec with half its context missing. The grill-the-user instruction exists because agents used to take a vague idea at face value and just run, and the output can’t be better than the input. The three-round cap exists because the loop once needed a human and didn’t ask for one.
The purest version of the loop lives in our lint setup: the model that made the mistake gets asked to write the rule against it.
Each failure pushed us toward the bad end of the spectrum. Each constraint we installed pushed us back, one click, and the click holds.19
The next time an AI gets something wrong in your codebase, don’t just fix it. Ask which constraint would have made that failure impossible, and install it. Every failure converts into one permanent click up the spectrum, and if you do that for a year, you’ll have a constraint spectrum of your own.
The born and the made are going to walk the road together either way. Constraints are how the made earns it.
Footnotes
-
I found the image in Thinking Machines Lab’s The Future Worth Building Is Human (2026), but “the born and the made” is older: Kevin Kelly opened Out of Control with it in 1994, in a chapter literally titled “The Made and the Born.” Good images get borrowed; this post’s subtitle borrows it again. ↩
-
Brandolini’s law, coined by Alberto Brandolini in 2013. An observation rather than a theorem, but it has held up painfully well. ↩
-
Unmesh Joshi, LLMs and DSLs (2026), on martinfowler.com. ↩
-
Anthropic’s Claude Mythos, reported in The Hacker News: 271 vulnerabilities in Firefox, including flaws 15 and 20 years old. ↩
-
Simon Willison, Rewriting Bun in Rust, and The Register’s coverage of the merge. ↩
-
METR, Measuring AI Ability to Complete Long Tasks, and the accelerated estimate in their Time Horizon 1.1 update (January 2026). ↩
-
Handy: free, open source, fully offline speech-to-text. Plenty of similar apps exist; I just haven’t had a reason to switch. ↩
-
In-context learning, first characterized at scale in Language Models are Few-Shot Learners (Brown et al., 2020). Examples are a form of soft constraint the model picks up at inference time. ↩
-
“Quality gate” is standard CI/CD vocabulary: a set of measurable pass/fail conditions at a pipeline checkpoint. See SonarQube’s definition. ↩
-
Maker-checker, a central authorization principle in the information systems of financial organizations, closely related to the four-eyes principle. ↩
-
The isolation isn’t superstition. Models measurably favor their own generations, and LLM judges carry known biases (position, verbosity, self-enhancement) that a clean context and a rubric are there to fight. ↩
-
Built In’s explainer covers both the slang and the corporate leaderboard stories that made it famous in April 2026. ↩
-
fast-check for TypeScript, Hypothesis for Python. ↩
-
Query by committee (Seung, Opper & Sompolinsky, 1992), from the active learning literature. ↩
-
Google, The New SDLC With Vibe Coding (May 2026), and its spec-focused companion, Spec-Driven Production Grade Development. Useful papers, with the caveat that the later chapters lean promotional about Google’s own tooling. ↩
-
NIST SP 800-218 (the Secure Software Development Framework) and SP 800-218A, its generative-AI addendum. ↩
-
OWASP Top 10 for LLM Applications. LLM06, “Excessive Agency,” is the risk the capability envelope speaks to, and only the permissions half of it; LLM01, prompt injection, is the checker’s territory. ↩
-
NCSC/CISA, Guidelines for Secure AI System Development, endorsed by agencies from 23 countries. ↩
-
NIST’s framework asks for exactly this loop in its Respond to Vulnerabilities practices: root-cause every failure and feed the fix back into the process, not just the code. ↩