Dev.to AI 🤖 Ai 👁 0 📖 9 min read

Agents Made Rewrites Cheap to Type. Spolsky's 2000 Warning Was Never About Typing

Book: Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My proje

Agents Made Rewrites Cheap to Type. Spolsky's 2000 Warning Was Never About Typing

In February, two rewrites made the rounds. Anthropic had 16 Claude agents write a 100,000-line C compiler in Rust from scratch: nearly 2,000 Claude Code sessions over two weeks and just under $20,000 in API cost (Anthropic). Nineteen days later Cloudflare announced vinext, a reimplementation of the Next.js API on Vite: "one engineer and an AI model rebuilt the most popular front-end framework from scratch." It took more than 800 sessions in OpenCode and roughly $1,100 in Claude API tokens (Cloudflare).

Read those numbers and the old rule against rewrites looks dead. If a framework costs $1,100, your 4,000-line shipping-quote module costs a lunch.

Look at what both projects had before an agent typed a line, though. The compiler team had the GCC torture test suite. The vinext team ported tests straight out of the Next.js repository. Each rewrite was graded by a test suite that other people had spent years writing. Your shipping-quote module doesn't have one.

What Spolsky actually said in 2000

Joel Spolsky published Things You Should Never Do, Part I on April 6, 2000. Netscape 6.0 was going into its first public beta. "There never was a version 5.0," he wrote, and version 4.0 had shipped almost three years earlier. Netscape, in his words, had made "the single worst strategic mistake that any software company can make": it rewrote the code from scratch.

The essay is easy to remember as "rewrites take too long." He does make that argument: starting over means "giving a gift of two or three years to your competitors." Agents really do shrink that part. The core of the essay is about what the old code knows. That ugly function "has grown little hairs and stuff on it and nobody knows why." Each hair is a fix: a LoadLibrary call that keeps the code running on old versions of Windows 95, a branch for "that bug that occurs in low memory conditions." "Each of these bugs took weeks of real-world usage before they were found."

Then the sentence that matters for 2026: "When you throw away code and start from scratch, you are throwing away all that knowledge. All those collected bug fixes."

That second argument is the one agents leave untouched. An agent shrinks the typing to an afternoon, and the knowledge it throws away is the same size as in 2000.

A gnarled old tree cut open to show growth rings packed with small patched bugs, while a robot arm plants a clean new sapling beside the stump
Every ring is a fix someone found the hard way. The sapling starts with none of them.

The famous agent rewrites borrowed their memory

The compiler write-up is honest about where the work went. The author says the task verifier has to be "nearly perfect, otherwise Claude will solve the wrong problem," and that most of the effort went into the tests, the environment and the feedback around the agents. Near the end, the agents kept breaking things that already worked, so a stricter CI gate went in to stop new commits from breaking existing code. When the Linux kernel build stalled, GCC itself became "an online known-good compiler oracle to compare against" (Anthropic).

Even with all that, the result is a research artifact. The same post says it has no assembler or linker of its own, calls GCC for 16-bit x86 code, generates inefficient code, and isn't a drop-in replacement for a production compiler.

vinext followed the same pattern. Cloudflare reports over 1,700 Vitest tests and 380 Playwright end-to-end tests, including tests ported directly from the Next.js suite, with CI running all of it on every pull request. It still calls the project experimental, "not yet been battle-tested with any meaningful traffic at scale," with 94% of the Next.js 16 API surface covered (Cloudflare).

Both teams turned someone else's accumulated knowledge into an executable oracle, and let the agents grind against it. That's the part to copy.

The cost moved into the tests that don't exist yet

Your internal module has no GCC and no public test suite. Its knowledge lives in three places:

  1. Branches in the code whose reason nobody wrote down.
  2. Commit messages and tickets that explain some of those branches.
  3. Production, where customers hit the cases nobody remembers.

Spolsky's axiom from the same essay still applies: "It's harder to read code than to write it." Agents read fast, and they will happily summarize what a branch does. They can't recover why it exists. A rewrite prompt like "clean this up, keep the behavior" gets you code that keeps the behavior the agent noticed. The edge case it skipped looks like dead code, and an agent asked for clean code deletes dead code.

So the rewrite is still expensive; you pay in the tests you have to write before the typing starts, because those tests are the only form of the old knowledge an agent can check itself against.

Michael Feathers named this kind of test in Working Effectively with Legacy Code: a characterization test describes "the actual behavior of an existing piece of software," right or wrong. People also call it a golden master.

Pin the old code before any agent touches it

Mine the history first. Every commit that touched the module is a candidate edge case, and the hairy lines tell you which commits matter:

# every commit that touched the module, oldest first
git log --reverse --format='%h %ad %s' --date=short \
  -- src/Legacy/ShippingQuote.php

# which commit last changed each line, whitespace ignored
git blame -w -L 40,90 src/Legacy/ShippingQuote.php

Turn each fix you find into a named input. Name it after the commit, so the next person knows where it came from:

{
  "domestic_standard": {"country": "DE", "weight_g": 900},
  "zero_weight_voucher": {"country": "DE", "weight_g": 0},
  "island_zip": {"country": "DE", "zip": "18565",
                 "weight_g": 900},
  "fix_4f2a91c_rounding": {"country": "AT", "weight_g": 2001}
}

