Dashboard

Hand a Big Code Migration to an AI Agent

A framework upgrade across 400 files is the kind of work agents are genuinely good at, and the kind they ruin when handed over in one piece.

Steve Jefferson
Steve Jefferson
Developer Advocate
14 October 20261 min read

To hand a large code migration to an AI coding agent, split it into slices that each compile, pass tests and ship on their own, then run the agent one slice at a time with a checkpoint between them. The migration itself is mechanical work across many files, which is where agents are strongest. The failure is almost never the code transformation. It is handing over 400 files at once and having no way to tell which of the 400 went wrong.

Google describes code migrations as one of the uses it built Gemini 4 Argon for in its developer announcement. The capability is real. The project management around it is still yours.

When this is a good fit

Agents do well on migrations with a consistent, checkable transformation:

  • A framework or major-version upgrade with documented API changes.

  • Swapping one library for another of similar shape, such as one HTTP or date library for another.

  • A language-level change, for example adding type annotations or moving to a new module system.

  • Mechanical renames and signature changes across many call sites.

They do badly where the migration requires taste or hides a design decision:

  • Rewrites that change behaviour as well as structure.

  • Anything where the old behaviour is undocumented and the tests do not pin it down.

  • Data migrations, which need their own treatment entirely, covered in doing database migrations with an agent safely.

The second list has one thing in common: the agent has to guess what correct means. When it guesses, it guesses plausibly and consistently, which is the worst combination, because the result looks finished.

Slice it like a strangler, not a rewrite

The reason wholesale replacements fail is well documented outside AI entirely. Martin Fowler's strangler fig describes growing the new system alongside the old and moving behaviour across piece by piece, finding seams where components can be replaced independently, so each step delivers something and risk stays bounded.

That maps onto agent work almost directly. Each slice is a seam. The two systems coexist for the duration, which means your build has to tolerate both the old and the new API at once. That is the transitional architecture, and skipping it is what forces the all-at-once rewrite you are trying to avoid.

Pick seams in this order: leaf modules with no internal dependents, then modules depended on by one or two others, then the shared core last. The core is where a wrong assumption propagates, so you want the agent to have learned the pattern on 30 easy files before it reaches it.

The six steps

  1. Pin current behaviour. Before anything moves, make sure the tests actually cover the area. Where they do not, have the agent write characterisation tests against the existing code and commit them first, as a separate commit. They are your definition of done.

  2. Do one file by hand. Migrate a single representative file yourself. This becomes the worked example and surfaces the surprises in an hour rather than on file 200.

  3. Write the transformation down. Old pattern, new pattern, and the three exceptions you hit in step two. Specific before-and-after code beats prose.

  4. Generate the slice list. Group files into slices of 10 to 30 by dependency order. Commit the list, so progress is visible and resumable.

  5. Run one slice per session. Fresh context each time, the written transformation plus the files in that slice and nothing else. A 400-file migration in one context window is how an agent loses the thread in a large codebase.

  6. Checkpoint between slices. The step everything else depends on, below.

If step four produces slices that still feel too big, the general technique for splitting a task so an agent can finish it applies unchanged.

The checkpoint

A checkpoint is a script, not a habit. It runs after every slice, and it refuses to let the next slice start on a broken base. Without it, slice 7 is built on slice 6's mistake and you are bisecting 90 files instead of 12.

bash
#!/usr/bin/env bash
# checkpoint.sh <slice-name>  -> run after each slice, before the next
set -euo pipefail
slice="$1"

# 1. it must build
npm run build --silent

# 2. the full suite, not just the slice's tests
npm test --silent

# 3. no leftovers from the old API outside files we have not reached yet
if git diff --name-only origin/main... | xargs grep -ln 'oldApiCall(' 2>/dev/null; then
  echo "migrated files still reference the old API"; exit 1
fi

