Chapter 9 of 18

Backpressure

Checks that say no. Types, linters, hooks and gates that reject bad work before you ever see it.

Ask an agent if its work is finished and it will honestly tell you yes, because from the inside, almost-right looks the same as right. If nothing in the room can say otherwise, you are the only check there is, and you will be reading diffs until you stop reading them.

Backpressure is the fix. Instead of you saying no after the agent has finished, the environment says no while it is still working, in a form it can act on: a type error, a failing test (a small program that runs your code and checks the answer, which the next chapter covers properly), or a lint error that says what to do next. The agent reads the refusal and tries again, and by the time you look, the easy failures have been dealt with.

This chapter also has to be honest about the limit: a mechanical check can only refuse what someone thought to check. In our own experiment (one small model, one-function tasks, no real repo), a vague brief passed every hidden scenario 88% of the time in one shot, and 91% with a full line of checks behind it. The right words in the brief reached 98% with no checks at all, and 100% with them. Most of that gap is one task, where the vague brief never asked for the behaviour that mattered. The checks did make the code cleaner, and that is a real thing to want.

So the argument runs like this: what a harness is, the two ways to sort any check, the checks themselves from cheapest up, where they run, what they cost, how they break, and how they get walked round.

Harness engineering

When an agent makes a mistake, you don't just ask it to try again. You change its environment so that mistake can't easily happen again: a line in its instructions, a script it can run, a check it has to get past.

Retrying fixes this one mistake, today. A change to the harness fixes that mistake for every future session, including ones you never watch.

Say it to your agent

“You committed a generated file again. Don't just remove it: add it to .gitignore, add a line to AGENTS.md, and add a pre-commit check that fails if anything under dist/ is staged.”

The most useful thing a harness can do is say no at the right moment.

Backpressure

Also called Back pressure

Any automated check that refuses an agent's work and tells it why, so that it corrects itself before a person is involved. Types, tests, linters and security scanners can all do this.

Writing code is now cheap. Knowing that the code is right is not. Backpressure moves some of that cost from your attention to a machine that doesn't get tired. It moves only the part someone wrote a check for, and the rest of this chapter is about that line.

Say it to your agent

“Before you tell me you are finished, run bun run typecheck, bun test and bun run lint, and keep fixing until all three exit 0. Show me the output, then list what those checks could not have caught.”

Birgitta Böckeler, writing on Martin Fowler's site, sorted everything in a harness along two lines. They are the two most useful distinctions in this chapter. The first is about when a control acts.

Guide

Also called Feedforward control

A control that acts before the agent does, steering it towards the right thing: an instructions file, a skill, a code template, an example to copy.

A good guide means the agent gets it right first time, which is cheaper than being told it got it wrong. But a guide is only advice. The agent can read it and do something else.

Say it to your agent

“Add a guide for this: in AGENTS.md say that all database access goes through src/db/, and point to src/db/users.ts as the example to copy.”

Sensor

Also called Feedback control

A control that acts after the agent does, observing what it produced so it can correct itself: a test run, a type check, a screenshot, a review.

Sensors are what make a guide trustworthy. Without one, you can't tell whether the agent followed the instruction or only said so.

Say it to your agent

“You have a guide saying all database access goes through src/db/. Now add the sensor: a test that fails if any file outside src/db/ imports the database client.”

The second distinction is about what is doing the checking.

Computational control

Also called Deterministic check

A check run by ordinary code, so the same input should give the same answer, usually within seconds. Type checkers, linters and tests are computational.

You can rely on the result and run it a thousand times a day, which is why you turn rules into code wherever a rule can be written as code. Reliable doesn't mean relevant, though: it answers the one question it was written to ask, and no other.

Say it to your agent

“Asking you nicely to use the logger isn't working. Write this as a lint rule that fails on console.log in src/, with an error message that names the logger to use.”

Inferential control

Also called LLM as judge

A check where a model forms a judgement: an AI code review, a second agent grading the first. It can notice things no rule can express, such as whether a change makes sense.

It catches what computational checks can't. It also costs tokens, takes longer, can change its mind, and answers differently depending on how you ask. Run it after the cheap checks pass, never in place of them, and make it show its evidence.

Say it to your agent

“Have a fresh agent review this diff against the spec. It must quote the line and say what input breaks it. Anything it can't demonstrate goes in a separate list, and doesn't block the merge.”

Now the machinery, cheapest first. The cheapest of all doesn't refuse anything. It just fixes the problem.

