Codex vs Claude

Fleet-Wide Maintenance: Generating Twenty Pull Requests Is the Easy Part

David Guzenburg/ / 11 min read

Twenty correct branches, each with a green build, all still open three weeks later. The initiative produced negative value.

migrationautomationmulti-repodependency graph

Where agents genuinely change what is possible

Most claims about agent productivity are arguable. This one is not: bumping a dependency across twenty repositories, applying a deprecation fix everywhere, or migrating every service off an API that is going away is work that used to be a quarter-long initiative nobody staffed, and it is now a scheduled job.

It is a good fit for a specific reason. Each repository's change is small, mechanical, and independently verifiable by a test suite that already exists. That is exactly the profile that hands off well, multiplied by twenty.

So the interesting question is not whether it works. It is what breaks when it does, and the answer is not what people expect.

The first failure: twenty pull requests and nobody to merge them

The generation succeeds. Twenty branches appear, each correct, each with a green build. They then sit, because every one needs an owner to look at it, and the twenty repositories have somewhere between four and fifteen different owners, none of whom asked for this today.

Three weeks later half are still open, they have conflicts, and the dependency has released two more versions. The initiative has produced negative value: real work for other teams, no completed migration, and a set of stale branches that now need rebasing before anyone can evaluate them.

This is the constraint from parallel agents and the serial reviewer in its most acute form, because here the reviewers are not even the people who started the work.

Get the merge commitment before you generate

The fix is organisational and it goes first, before any code. Know who will merge each pull request and when, before opening any of them. That conversation takes an afternoon and it is the difference between a completed migration and twenty abandoned branches.

In practice it usually means going in waves aligned to teams rather than opening everything at once: five repositories this week whose owners have agreed, five next week. Slower on paper, and the only version that finishes.

The second failure: the change is not the same change

"Bump library X from 3 to 4 in twenty repositories" sounds like one change applied twenty times. It is usually three or four different changes wearing the same description.

Twelve repositories use the library in the straightforward way and the bump is a lockfile edit. Five wrap it in an adapter, so the change is in the adapter and the tests around it. Two use a deprecated entry point that version 4 removed, which is a real migration. And one has a patched fork nobody remembers creating.

An agent told to apply the same change everywhere will produce something plausible in all twenty, and the four unusual ones are where the bugs are — passing tests included, because a test suite that never exercised the deprecated path will not notice.

Classify before you batch

The step that prevents this is a survey pass that changes nothing.

#!/usr/bin/env bash
# Read-only. Classify each repo before any change is proposed.
set -euo pipefail
LIB="${1:?library name}"

for repo in $(cat repos.txt); do
  d=".work/$repo"
  [ -d "$d" ] || git clone -q --depth 1 "git@host:$repo" "$d"

  ver=$(jq -r --arg l "$LIB" '.dependencies[$l] // .devDependencies[$l] // "-"' \
          "$d/package.json" 2>/dev/null || echo "-")
  sites=$(rg -c --no-messages "from '$LIB'|require\('$LIB'\)" "$d/src" | wc -l)
  wrapper=$(rg -l --no-messages "$LIB" "$d/src" | rg -c 'adapter|wrapper|client' || echo 0)
  patched=$([ -d "$d/patches" ] && ls "$d/patches" | rg -c "$LIB" || echo 0)

  printf '%-28s ver=%-10s sites=%-4s wrapper=%-3s patched=%s\n' \
    "$repo" "$ver" "$sites" "$wrapper" "$patched"
done

Ten minutes of output, and it sorts twenty repositories into the twelve that are genuinely identical, the five that need a variant, and the three that need a person. That sorting is the entire difference between a batch that works and one that quietly ships four wrong changes.

Pilot on one, then three, then the rest

Never open twenty first. Do one repository end to end, including the review and the merge, and see what the reviewer says. Their comments are information about the brief, and applying them before generating nineteen more is free.

Then three, ideally from different classes. Then the remainder. Each stage is a chance to discover that the change needs an adjustment, at a cost of one or three rather than twenty.

Order by consequence, lowest first

Sequence the rollout so the least critical repositories go first. An internal tool, a documentation site, a service with no external consumers. Then the middle tier. The payment service goes last, when the change has already survived contact with fifteen other codebases.

This is a deployment practice applied to a migration, and it works for the same reason: the early repositories are a canary, and the information they produce is worth more than the hours saved by doing everything at once.

Verification has to be per-repository

A green build in each repository is necessary and not sufficient, because the interesting failures are in repositories whose tests never covered the affected path.

So the survey should also record test coverage of the call sites it found, and a repository with call sites and no coverage of them is flagged for human verification rather than accepted on a green build. That is usually three or four of the twenty, and it is a manageable amount of manual checking once you know which ones.

Green means the tests passed, not that the change is safe

For a batch change specifically, ask what would have failed if the change were wrong. If the answer is "nothing, that path is not tested", the green build is telling you about the test suite rather than about the change.

What each pull request should carry

Reviewers seeing an unrequested pull request from an automated process need context immediately: why this is happening, who asked, what class this repository was put in, what the agent verified, and what it wants the reviewer to check specifically.

The last part matters most and is usually omitted. "This repository wraps the library in src/clients/adapter.ts; please confirm the adapter's retry behaviour is unchanged" gives the reviewer a two-minute job. Without it they either rubber-stamp or spend twenty minutes working out what they are supposed to be looking at, and most will do the first.

Major versions need a human to read the changelog

A patch bump is mechanical. A major version bump involves reading release notes, understanding what the breaking changes mean for your usage, and deciding whether an intentional behaviour change is acceptable.