# 4. tests were not quietly weakened to get here
if [ "$(git diff origin/main... -- 'tests/**' | grep -cE '^-.*(assert|expect\()')" -gt 0 ]; then
  echo "assertions were removed during this slice"; exit 1
fi

git commit -am "migrate: ${slice}"
echo "checkpoint ${slice} ok"

Check four matters more than it looks. An agent under instruction to get a slice green can reach for the test file, which is the behaviour worth blocking structurally rather than discouraging.

What a slice actually looks like

Concretely, take a date library swap, the most common mechanical migration there is. The transformation description from step three might begin like this:

javascript
// old: moment
const start = moment(input).startOf('day');
const label = start.format('DD/MM/YYYY');
const later = start.add(3, 'days');

// new: date-fns
const start = startOfDay(new Date(input));
const label = format(start, 'dd/MM/yyyy');
const later = addDays(start, 3);

// exceptions found while doing one file by hand:
// 1. moment mutates, date-fns returns new values. Any code relying on
//    mutation in place must be rewritten, not translated.
// 2. format tokens differ in case: DD/MM/YYYY becomes dd/MM/yyyy.
// 3. moment(undefined) is 'now'. new Date(undefined) is Invalid Date.

Exception one is why hand-migrating a file first is not optional. An agent translating call by call will produce code that looks right and silently drops the mutation behaviour, and no amount of context will surface that unless someone has met it. Written down, it becomes a rule the agent applies on all 400 files.

Tracking progress so it is resumable

A migration that runs over several sessions needs state that lives outside anyone's memory. Keep the slice list in the repository and mark it as you go.

markdown
## migration: moment -> date-fns
- [x] slice-01  src/utils/dates       (8 files)   checkpoint ok
- [x] slice-02  src/reports           (14 files)  checkpoint ok
- [ ] slice-03  src/billing           (19 files)  <- next
- [ ] slice-04  src/core/scheduling   (23 files)  shared core, do last
- [ ] remove moment from package.json

This file is doing three jobs. It tells the next session where to start, it stops the agent wandering into the shared core early, and it gives you a visible burn-down that makes the work reportable to anyone who asks. The final line matters too: a migration is not finished while the old dependency is still installed, because nothing stops a new call site appearing.

The four ways it goes wrong

Failure

What you see

Guard

Plausible invention

Calls to an API that does not exist in the new version

Build step in the checkpoint

Silent behaviour change

Everything passes, something is subtly different

Characterisation tests from step one

Half-migrated module

Old and new patterns in one file

Grep check in the checkpoint

Scope creep

Unrelated formatting and refactors in the diff

One slice per commit, review the file list

Plausible invention is the common one. An agent that knows the shape of the new API will produce code in that shape whether or not the specific method exists, which is the same root cause as an agent inventing APIs outright.

Budget and stopping rules

Decide before you start what a slice is allowed to cost and what happens when one fails twice. Two failed attempts on the same slice means the transformation description is wrong, not that the agent needs another try, and the fix is to go back to step three. A hard spend cap on the agent stops a confused loop from becoming an invoice.

Questions people ask

How big should a slice be?

Small enough that you would review the diff properly, which in practice is 10 to 30 files for a mechanical change and fewer where judgement is involved. If you find yourself skimming, the slice was too big.

Can the agent generate the slice list itself?

Yes, and it is a good use of it, as long as you read the list before any work starts. Dependency order is the thing to check: a slice containing the shared core early in the list is a sign it has misread the graph.

Should each slice be a pull request?

If the migration runs over days and anyone else is committing, yes. One PR per slice keeps each review small and makes a revert cheap. For a one-afternoon migration on a quiet branch, one commit per slice is enough.

What if a slice breaks something days later?

Because each slice is a single commit that built and passed on its own, you have a clean revert boundary. That is the main dividend of the whole approach, and it is why the per-slice commit in the checkpoint matters more than its tidiness. The general case is covered in rolling back a bad agent change, and the wider set of guards worth having in place first is in the agent tooling landscape.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.