Formatter

A program that rewrites your code into one fixed layout: spacing, line breaks, quote marks. It doesn't change what the code does, and running it a second time changes nothing.

The layout of an agent's output can drift from one edit to the next, and a one-line change can arrive looking like a rewrite. You pay to read that noise, and so does the next agent. A formatter removes it without asking anyone to decide anything, so it costs no retry and no review.

Say it to your agent

“Add a PostToolUse hook that runs biome format --write on each file you edit, and a CI step that runs biome format . and fails if anything would change. Don't reformat files you aren't otherwise changing in this commit.”

Linter

A program that reads your code without running it and flags patterns you have decided are wrong. It takes milliseconds and never gets bored.

For an agent, the message is what matters. An error that names the fix is an instruction that arrives at the exact moment it is relevant, and costs nothing until then.

Say it to your agent

“Add a lint rule banning direct fetch calls in src/. The error message must say: use apiClient from src/lib/api.ts, which adds auth and retries.”

That message deserves a term of its own, because for an agent an error is the next instruction.

Errors as prompts

Also called Remediation message

Writing an error message for the agent that will read it: what is wrong, where, and exactly what to do instead. The message goes straight into the agent's next turn, so it works as a prompt.

A person can look up what a terse error means. An agent takes the message as its next piece of context and guesses at whatever it leaves out. And it only costs you tokens when the rule was really broken, unlike a line in an instructions file, which is read every time.

Say it to your agent

“Rewrite this rule's error so an agent can act on it alone. Give the file and line, what is wrong, and what to use instead with its path: "Don't call fetch here. Use apiClient from src/lib/api.ts, which adds auth and retries." When the check passes, print one line and no more.”

Type checker

A program that reads your code without running it and checks that each value is used as the kind of thing it is: no adding text to a number, no asking for a field that isn't there. For TypeScript that program is tsc, and tsc --noEmit only checks.

Nobody has to write a rule or a test first, so it covers code the agent wrote a second ago. Its errors come with a file, a line and a reason, which is close to a ready-made prompt. And it has no idea what you meant: a program can be perfectly typed and wrong.

Say it to your agent

“Run bunx tsc --noEmit and fix every error at its cause. Don't add any, as, ! or @ts-ignore to make one go away. If you think the type itself is wrong, stop and show me why.”

Strict mode

A setting that makes a type checker stop trusting you in the places it otherwise would. The one that matters most: with it off, a value that could be missing is treated as if it were always there, so the crash waits until the program runs.

An agent only knows what the checker tells it. With strict off it is told nothing about a missing value, so it has no reason to handle one. Turning it on costs almost nothing on a new project. On an old one it can raise a flood of errors at once, which is a job for a ratchet.

Say it to your agent

“Set "strict": true and "noUncheckedIndexedAccess": true in tsconfig.json, then run bunx tsc --noEmit. Fix each error by deciding what should happen when the value is missing. Don't use ! or ?. only to silence one. If there are more than 50, stop and show me the count by file.”

Types and lint rules mostly look at one place at a time. The shape of the whole project needs a check of its own.

Structural test

Also called Architecture test, Fitness function

A test about how the code is arranged and not what it computes: which folder may use which, where the database may be touched. It reads the imports (the lines where one file pulls in another) and fails if a forbidden one appears.

An agent takes the nearest route to a working change, and a forbidden import works perfectly well, so no ordinary test will complain. Once it is in, that shortcut is an example the next session can copy. AGENTS.md can state this rule. Only a test can hold it.

Say it to your agent

“Add tests/architecture.test.ts. The order is types, config, repo, service, runtime, ui, and a file may only import from its own layer or an earlier one. Only src/repo/ may import the database client. Parse imports with the TypeScript compiler API, not a regex. Each failure must name the file, the line and where the code should go. Show it failing on a forbidden import.”

So much for what the checks are. Where one runs decides who it can stop, starting with the last moment on the machine doing the work.

Pre-commit hook

A script git runs when someone makes a commit. If it exits with anything but 0, the commit is refused. The git chapter showed how one works. The question here is what belongs in it: fast checks only, on what is really going in.

An agent commits often and reads the refusal at once, so this is a cheap place to catch things. It has two weaknesses. A slow hook tempts everyone to skip it, and because it lives on the machine doing the work, the agent is able to.

Say it to your agent