An agent can summarise a changelog well and cannot make that judgement, because it turns on how your systems use the library and what your users depend on. The workable split: agents handle patch and minor bumps automatically, and a major version produces a summary and a proposed plan for a person to approve before anything is generated.

Sometimes the batch is telling you something

Worth pausing on when a change is genuinely identical across twenty repositories. That is a strong signal that the thing being changed should be in one place: a shared library, a template, a base configuration.

The agent has made a symptom cheap to treat, and cheap treatment reduces the pressure to fix the underlying duplication. That is a real risk of this capability — it makes fragmentation sustainable, and sustainable fragmentation persists. If you find yourself running the same batch quarterly, the batch is not the solution, it is the workaround.

Standing job or one-off

Both exist and they need different treatment. A one-off migration is a project with an end, and it deserves the classification and pilot process above.

A standing job — weekly dependency updates, say — is infrastructure, and its main risk is volume fatigue: twenty small pull requests a week become background noise, and background noise gets approved without reading. Batch the routine ones by repository rather than by dependency, keep the frequency low enough that each batch is an event, and separate the security updates so they are visibly different from the housekeeping.

What to measure

Not pull requests opened. The number that matters is repositories actually migrated, and the gap between the two is where every failure in this article shows up.

Track opened, merged, and time from open to merge. If merges lag opens by more than a week, the constraint is review and generating more is actively harmful. If a repository has had an open bump for a month, it is not going to be merged by waiting, and someone needs to go and talk to its owner — which was the conversation that should have happened before the branch existed.

The repositories nobody owns

Every fleet has some. A service whose team was reorganised, an internal tool with one original author who left, a library four teams depend on and none maintain. They are the repositories most likely to be running an old dependency and least likely to have anyone who will merge a pull request.

A batch initiative surfaces them, which is useful, and then stalls on them, which is not. The productive response is to treat ownership as a prerequisite rather than a detail: repositories without an owner are excluded from the batch and escalated as an ownership question, not left as four permanently open branches that make the migration look nearly finished.

Rollback across twenty repositories

Worth thinking about before you need it. If the bump turns out to be bad — a regression that only appears under production load, in one service, three days later — what does reverting look like across everything already merged?

If each repository got its own single-purpose commit, the answer is twenty reverts, which is tedious and tractable. If the change was mixed with other work, or squashed into a release commit, it is considerably worse. Insisting that a batch change lands as an isolated, individually revertible commit costs nothing at the time and is the entire recovery plan.

What it feels like when it works

A survey on Monday sorting twenty repositories into three classes. One pilot merged on Tuesday with two review comments that improve the brief. Three more on Wednesday. The remaining twelve opened Thursday against owners who already agreed to look, merged over the following week. Three flagged for human verification because their tests do not cover the call sites. Two excluded because nobody owns them, raised separately.

Two weeks, one person, twenty repositories actually migrated. The version that fails takes the same two weeks, opens all twenty on the first afternoon, and finishes with eleven merged and nine rotting.

One organisational note to close on. The person running a fleet-wide batch is usually not on the teams receiving it, which means the whole exercise depends on goodwill they are spending rather than authority they have. Spending it well — small batches, clear asks, a specific thing for each reviewer to check, and finishing what you start — is what makes the second migration easier than the first. Spending it badly gets your automated pull requests filtered.

The version of this that is genuinely new

Worth ending on, because the hazards above should not obscure it. Before this capability existed, a cross-cutting fix in twenty repositories simply did not happen unless it was urgent enough to staff a project. The work was known, agreed to be worthwhile, and permanently deferred.

That category — worth doing, never worth staffing — is where the real gain is, and it is larger than the time saved on any individual change. A team that can clear it routinely accumulates less of the drift that makes old codebases expensive, and that compounds in a way no single migration does.

Takeaway

Fleet-wide maintenance is where agents genuinely change what a small team can attempt, and generation is not the constraint — merging is. Secure the merge commitment from each repository's owner before opening anything. Survey first with a read-only pass, because "the same change" is usually three different changes and the odd ones out are where the bugs are. Pilot on one, then three; order the rollout from least critical to most; flag repositories whose tests do not cover the changed call sites; and measure repositories migrated, not pull requests opened.

Keep reading
Codex vs Claude

Long-Horizon Runs: The Loop Cannot Tell Progress From Motion

A multi-hour agent run does not stop when it stops making progress. Why activity metrics rise fastest during a failing search, the degenerate solution to make the tests pass, checkpointing, budgets with defined exits, and compaction as a source of drift.

Codex vs Claude

Lifecycle Hooks or Standard Policy Boundaries?

Hooks are strongest when an organisation needs repository-specific deterministic enforcement or integration. Standard policy boundaries are strongest.

Codex vs Claude

The Hook Lifecycle: Everyone Uses Two Events Out of Thirty

Session start for environment assertions, prompt submission for state that moved, before-compaction for the constraints a long session is about to lose, failure events, subagent boundaries and worktree setup — and why cadence sets your performance budget.

Codex vs Claude

Thirty-Three Hook Events or Three Approval Policies: Matching the Instrument to the Rule

Claude Code hooks fire on thirty-three named lifecycle events; Codex leads with approval policies and sandbox profiles. Which rules need which instrument, and why most teams use two events out of thirty-three.

← What a Build Actually Costs: Reading the Gap Between 23% and Four Times  ·  Parallel Agents, Serial Reviewer: The Bottleneck Did Not Move →

All codex vs claude articles  ·  Every article