Change Is Cheap. Until It Isn't.

A cinematic view of a city block at dusk where a row of demolished buildings has been reduced to rubble, yet around each ruin a pristine steel scaffold still stands intact, glowing warmly with work lamps. Inside the nearest scaffold, a new building is rising to fit the old frame. No people; dramatic sunset clouds overhead.

In June I started working with groundcover, two days a week, on an internal project in their AI tooling space. Within eight working days, using Claude Code running Opus 4.8, I had an eleven-thousand-line TypeScript subsystem built, deployed, and running on developers’ machines. A dozen working days in, I retired the entire thing. The approach it embodied had proven wrong, a replacement was already serving traffic, and the retirement itself took one sentence in a chat session. I didn’t feel a thing.

In February 2021 I argued that there’s no such thing as a software prototype; that the code you’re writing now is your product, here to stay; and that the best gift you can give yourself when starting a new project is a single end-to-end test, written before anything else. For two decades I practiced and taught outside-in TDD from the first line of code, later building a consulting practice around helping teams do the same. So it’s only fair that I document, head-on, the fact that I no longer work this way.

It Was Always About Cost of Change

Why did I insist on ATDD from the first line? Because software changes over time; it is never done. A team that cannot change its code cheaply ends up with fear of change, and fear of change kills companies. The tests were the means to that end: a baseline of asserted behavior to iterate on top of, facilitating emergent design so that at any given moment your design is the simplest one that satisfies the behaviors you’ve discovered so far. Writing the tests first was how you guaranteed the system stayed testable, and testable systems stay changeable.

Yes, this had a price. A walking skeleton is a frustrating exercise; you pay much of the integration cost upfront, and you write harness code for weeks before the product does anything interesting. I argued that the premium was worth it, because every line of code was expensive to write and more expensive to change, and because a codebase that started without the harness would calcify long before anyone got around to retrofitting one. In that economy, you bought the insurance on day one; by the time you needed it, the premium would have tripled.

That economy is now virtually gone.

A Few Days of Violence

When I start something new today, I begin with a few days of prototyping, making coarse, sometimes violent changes. Not refactorings; demolitions. On day two of the groundcover project we reversed the entire storage design. A week in, we deleted the build step; in the space of a single conversation the agent proposed a migration path, rejected it on grounds it later admitted were weak, and shipped a different one - an entire approach created and destroyed between breakfast and lunch. Ten working days in, I opened a session with: “I spent 10 working days so far on this epic and I feel we’re not moving in a good direction… I feel that I’m running into a sunk cost fallacy.” About 24 hours later, a 125-file pull request replaced the storage layer wholesale. And two working days after that, the original subsystem (11,460 lines of TypeScript, built in eight working days) was declared legacy in one sentence.

The 2021 me would need resuscitation. Current me didn’t care. The code decays; what accumulates is my understanding of the problem. Each demolished approach taught us something the next approach was built on, and since the agent does the coding, tearing down a week of work costs an afternoon. The cost of change at this stage of a project has collapsed to nearly zero, and when change is nearly free, the rational move is to change violently and often until you know what you’re building. It isn’t just client work, either. Earlier this year I wrote two side-project educational apps for my 6-year-old, teaching math and Hebrew reading, and there I took all of this to its extreme: no tests at all, because nothing about a side project justifies them. I’ve never seen their code and I don’t care what it looks like; I test them by using them on my iPhone. Only where I kept hitting regressions did I ask the agent for an e2e test, and I never read those either. Guardrails are bought in proportion to what breakage costs.

Mind you, the groundcover project was never literally test-free. Agents write unit tests by reflex, and the day-one commit already had a few thin ones. But they gated nothing; tests didn’t even run in CI at first (and then only because I demanded it), and they were not the safety net. In all that time, the safety net was dogfooding: run the system, read its logs, watch its telemetry.

Up To A Point

The reason my 2021 advice was right for its time is the same reason it eventually reasserts itself now: change doesn’t stay cheap. There’s a point in every project where the cost curve bends back up, and I’ve learned to watch for the signals. The scope no longer fits in my head. Checking for regressions by poking at the system starts to hurt. There are more than a few users, so breakage has consequences. And the agent starts making more and more mistakes, because the codebase has outgrown what it can safely reason about from context.

On the groundcover project, the turn arrived with an invoice. An expensive overnight batch job was killed six times in a row by cluster infrastructure: spot evictions, disk pressure, node consolidations. $60 burned, zero completed runs. The property that mattered - kill the job mid-run and it resumes where it left off - is exactly the kind of thing unit tests cannot touch. After the sixth kill I called it: stop here, we deal with durability before running again. I opened the fix session with: “First reproduce the bug in a failing e2e test, prove that it fails as we expect, then implement the solution. ATDD style.”