“Add a pre-commit hook with Lefthook that runs on staged files only: biome check on them, then gitleaks git --pre-commit --staged --redact. It must finish in under 10 seconds. Slow tests go in CI, not here. If gitleaks is missing, exit 1.”

Stop hook

A script the harness runs when the agent tries to end its turn. If the script says no, the agent is told why and has to keep working.

An agent can finish without committing, so git's hooks may never fire. This one fires when it decides it is done, and turns "please run the tests before you finish" from a request into a step the harness takes whether the agent remembers or not. The agent can't end its turn while tests are failing until the hook lets it, or the harness's own limit is reached.

Say it to your agent

“Add a Stop hook that runs bun test and bun run typecheck, and blocks with the last 20 lines of output if either fails. Don't exit early on stop_hook_active, because that gives you one retry. If the harness ends your turn with the tests still failing, tell me so plainly.”

Everything so far runs on the machine where the agent is working. The check that holds runs somewhere the agent can't reach.

Continuous integration

Also called CI

Everyone merges their work into the shared code often, and a server builds and tests each change on a clean copy of the project. That server is called CI too, and its green tick or red cross sits on the pull request.

A clean machine has only what was committed, so it finds what works only on the agent's laptop. A local hook can be skipped, and a required check on a server can't, unless the agent can edit the check itself, so the file that defines the check needs guarding as well. And ten agents get the same gate as one.

Say it to your agent

“Add .github/workflows/ci.yml that runs bun install --frozen-lockfile, bun run typecheck, bun run lint and bun test on every pull request. Name the job check, so I can make it required on main. Then break a test on purpose and show me the run failing.”

Fast feedback

Also called Shift left, Fail fast

Run first the checks most likely to fail for each second they take, so a bad change hears no as early as possible. In practice that mostly means cheapest first: format, lint, types, unit tests, then the slow and costly ones.

An agent sits idle or burns tokens while it waits, and every retry goes through the line again, so a slow first failure is paid for over and over. Good work doesn't get any faster from this. Only the bad news does, and that is where an agent spends its time.

Say it to your agent

“Order the CI pipeline by cost: format and lint first, then tsc --noEmit, then unit tests, with mutation tests only after merge. Stop at the first failure. Print how long each step took, and if the total is over 5 minutes, tell me which one to move.”

Line those checks up and make them binding, and you have a gate. This is where our experiment has the most to say, and it isn't what you might hope.

Quality gate

A set of conditions work has to meet before it may go further, where failing any one stops it: types clean, tests pass, no new security findings. It answers yes or no, and nobody gets to argue with it on the day.

A mechanical gate can only refuse what someone thought to check. An agent can pass every one and still have built the wrong thing, because the behaviour that mattered was never asked for, so no check knows it is missing. Someone reading the code and thinking about the job sometimes will. A gate works with a good brief. It can't stand in for one.

Say it to your agent

“Our gate is: bun run typecheck, bun run lint and bun test all exit 0. Before you start, list the behaviours this brief needs that none of those would notice if you got them wrong, such as what happens when events arrive out of order. Write a test for each, or ask me.”

A gate also has a price, and you pay it on the good work.

Check fatigue

Also called False positive, Alert fatigue

What happens when a check says no to good work too often: nobody believes its no any more. Each wrong no is a false positive. So count how often a check's no turned out to be right, and keep it, fix it or demote it on that number.

People get tired and start clicking through. An agent doesn't get tired and does what it is told, so it will rewrite good code to please the check, spending a try and more tokens. Either way, a check that cries wolf costs more than it looks.

Say it to your agent

“For each check in this pipeline, list the last 20 times it failed, and mark each as a real problem or not, with the commit that settled it. For any check under half real, propose fixing the rule or making it advisory. Show me the list before you change anything.”

Check fatigue is a check that says no too often. The opposite is worse: one that has stopped saying anything.

Fail closed

When a check can't run or crashes, treat that as a failure and stop, rather than letting the work through. The opposite, fail open, lets the work through when the check breaks.

A check that quietly stops running looks exactly like a check that keeps passing. This is how a project can go months with a security scan that never scanned anything.

Say it to your agent

“If gitleaks is not installed, the pre-commit hook must exit 1 with a message saying how to install it. Don't skip the check silently. Then rename the binary and show me the commit being refused.”

Then there is the check that works perfectly and gets walked round.

No bypass

The rule that nobody gets past a check by switching it off: skipping the hook, silencing the error, deleting the failing test. An agent may try any of these, so the rule has to be held by something that can see them, and not by asking.

