Most of my work is done by models now. That is the easy part. The hard part is saying, in advance, how I will know it is finished.
What goes to which model
I keep a table with three numbers per model: what it costs me, how hard a problem it can carry unsupervised, and taste: by which I mean judgement about interfaces, naming, API shape and copy. Every delegation decision comes out of those three. I wrote up how the routing works, and why no agent reviews its own work, separately.
- Bulk mechanical work goes to the cheapest tier: extraction, classification, file-by-file transformations, codemods, i18n keys, batches of narrow checks.
- Clear-spec implementation and repository investigation with a defined question go to the default worker.
- Ambiguous, architectural or security-sensitive work escalates, and so does anything a cheaper tier already got wrong.
- Anything a user sees needs taste of seven or above. That single rule is what keeps the cheap tiers out of interfaces, naming and copy, and it is the rule that blocks the most runs.
The cheap tier recently got much cheaper, so more work goes there than a year ago. That sounds like a pure win. It is where my process broke.
Fourteen green releases
I had run the full spec-driven chain on two starter kits: requirements, design, plan, implementation, each step written down before the next one started. Fourteen releases went out. typecheck, lint, knip, test, test:boundaries, build, Playwright and axe: green throughout.
Nobody was reviewing in between. I triggered the next loop, the gates came back green, and I triggered the next one. That is the part worth naming: the gates did not fail me so much as they removed my reason to look.
Then I sat down and used the application myself.
A fresh clone could not start. Every page behind /app crashed. The sign-in form put the password in the URL in plain text, because it relied on onSubmit and a pre-hydration submit falls back to a native GET. The onboarding checklist was decorative: every write died on a foreign key.
None of my gates had noticed, and that was consistent rather than surprising. They all answer the same kind of question: one about the code. Does it compile, does it bundle, does it render. Not one of them answered a question about the product: does signing up create an account?
A test that renders a page and never operates it is a screenshot with extra steps.
The second lesson was quieter and worse. Two attempts at a lint rule forbidding client islands from importing server code had both been silently ineffective: one was schema-invalid, one sat outside the config call. Both linted green and caught nothing. A gate you have never seen fail is untested. Now I write the violating code as a fixture and watch the rule fire before I trust it.
Four gates, four questions
What came out of that is a pipeline where each stage answers a different question, and I can name which:
typecheck,lint,test,build: does the code compile and bundle?- anti-slop, a set of opinionated Oxlint rules: does this read like model output nobody reviewed?
- Greptile, which reviews every pull request: does a second reader notice something? It has caught real things in mine, including a median over two samples that is arithmetically a mean.
- A browser probe that signs up, creates an organisation and uses it, plus a crawl asserting zero error pages and zero console errors, signed out and signed in: does the product do its job?
Only the last one asks about the product. It is also the only one that would have caught any of the fourteen.
This is why the cheap tier comes with a condition rather than a discount. Work only goes there if I can state a machine-checkable acceptance criterion before it starts. If I cannot name one, that is not a cheap task, and I hand it to a tier that can be trusted without one. Cheaper runs mean more output nobody reads line by line, and green gates were not evidence fourteen times in a row.
It generalises past agents, incidentally. I have had an agent review file a finding that was largely fabricated, and the claims that turned out true were the ones naming a path and a value, while the ones characterising system state in prose were where every error was.
Sharpening the task before it runs
The cheapest stage is the one before anything runs. I have a skill that interviews me about a task, one decision at a time, each with a recommended answer, until there are no blanks left: unstated constraints, the alternative I did not consider, the thing I assumed everyone knew.
It is uncomfortable and it is worth it. A question costs me two minutes. A run built on a wrong assumption costs the run, the review, and the rework, and the rework is the expensive part, because by then there is code to argue with.
When the goal is measurable
Some work can be scored rather than reviewed. For that I have a skill that designs the loss function and the harness: it interrogates the task, builds a blinded eval, generates the harness, and red-teams the target for ways a model could score well without doing the work. That last step is the one people skip, and it is the one that decides whether the number means anything.
What it produces is a goal file, which the harness then runs as a long optimisation loop. Two things I would tell anyone starting: the author of the oracle must not be the model running the loop, and when the loop reports that the harness is buggy, verify that claim independently before touching the scorer. Both times a loop told me the loss function was wrong, it was right, but I would not have known without checking.
Two sounds
A loop runs for a long time, and watching it is a waste of a person. So two hooks, one line of configuration each:
{
"hooks": {
"Notification": [
{ "matcher": "*", "hooks": [
{ "type": "command", "command": "afplay /System/Library/Sounds/Frog.aiff" }
]}
],
"Stop": [
{ "matcher": "*", "hooks": [
{ "type": "command", "command": "afplay /System/Library/Sounds/Glass.aiff" }
]}
]
}
}
Notification fires when the agent needs me: an input, a permission. Stop fires when the turn is finished. Two different sounds, because those are two different states and I want to tell them apart without looking at the screen.
That is the whole point, and it is smaller than it sounds. It is not a notification system. It is a status light. One sound means come back, the other means it is your move again, and in between I can do something else, which is the only way a long loop is actually cheaper than doing the work myself.
One file, two readers
The instructions themselves are the last piece. Different tools read different filenames: some look for AGENTS.md, some for CLAUDE.md. Keeping both by hand produces exactly what you would expect, which is two files that agree until they quietly do not.
So one is the source and the other is generated, with a header saying so:
AGENTS.mdis written by me.CLAUDE.mdis generated from it and carries a do-not-edit line.
There is one project where this does not work, and it is worth knowing before you standardise: Next.js writes its own block into AGENTS.md and re-adds it on every next dev. Generating the other file from that one would replace the project's rules with the framework's boilerplate. Two conventions want the same filename, so there the generator stays off and the reason is written down next to it.
What goes in the file matters more than which file it is. Mine opens with four rules that have nothing to do with any codebase:
- Done means done. Five things asked means five things delivered. If one is genuinely blocked, finish the other four and name the specific blocker in one sentence.
- A question is a question. "Should we use X?" is not "migrate everything to X". Answer first, act when I say go.
- Act inside the plan, not around it. A plan exists before implementation. Within an approved plan, anything reversible and cheap gets done and reported, not asked about.
- Reply in simplified English: short sentences, one instruction each. That governs replies to me only; commit messages and anything with a real audience keep their own register.
The third one took two tries. "Act, don't ask" and "always plan first" read as opposites, and an agent handed both will pick whichever the current sentence favours. Writing the precedence down, plan first, then act freely inside it, is what turned two rules that fought into one that works.
Next
The gap I have not closed is the browser probe on every project rather than the two that already carry it. Porting it is not the work: deciding what "the user's job" is for each product is, and that is a sentence per feature that nobody wants to write.
What would you add as a fifth gate? I am most interested in whether anyone has found a check that catches the class of thing that only shows up when a person operates the thing: without a person operating the thing.
The fourteen releases are documented in the post-mortem for the ztl-next and ztl-start kits. Hook configuration is from my own ~/.claude/settings.json.
Building something similar?
I write about setups I actually use. If you're working on something comparable, I'd be curious what your workflow looks like.