That was my twelfth working day on the project: the first true end-to-end test, and the first test harness. Within 24 hours there were six more e2e suites and three test kits, and every pull request since has started from a failing e2e test. ATDD survived; it just arrived late, knowing exactly which behaviors it was there to defend.

Guardrails With Real Dependencies

The tests themselves changed shape too. My old definition of an acceptance test was system-scoped but in-memory: hexagonal architecture, IO replaced by fakes, contract tests proving the fakes honest, the whole suite running in seconds. Today’s guardrails are e2e in their mechanics. The suites run against a real Postgres that fails loudly when the database isn’t running, rather than skipping (a skipped test is a lie of omission), with an ephemeral schema per run so tests stay isolated. Real protocol clients talk to the real server over real HTTP. The batch job’s tests spawn the real process and communicate with it as production would. The one thing we script is the LLM CLI itself: a fake binary that parses the real prompts, answers in the real protocol, and can be told to hang at a named phase, so that a test can kill the process mid-run and assert that a restart resumes in milliseconds instead of re-spending the budget.

The exception was Auth0. Testing against the real provider is possible - a Playwright test browsing to the real login page and signing in with a real username and password - but it would complicate the test well beyond what it’s worth. Instead, the agent built a seventy-line fake OAuth server (discovery, registration, PKCE, minting real signed tokens) and the e2e suite drives the real client through the full login flow against it.

The whole suite is currently ~500 tests and takes ~80 seconds on my machine. Which brings me to the part of my old doctrine I was most attached to, and which turned out to be the most disposable.

Nobody Is Waiting Anymore

For years I pushed integrative tests to be as fast as I could possibly make them. At Orbs in 2018 our acceptance suite initially ran in under two seconds and the component suites in 120 milliseconds, and I treated those numbers as sacred. The reasoning was human: if I have to wait, I run the tests less often, and everything downstream of that (TDD rhythm, refactoring courage, trust in the suite) falls apart. “As fast as humanly possible” was the requirement precisely because a human was in the loop, and humans are impatient.

Agents are not. An agent runs the suite after every change without being nagged, doesn’t lose its flow during a 90-second run, and will happily run it forty times a day. When nobody’s attention span is being spent, the speed budget can be spent on realism instead. In-memory fakes and contract tests were the price I paid for milliseconds; now that milliseconds buy me nothing, mostly-real dependencies win. I still reach for a fake where the real thing can’t live in a test (an external SaaS, an LLM, hardware), but the default has flipped. A few minutes of real infrastructure beats a few milliseconds of simulation, when the entity running the loop has infinite patience.

The Tests Are What I Read

I’m now roughly 99% out of the loop on production code. Over the month of this project I made about nine hundred substantive turns in agent sessions; I pasted code into the conversation seven times, all of them prompts rather than source, and I cited a specific source line once. What I read instead: logs, traces, telemetry dashboards, review-bot comments. And tests.

The tests I read carefully, and more than once I halted everything until they were reworked. When the first e2e test came back functional but unreadable, my feedback was: “the test is very mechanical, introduce the abstractions you know I like such as a test harness, driver, etc.” Test harnesses, drivers, custom matchers, object builders - the same toolbox I’ve been using for a decade, now enforced on an author who would otherwise happily produce tests that read like shell scripts. When a hard-won design invariant emerged from a long argument with the agent, it proposed recording the conclusion in a code comment; I had it recorded in a failing e2e test instead.

Earlier this year I argued that prompts and rules files are suggestions, and that the only thing that binds an agent is executable law: tests, types, linters. I still believe that; what this year taught me is when the law gets written. You don’t legislate during the constitutional convention. You prototype violently until you know what the desired behavior is, and then you encode that behavior as executable law, because a readable, behavioral e2e test is the only artifact that will still bind the next agent, and the one after it, long after every prompt has scrolled out of context. Recently I wrote that I’d spent a decade reviewing tests more carefully than implementations and calling it code review. This year made it literal: the tests are the only code I read.

In Summary

Nothing about my principles changed; the price list did. It was always about the cost of change. When code was expensive to write, ATDD from the first line was how you kept change cheap, and I stand by every word I wrote back then, for that economy. Agents made the first weeks of a project nearly free to change, so I now spend them making coarse, violent changes with no guardrails and no regrets. But change doesn’t stay cheap: when the scope outgrows my head, when users arrive, when regressions start to sting, when the agent begins to stumble - that’s when I stop and buy the guardrails, as e2e tests that assert the behavior I’ve finally discovered, running against mostly-real dependencies, at speeds that would have horrified me in 2018, executed by agents that never get impatient.

I must not fear change. It has never been this cheap to face.