Back to blog
Engineering

We Deleted 862,973 Lines in Nine Days

Between September 6 and 15 we removed 3.3 lines from our monorepo for every line we added. Agents bias toward adding, and the sprawl becomes the bug surface, so we made deletion the default and made every new thing argue for itself.

A bare concrete corner, half in shadow, with nothing on either wall.

Between the last commit of September 5 and the morning of September 15, 2026, 1,177 commits landed on the main branch of our monorepo. Together they added 260,771 lines and removed 862,973 (our git log for those nine days, lockfiles and archived code excluded). For every line we wrote, we deleted 3.3.

Agents did much of that work. 558 of the 1,177 commits carry an agent's co-author trailer (our git log for the same nine days), and that is a floor, because not every agent we run signs its commits that way. That is the lesson. Agents bias toward adding: given a task, an agent writes a new module, a new flag, a sibling endpoint. Over months that sprawl becomes the product's bug surface. The fix we landed on is to make deletion the default verb and to make every new thing justify itself in the pull request that adds it.

Nine days on main

Most of the deleting happened in two days. On September 10, main lost 432,843 lines, and on September 11 another 333,094 (our git log, end-of-day snapshots of main). Those were whole seams going at once: a directory, its tests, its docs, and every import that pointed into it. The other days were slower work, a route here and a flag there, each with its own pull request.

Lines added and removed on main each day, September 6 to 15, 2026Added lines rise above the axis and removed lines fall below it. September 10 removed 432,843 lines, the largest day, shown in gold. September 11 removed 333,094.Nine days on main+100k−100k−200k−300k−400k6789101112131415432,843removed on Sep 10September 2026. Light bars: lines added. Dark bars: lines removed.Source: our git log, end-of-day snapshots of main; lockfiles excluded.
Each day compares main at midnight with main the night before, so a line added on the 7th and removed on the 10th counts on both days, and the daily bars sum to more than the ten-day total.

Where the lines came from

The two largest deletions were decisions we made once. A directory of consumer agents we no longer sell was 280,365 lines. The agent framework and UI kit built for the old desktop app were another 261,311. Nobody was going to extend either again, so we removed them whole instead of letting them rot in place.

Where the 862,973 removed lines came fromThe consumer agent directory, 280,365 lines, in gold. The old desktop app's agent framework and UI kit, 261,311. The API, 113,115. The CLI, 87,219. Everything else, 120,963.Lines removed, by areaA directory of consumer agentswe no longer sell280,365The old desktop app's agentframework and UI kit261,311The API113,115The CLI87,219Everything else120,963September 6 to 15, 2026. Source: our git log, lockfiles and archived code excluded.
The gold bar is a single directory. The API and the CLI are the live product; their bars are lines removed while both kept shipping.

The smaller bars teach more. In the same nine days the API lost 113,115 lines while gaining 55,244, and the CLI lost 87,219 while gaining 15,820 (our git log). That was live code. Much of it had been added by agents finishing a task, and each piece came with its own tests, docs and callers.

Sprawl is one reasonable commit at a time

From February 1 to September 13, 2026, our main branch took 11,756 commits. Once the conventional-commit prefix (fix(api):, refactor:, chore:) is stripped, the subjects of 577 of them begin with remove, delete, drop, kill, retire or prune (our git log). Reading them back, the same shapes repeat: dead modules, the same endpoint served twice, types with one caller, three hand-maintained lists of the agents we support that had to be kept in step, and registry rows for routes that no longer existed.

No single commit was wrong. An agent sees the task and the files it opened. Adding a sibling is the locally safe move, because it cannot break callers the agent never read. Finding the thing that already does the job means reading code nobody asked it to read. So each change adds a new thing instead of extending the one that exists, and each new thing brings its own bugs, its own tests, and its own copy of a decision already made somewhere else. When the decision changes, the fix lands in one copy and the other two become the next bug report.

GitClear sees the same pattern at a much larger scale. Its analysis of 211 million changed lines found that moved code, the signature of refactoring and reuse, fell from 25% of changed lines in 2021 to under 10% in 2024, and that 2024 was the first year copy/pasted lines outnumbered moved ones (GitClear code-quality report, 2025; DevClass, February 20, 2025). Their data covers code completion with a person at the keyboard. Our agents ran whole tasks with nobody watching each edit.