Put the old class and the rewrite behind the same small interface, a port, so the recording and the test call the same method. Then record what the old code returns, once, on main, and commit the recording:

<?php
// tests/golden/record.php
// Run once on main, before any agent touches the module.
require __DIR__ . '/../../vendor/autoload.php';

use App\Shipping\LegacyQuoter;

$inputs = json_decode(
    file_get_contents(__DIR__ . '/inputs.json'),
    true,
    flags: JSON_THROW_ON_ERROR,
);

$legacy = new LegacyQuoter();
$expected = [];
foreach ($inputs as $name => $order) {
    $expected[$name] = $legacy->quote($order);
}

file_put_contents(
    __DIR__ . '/expected.json',
    json_encode(
        $expected,
        JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION,
    ) . "\n",
);

The test replays every input against the rewrite and demands the exact same answer:

<?php
// tests/ShippingGoldenMasterTest.php
use App\Shipping\RewrittenQuoter;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class ShippingGoldenMasterTest extends TestCase
{
    public static function cases(): iterable
    {
        $in = self::load(__DIR__ . '/golden/inputs.json');
        $want = self::load(__DIR__ . '/golden/expected.json');
        foreach ($in as $name => $order) {
            yield $name => [$order, $want[$name]];
        }
    }

    #[DataProvider('cases')]
    public function testRewriteMatchesLegacy(
        array $order,
        array $expected,
    ): void {
        $got = (new RewrittenQuoter())->quote($order);
        // Same JSON round trip the recording went through.
        $got = json_decode(
            json_encode($got, JSON_PRESERVE_ZERO_FRACTION),
            true,
        );
        $this->assertSame($expected, $got);
    }

    private static function load(string $path): array
    {
        return json_decode(
            file_get_contents($path),
            true,
            flags: JSON_THROW_ON_ERROR,
        );
    }
}

assertSame is strict on purpose, and JSON_PRESERVE_ZERO_FRACTION keeps floats as floats through the JSON files (PHP manual). If the old code returned 5.0 and the rewrite returns 5, that's a behavior change, and you want to decide about it, not discover it in an invoice. If a recorded answer is a bug you want fixed, fix it in a separate commit after the rewrite lands, so "same behavior" and "better behavior" never share a diff.

I walked through a longer version of this on a Laravel controller, with frozen time and a mutation check, in Pin a Legacy Laravel Controller With Golden-Master Tests.

A balance scale weighing an old brass machine against a sleek new one, both dropping identical orange tokens into matching trays
The golden master asks one question: same input, same output, for every case the old code learned.

Spike the rewrite where throwing it away is cheap

With the recording committed, the rewrite is a spike. Let the agent try. If it can't get the golden master green, you delete a folder and lose nothing.

I build Hermes IDE, so weigh this part with that in mind. In Hermes, ⌘N (Ctrl+Shift+N on Windows and Linux) starts each task on its own hermes/<name> branch in its own worktree, with the task as the agent's first prompt (README). A worktree recipe in .hermes/worktree.toml prepares the fresh checkout and sets the default checks for when a task is done (recipe docs). Put the golden master there:

# .hermes/worktree.toml
setup = ["composer install"]
done_when = [
  "vendor/bin/phpunit --filter ShippingGoldenMasterTest",
]

With that config, the default done check for every task is the recording from the old code, whatever the agent reports.

The Review Desk's Worktrees tab in Hermes IDE, listing one worktree each for orders-api, web-app and orders-types with branch, task, age and disk size
The Review Desk's Worktrees tab: one Hermes worktree each for orders-api, web-app and orders-types under ~/code, with each worktree's hermes/ branch, task, age and disk size, and the free disk space.

One rule before you read the diff: the agent may not touch the recording. If this prints anything, the test was moved to fit the code, and the spike is void:

git diff --stat main...hermes/rewrite-shipping-quote \
  -- tests/golden

Then read the rewrite in the Review Desk (⌘G), turn by turn. Two endings, both cheap. If the golden master is green and the new code is easier to read, Land commits it, opens a pull request or merges locally, and can be undone. If it isn't, close the session. When you close it, you choose whether to keep the worktree, and since 2.2.9 a kept worktree isn't deleted later (release notes). Throw it away, and the old module is exactly where it was.

Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Anthropic, OpenAI, Nous Research or any agent vendor. If you'd rather stay in plain git, the same flow is git worktree add, your test command, and git worktree remove.

So is Spolsky still right?

About whole products, yes, and agents make his warning more urgent. A team can now start a full rewrite in a morning, which means it can throw away ten years of bug fixes before anyone in the room has asked what they were.

About one module, behind a port, against a golden master recorded from the old code, the rule bends. That isn't a rewrite from scratch in Spolsky's sense. The old knowledge goes with you, as a file of inputs and expected outputs the agent has to satisfy.

Record that file first, and an agent rewrite is a spike you can afford to lose.

Has an agent "cleaned up" an edge case out of your code yet? How did you find out? Tell me in the comments.

If this was useful

Putting the old module and the rewrite behind one port is what makes the golden master possible, and it's the same move that lets a PHP application outlive its framework. Decoupled PHP is my book on doing that with clean and hexagonal architecture, one seam at a time instead of one big rewrite. If you try the spike above, Hermes IDE gives each attempt its own branch and worktree, and the repo is on GitHub.

Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework

Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.