Fundamentals First · Part 5
Outrunning Your Headlights
Part four ended on a promise I made quickly and should take back slowly. The gray box works — you hand the AI the room, stop reading the implementation, and trust the door — as long as the tests through it still pass. That clause carried more than its length let on, and it hid a second clause inside itself. It is not enough that the tests pass. It matters how fast they tell you when they don’t.
That “how fast” is the whole game, and Matt hands it a name older than any of this. In the talk he does what he keeps doing — reaches for an old book, The Pragmatic Programmer, and its warning about driving at night: don’t outrun your headlights, never travel so fast you can’t stop inside the distance you can see. It was written about people. It’s also the tightest description of what happens when you let an AI move fast, because the machine has no instinct for the limit at all. The rate of feedback is your speed limit. Everything after this is a consequence of that one sentence.
Here is what no instinct looks like in practice. The feedback loops are right there — the type checker, the test runner, the browser — and the AI barely reaches for them. Left to itself it does the opposite of small: it writes a great deal of code in one confident stretch, wires up half a feature, and only near the end thinks to run a check — by which point a type error three functions back has quietly poisoned everything stacked on top of it. A veteran drives the other way: a few lines, a check; a few lines, a check; never far from the last thing known to be good. The AI floors it into the dark and learns what it hit when the whole thing comes back broken.
So the move — and this is the part of the talk I hadn’t put into words myself — is to stop treating “feedback” as one thing and start sorting it by speed. There are three loops and they run at wildly different latencies. Types are the fastest: the checker answers in the editor, before you’ve even saved, a red underline under the mistake the instant it exists. Tests are next — a run, a few seconds, the price of a command. The browser is the slowest: you bring up the running app and look, and looking stays a human-speed act no matter how fast the page paints. One word, “feedback,” three speeds — instant, seconds, a look.
Once they’re sorted, the rule writes itself: ride the fastest loop that can catch the mistake you’re about to make, and use it to stay on the rails between the slow ones. You don’t drive all the way to the browser to find a typo the checker would have underlined in the editor; you don’t spend a test run on what the types already refused to compile. Each slow loop is kept for the error only it can see — the test for behaviour the types can’t express, the browser for the thing that’s wrong only once a human looks. The AI’s whole failure was skipping the cheap loops and finding out at the expensive one. Speed isn’t going faster. It’s failing at the cheapest loop that could have told you.
Small steps are obviously right and the AI still won’t take them, for the same reason it outran its headlights to begin with: nothing in it wants to stop. So you don’t ask it to want to — you make stopping structural. That is what test-driven development is, underneath the ceremony. Write the test first, watch it fail, write only enough code to make it pass, then clean up. The discipline sold as being about coverage is really about batch size: you cannot pour out a thousand lines before checking, because a failing test is sitting right there demanding to go green, and the step from red to green is small by construction. TDD is a governor bolted onto a machine with no feel for its own speed limit.
And because the AI won’t reach for that on its own, I don’t leave it to the moments I remember to ask. I keep it in a skill. tdd makes the loop the default — a failing test before the code, the smallest change that passes it, a refactor once it’s green — every time, instead of once in a while. The skill is how “take small steps” stops being advice I have to give and becomes the shape the work already arrives in.
Here is the smallest true version of that from this blog. When I moved the one old post — the 2022 TypeScript-and-Blazor piece, the “before” from part one — into the archive, I thought of it as editing a post. It wasn’t. It was a change to a contract: the home page and the RSS feed, which had always listed that post, now had to not list it. And that contract wasn’t written in the post. It was written in the tests.
Two Playwright specs had the old promise in plain assertions — the home page lists the post, the feed carries its title. The moment the archived flag went in, they inverted:
-test('home page lists the TypeScript/Blazor post with a link', async ({ page }) => {
+test('home page does NOT list the archived TypeScript/Blazor post', async ({ page }) => {
await page.goto('/');
- await expect(page.getByText(POST_TITLE, { exact: false }).first()).toBeVisible();
- await expect(page.locator(`a[href*="/blog/${POST_SLUG}"]`).first()).toBeVisible();
+ await expect(page.getByText(POST_TITLE, { exact: false })).toHaveCount(0);
+ await expect(page.locator(`a[href*="/blog/${POST_SLUG}"]`)).toHaveCount(0);
});
The RSS spec flipped the same way — “carries its title” became “excludes its title.” None of this is a bug a test heroically caught in the night; it’s quieter and more useful than that. The test was the one place the promise was written down, so the promise couldn’t change by accident. Flipping the behaviour meant flipping an assertion, by hand, on purpose — and the red in between named every page that had leaned on the old answer. This is the test loop doing the one job the types can’t: the checker will never know that an archived post shouldn’t appear on the home page. That’s a behaviour, and behaviour is what the seconds-long loop is for.
There’s a reason that story stays so small and clean, and it runs straight back into part four. I could flip one assertion and trust it because the thing under test had a single honest seam: a page either lists a post or it doesn’t, and the test speaks to that surface and nothing behind it. That is the deep module seen from the other side. In part four the simple door was for navigation — one surface the AI reads instead of four. Here the same door is for speed: a boundary you can test through in seconds, with an assertion that doesn’t shatter every time the room behind it is rearranged. Matt lands it in a line — good codebases are easy codebases to test — and that line is the hinge between the two posts. The better the structure, the faster the feedback; the faster the feedback, the higher the speed limit. A shallow codebase — logic smeared across forty files with no seam to aim a test at — can’t hand you a fast loop no matter how many tests you write, because every test reaches through three things to touch a fourth and breaks when any of them moves. Part four’s clean interface and part five’s speed limit were never two ideas. They are one seam, measured once for how well you can read it and once for how fast it can answer.
So that’s part five. The failure was the AI outrunning its headlights — building the right thing and still landing on broken, because it did far too much before it checked any of it. The fundamental, older than AI and borrowed from people who drove at night, is that the rate of feedback is your speed limit: small deliberate steps, and “feedback” pulled apart into three loops by how fast each one answers. The skill is tdd, which turns “take small steps” from a thing I mean to do into the shape the work arrives in. And the artifact is a test suite that wouldn’t let a contract change quietly — one assertion inverted by hand, and the red in between telling me exactly what I’d promised and was now taking back.
But feedback loops solve one problem by sharpening another. When the loops are working and the AI is riding them, you ship more than you ever have — and the bottleneck slides off the machine and onto you. The code stops being the thing that can’t keep up; your own head becomes it. You can generate more than you can hold in your mind at once, and that is its own failure mode, the most physical one in the series: the tiredness of keeping a whole system loaded behind your eyes while it grows faster than you can read it. Next time: cognitive load, the gray box as the thing that saves your brain, and why the answer is to design the interface and refuse to hold the room.
The frame here is Matt Pocock’s — sorting feedback by speed, and answering an AI failure mode with an old book; he walks through it in the talk. The book is The Pragmatic Programmer*, where* outrunning your headlights comes from and where the rate of feedback gets called a speed limit. TDD is older than all of us. I’ve only pointed the loops at a real repo and written down what they caught.
‖:●·●·●:‖ Chenda melam — the temple percussion of Kerala, dozens of drummers moving as one through a fixed rhythmic cycle, the tala*, that climbs through its stages but never breaks, and that no single drummer is permitted to run ahead of.*