Deleting finds bugs too

One pull request on September 10 retired 50 API endpoints from a list. Before merging, the agent doing the work ran our API reference generator on the old tree and the new one. The old tree had 307 endpoints. The new one had 256, one fewer than the plan allowed.

Diffing the two endpoint sets found the cause. Deleting the whole email router had also taken POST /api/v1/emails/webhook, the live webhook our email provider calls, which was never on the list. On paper the list matched. The router-level delete took one more route with it. The agent restored that route as a one-route router, counted again, got 257, and diffed again to confirm the removed set was exactly the 50 on the list.

How a diff of endpoint sets caught one route too manyThe API had 307 endpoints. The plan removed 50. The first count after deleting was 256, one too few. Diffing the two endpoint sets showed the extra loss was the live email webhook, marked in gold. Restoring it gave 257.307 − 50 should be 257307endpoints before256first count after257after the restoreThe one extra: POST /api/v1/emails/webhookThe live webhook our email provider calls, removed with its routerOne pull request, September 10, 2026. Counts from our API reference generator.
The plan was correct and the first deletion still overshot. Counting against a measured baseline caught it before merge.

The rule we took from it: verify a deletion against a measured baseline, never against the plan. A deletion is a change like any other, and it gets the same proof.

The rule we wrote down

On September 13 we added a section called "Extend, don't add" to the instructions every agent in this repository reads before it writes code. It starts from the default answer to "where does this go?": an existing function, type, route, table, flag or module. Adding a new one is the exception, and the pull request has to argue for it.

Where does this go: the order every change followsFind the owner, extend it, derive instead of syncing. If the existing thing can carry the change, the pull request lists no new surface, the goal, in gold. If not, the pull request lists each new thing, the existing thing tried, and the one reason it could not carry it.Where does this go?1Find the ownerSearch the concept under three names. Read the owning module end to end.2Extend itA parameter, field, enum case, query filter, or mode of a command that exists.3Derive, never syncOne source of truth per fact. A second list kept in step is a bug in waiting.Can the existing thing carry the change?YesNew surface: noneThe goal. A PR that removes net linesand keeps behavior is the best kind.NoList each new symbol, route, table,flag or dependency, the existing thingyou tried, and the one reason itcould not carry this.From our agent instructions, September 13, 2026.A PR with new surface and no list is blocked.
The three steps come before any new file. The gold box is the outcome we want from most pull requests.

The argument has a fixed shape. Every pull request description lists, under New surface, each new exported symbol, type, route, table or column, config key, environment variable, CLI flag, module and dependency it introduces. For each one it names the existing thing the author tried to extend and the one concrete reason that thing could not carry the change. An empty list is the goal. A pull request with new surface and no list is not reviewable, and reviewers block it.

This works on agents for a plain reason. An agent follows an explicit procedure well, and it will search under three names if the instructions say to. The list also moves the cost of adding to where a person can see it. I argued in Agents Are Eating the Org Chart that review is the rate that bounds what agents can ship; a pull request that says "New surface: none" is one a reviewer can read in a minute.

What I can't claim yet. Lines are a crude measure. The 862,973 include docs and tests, and the first nine days took the cheapest deletions, the seams nobody used. The rule is four days old, and I have no before-and-after bug count. What I can say is that the product kept shipping through all of it: the engine and the console grew while the rest shrank.

Where Rush fits

We build Rush Cloud, and it returns work in the shape this rule asks for. A request starts an agent in an isolated cloud sandbox. When a one-shot run pushes code, the result is one pull request with checks, plus the full trajectory of what the agent did, so a reviewer can see what it read before it wrote anything and judge whether the new surface was needed. When a run pushes work without its pull request, it ends as needs_review. The same cost-of-failure reasoning from Every Agent Demo Hides the Same Number applies: review is where an agent's output becomes something you can merge.

Our agents did what we asked. The first thing we ask them now is to find what already does the job.

Read Next