A person knows --no-verify is cheating. An agent told to make the checks pass can see removing the check as a solution, and can then truthfully report that everything is green. A line asking it not to is only a guide. You need something on a machine it can't edit, looking at what changed.

Say it to your agent

“Add a CI step that reads git diff origin/main...HEAD and fails if a test file was deleted, if the diff adds .skip(, .only(, @ts-ignore or @ts-nocheck, or if it touches tsconfig.json or .github/workflows/, unless the pull request has the label harness-change. Add a PreToolUse hook that blocks --no-verify. Show me each one refusing.”

Every check in this chapter started as something that went wrong. That is how a harness grows, one mistake at a time, and it brings the chapter back to where it began.

Ratchet

A rule of thumb: when the agent makes a new kind of mistake, add something that catches that kind from then on. On an old codebase, record today's count of a problem and fail only when it goes up. Quality can then rise, and it can't quietly slip back.

Without it you fix the same class of problem again and again. With it, your instructions file becomes a list of real past failures, which is also the best evidence you can show of your own process.

Say it to your agent

“You have made this mistake twice now. Add a check that fails on the old version of this code, confirm that it does, then fix the code so the check passes. If the old code breaks it in many places, record today's count and fail only when it goes up.”

Do it for real

Reading gets you the words. This gets you the experience behind them, and something to show.

Make the check really able to fail

A check you have never seen fail is a check you are only hoping works. This is one of the quieter ways a harness lies to you.

  1. In a small project, add a pre-commit hook or a CI step that runs your tests.
  2. Break a test on purpose and try to commit. Confirm that it is refused.
  3. Now uninstall or rename the tool the check depends on, and try again.
  4. If it let you through, make it fail closed. Then write down what you changed and why.

Done when: You have a screenshot or log of the check refusing a bad commit, and of it refusing when its own tool is missing.

Turn one correction into a rule

This is the whole practice in one sitting, and the commit it produces is exactly what you want to be able to point at in an interview.

  1. Find something you have corrected your agent on at least twice.
  2. Ask yourself which layer could hold the fix: a type, a lint rule, a test, or at worst a line in AGENTS.md. Pick the strongest that works.
  3. Add it, and show that it fails on the old mistake.
  4. Commit with a message that says what went wrong and what now prevents it.

Done when: A commit in your repo that adds a check, whose message names the real mistake it came from.

Try to cheat your own harness

You only know what a line of checks can stop once you have tried to get round it yourself. Being able to say what still gets through is a stronger answer than saying it is all covered.

  1. On a scratch branch, break something so that a test or a type check fails.
  2. Try each way round by hand: git commit --no-verify, deleting the test, adding .skip, adding @ts-ignore, setting "strict": false, and removing the test step from your CI file.
  3. For each, write down which check refused it, or that nothing did.
  4. Close one gap with the cheapest check that works, and show it refusing.
  5. Keep the list of what still gets through, and name what covers each one: mutation testing, a review, or you.

Done when: A table of attempts, caught or missed, naming the check that caught each, and a commit that closes at least one gap.

The build, stage 9 of 18

Checks that say no

Lint, types, tests, a stop hook and a CI file that refuse bad work without you there, and a table of what still got through when you tried to cheat.

About 90 to 180 minutes, on the same app as every other stage. We ran it first and show what went wrong.

Open stage 9 of the build

Gate 9: Backpressure

0 of 6 right

  1. 1Your agent keeps importing the database client directly into page code, ignoring the line in AGENTS.md that says not to. What is the best next step?

  2. 2Say your brief says "handle payment events". Types, lint, tests and mutation testing all pass. In production, a late, older paid event undoes a refund. What do you do first?

  3. 3Your secret-scanning Claude Code hook exits 1 because the scanner isn't installed. You didn't set onFailure. It is November 2026. What happens to the agent's next action?

  4. 4You ran your AI reviewer twice on the same diff, and got an approval and then a rejection. What should you do?

  5. 5An agent can't fix a failing test. It deletes the test, adds @ts-ignore above a type error, and reports that all checks pass. That report is true. What stops this happening next time?

  6. 6Your pipeline runs a 4 minute mutation test first, then a 45 second test suite, then lint. Agents wait on it for every retry. Someone proposes deleting the mutation tests to save time. What do you try first?

Get every answer right to install it, and to put 7 files in your repo. Wrong answers cost nothing.

Next chapterTests

The workshop

Name a word, or describe what you mean in everyday language.