A PR a Day Keeps Tech Debt at Bay
We deleted 12,000 lines of code this summer, with small PRs landing every weekday morning, written by an AI agent working from a short written recipe.
Oct 29, 2026 — 5 min read

Written by
The migration graveyard
Every engineering team has a migration graveyard. Nobody can justify a sprint on it, so it happens nights and weekends, sporadically, then stops.
In September we finished two big ones:
- Faraday (backend). All six of our Ruby services now make outbound web requests through one library, Faraday, instead of three.
- Apollo (frontend). Our fundraising app now loads data one way instead of two: hooks instead of the older "render prop" style.
It started with a pull request (PR) or two each weekday, opened without anyone asking, each converting a single page. As we came to trust our AI coding agent, we let it open more and fold several pages into each one: about eight a day by September, until the last Apollo code was gone. That is how 395 places fit into 202 Apollo PRs. Faraday went the other way. Its 32 places took 41 PRs, because several turned into deletions that reached across services. An engineer reviewed each change, and we reverted one. Along the way, the migrations removed about 12,000 lines of hand-written code on net.
Review never turned into a rubber stamp. The pages got harder as we went, about 900 lines per PR by mid-September against 270 in August. Even as the pace quadrupled, the share of PRs that drew a reviewer comment rose from 15% to 45%. All of it fit in a 30-minute review block each day, under four minutes per PR at the September pace.
| Faraday | Apollo | |
|---|---|---|
| Change | HTTParty and Net::HTTP to Faraday | Render-prop wrappers to hooks |
| One pull request covers | One service, or a deletion across several | One page at first, several by September |
| Places to change | 32 | 395 |
| Pull requests, estimated / actual | 10 / 41 | 58 / 202 |
| How the migration happened | worked from least to most risky: standalone services, then shared libraries, then the shared request layer everything depended on | mechanical rewrite with one example pull request to copy, plus rules that began as review comments |
The playbook
Every migration we take on is tracked like a normal project: owner, estimated and merged pull requests, percent complete, incidents caused, and a playbook. The playbook is a written recipe for exactly one unit of work, small enough for one pull request. A recipe like this works for a new engineer. It also works for a coding agent, every morning, without being asked.
The first Faraday PRs carefully ported code that nothing called anymore. So we taught the recipe to check first, and 6 of the 41 Faraday PRs became deletions. Here's the step:
Confirm the integration is still live in production before migrating it. Several of these call sites are long-dead legacy code, and migrating them wastes review time. Dead code should be deleted, not ported. Check all three signals:
- Traces: search Datadog for calls to the vendor's hostname over at least 90 days. Zero calls is the strongest signal.
- Data: count the records the integration writes, and check when the last one was written.
- Reachability: trace the call site back to a live entry point (a controller, a worker schedule, a cron job).
Then decide. Live: migrate it. Dead: open a deletion PR instead. Ambiguous: migrate it and note the uncertainty in the PR.
One pull request per run
Each migration has a scheduled coding agent session that runs every weekday. Its instructions center on one line:
EVERY RUN MUST OPEN ONE NEW PR. That is the deliverable.
We never changed that line. To speed up Apollo, we scheduled more runs each day and let each unit cover several pages.
The rest protects that line:
- Fix yesterday's work first, quickly. Failing tests and unanswered comments get fixed. A pull request waiting on a reviewer never blocks today's.
- Look at the code, not the notes. The next unit comes from searching the code as it stands, and the run checks open pull requests so it never collides with work in flight.
- One pull request, one message. When tests pass, it posts a review request in our code review channel with a progress bar counted fresh from the code, and updates Linear to match.
- Never go quiet. If it can't open a pull request, it says so and tags an engineer.
- Stay on budget. If a unit won't fit its spending limit, it asks an engineer instead of grinding.
The day it broke a page
One day, an Apollo conversion crashed a specific page for every user. The old code read account details only when a pop-up actually opened. The conversion moved those reads into the form's starting values, which run as soon as the page loads, so they ran with the pop-up closed and the account not yet loaded. An engineer reverted it immediately, we fixed the root cause, and the playbook gained five new rules. The simplest: test the page with the pop-up closed. One bad pull request broke one page, took one revert to undo, and made the recipe better for every run after it.
What we learned
- Start small, then grow the unit as trust grows. One service or one page per pull request kept early reviews quick, mistakes contained, and reverts easy. Once the reviews ran clean, we let each pull request cover more.
- Estimates will be wrong, and that's fine. Both migrations took three to four times as many pull requests as we planned. A daily habit doesn't care how many days it takes.
- Reuse the recipe. We're now using the same recipe to run other daily migration jobs, from replacing admin pop-ups with proper dialogs to speeding up the slowest production endpoint each morning.
On September 21, the Apollo job found nothing left to convert and posted, unprompted:
No candidates left for this automation. Suggest retiring it or repointing it at a new target.







