How to Migrate a Codebase with an Agent Without a Rewrite
The pitch arrives with a certain inevitability. A codebase has accumulated a decade of decisions: a payments module written in a framework nobody supports, a test suite that takes forty minutes and still misses the checkout path, a dependency tree where half the packages were last published before the current team joined. Somebody senior proposes an agent. Not a rewrite, they say. The agent will handle the migration. The room nods. The room has heard this before, from consultants, from the previous CTO, from a conference talk about strangler figs.
The difference this time is real but narrower than the pitch suggests. An agent can read a repository at a scale no human team can match. It can hold the shape of four hundred files in working memory, trace a function through six layers of indirection, and produce a patch at three in the morning. What it cannot do is decide which of those four hundred files should survive the migration and which should be buried with honors. That decision is the migration. Everything else is typing.
So the useful question is not whether an agent can migrate a codebase. It can move code. The question is how to run the migration so the agent does the moving and a human keeps the judgment. That distinction has to be built into the process from the first day, or the project collapses into either a rewrite nobody approved or a pile of plausible-looking patches that pass the tests and quietly break the product.
Draw the boundary before the agent touches a file
The first artifact of an agent-assisted migration is a one-page document listing what stays and what changes. Not a design doc. A boundary. The old payments module stays until the new one passes a parallel-run reconciliation. The HTTP layer changes. The data access layer gets wrapped, not replaced. The admin dashboard can be rewritten wholesale because three people use it and they all sit nearby.
That document exists so the agent has a fence. Without it, a capable model will happily propose a full restructure, because a full restructure is the cleanest pattern it can see. Modern agents are trained on tidy greenfield repositories and tend to recommend them. Give the agent an explicit, written scope and a list of forbidden moves and the recommendations get dramatically more restrained. The boundary is also the first thing a reviewer checks a patch against. A patch that touches something outside the fence gets rejected on sight, no meeting required.
Keep the document short enough to read in five minutes. Long enough to name files, directories, and interfaces. A boundary list that runs to forty pages becomes another artifact nobody reads.
Map the codebase's live edges, not its file tree
File trees lie. A repository looks like a hierarchy of folders when it is really a web of callers, consumers, and side effects. The migration risk lives in those live edges: the endpoint another team calls at 2 a.m., the shared library three services import, the database column the analytics pipeline reads directly.
Before any agent work begins, someone needs to produce a map of those edges. Logs help. So does the API gateway configuration, the package registry, and a quick grep for the module's public entry points across sibling repositories. The map does not need to be exhaustive. It needs to name the ten or fifteen places where a change in this codebase will announce itself somewhere else, and it needs to assign a human owner to each.
An agent can help build this map by reading import statements and call graphs, and that is one of its genuine strengths. But an agent will only find edges visible in code. The nightly job that scrapes a CSV from a shared bucket stays invisible until someone remembers it exists. The map is a human artifact with agent assistance, not the other way around.
Build a test harness the agent cannot game
Every agent migration lives or dies on the quality of its verification loop. An agent that can run the test suite will optimize for passing it, and a test suite that exercises twelve percent of the code will bless twelve percent of the migration. The rest drifts.
The harness needs three properties. It needs coverage of the actual behavior being changed, which usually means golden tests captured from production traffic or from the old system's outputs. It needs to fail loudly on the specific things a migration tends to break: serialization formats, error codes, timing-sensitive paths, floating-point rounding. And it needs to be runnable in a sandbox the agent controls, so the agent can iterate without a human watching.
Capturing golden outputs from the old system is the highest-leverage work in the whole project. It is also the least glamorous. A team that spends two weeks recording request-response pairs from the legacy service and one week pointing an agent at the migration will finish faster than a team that spends three days on the migration and three weeks debugging the same edge cases in production. The agent's speed is real, but it multiplies whatever verification exists. Multiply zero and the answer is zero.
Run the agent in small, reviewable batches
A single prompt to migrate the whole service produces a single enormous diff. Nobody reviews an enormous diff. Reviewers skim it, approve the parts that look plausible, and miss the three lines that silently change a default parameter. The migration ships, the tests pass, and the bug surfaces six weeks later in a support ticket written in all caps.
The alternative is to slice the migration into batches small enough for a real review: one module, one interface, one endpoint per pull request. Each batch gets a prompt scoped to exactly that slice, a test run scoped to the same slice, and a reviewer who reads the diff line by line. The agent does the mechanical translation. The reviewer checks the judgment calls the agent made along the way, because there will be several, and some of them will be wrong.
Small batches also make failures cheap. When a batch breaks something, the set of candidate causes is bounded by the diff in front of the reviewer. When a thousand-line batch breaks something, the team starts bisecting commits and losing afternoons.
Reject the agent's structural proposals by default
Give an agent a migration task and, sooner or later, it will suggest a rename. Then a module extraction. Then an interface abstraction that would make the whole thing cleaner. These suggestions are usually sensible in isolation and corrosive in aggregate, because each one expands the diff without expanding the migration. The scope creeps one well-intentioned refactor at a time.
The rule that holds up in practice is to reject structural changes by default and require an explicit case for each one. Rename only if the old name actively misleads callers. Extract only if the extraction is required to make the migration testable. Refactor only if the refactor is the migration. Anything else goes on a list for after the project ships, where it will be considered with the same skepticism as any other refactor and probably deferred for a year, which is fine.
This rule feels pedantic for the first week and then saves the project. Agents are enthusiastic. Enthusiasm is not the quality most needed when a codebase is mid-migration and half the callers have not switched over.
Keep the old path running until the new one proves itself
Cutting over in a single deploy is the migration pattern that produces the most dramatic stories and the fewest good ones. The old code keeps running. The new code comes online beside it. Traffic gets shifted gradually, starting with internal users, then a small percentage of production, then a wider share, with the old path still available for rollback at every stage.
Agents are useful here for the boring but essential work of keeping both paths healthy. They can update the feature flags, write the shadow-traffic comparison scripts, and patch the small discrepancies that show up when the new implementation's output is diffed against the old one's. That diffing is where migrations actually get finished. The agent helps close the gaps; the humans decide when the gaps are closed enough to flip the switch.
Running two paths costs money and attention. It is also cheaper than the alternative, which is discovering in production that the new payment serializer rounds differently from the old one and reconciling a week of transactions by hand.
Document the agent's mistakes in a living log
Every migration run produces a class of errors the agent makes repeatedly: the wrong default, the missed null check, the subtle change in how a date gets formatted at the boundary. These errors are not random. They cluster around the parts of the codebase where the convention is idiosyncratic, where the right answer is not the one in the training data.
Keep a running log of these mistakes. Every time a reviewer catches one, it goes into the log with a one-line description and a pointer to the patch. Then, before the next batch, the log gets pasted into the agent's instructions. Within two weeks the log becomes the most valuable document on the project, because it encodes everything the agent gets wrong in this specific codebase, which is the thing no general-purpose model can know.
This log also outlives the migration. The next agent-assisted project in the same repository starts with it, and the third one barely needs it. Institutional knowledge about a codebase's quirks usually lives in the heads of two senior engineers. The log moves it somewhere an agent can read.
Assign one human to hold the whole picture
Agent-assisted migrations fail in a specific way when attention is diffuse. Each reviewer approves their batch correctly. Each agent run produces a plausible diff. Then, three months in, nobody can say whether the project is ninety percent done or forty percent done, because nobody has been holding the whole map. The batches fit together in the aggregate, and the aggregate has no owner.
One person needs to hold the boundary document, the edge map, the test harness coverage, and the mistake log, and to know at any given moment which modules have crossed over and which have not. That person runs the weekly sync and makes the call on whether the next batch is ready. The role is roughly a tech lead's, and it does not fit inside a sprint's worth of work. Budget for it explicitly or the project will drift in the direction of whichever engineer happened to look at it last.
The agent does not replace this person. The agent makes this person necessary, because the agent will produce a hundred plausible decisions a week and someone has to decide which of them are true.
The day the old code gets deleted
There is a moment near the end of a successful migration when the old module has been dark for two weeks, the shadow diffs are clean, and someone opens a pull request that deletes four thousand lines. That pull request is the migration's real conclusion. Everything before it was preparation.
The temptation, after months of careful batching, is to let the agent do the deletion too. It can find every reference, strip every import, and produce a tidy diff. Resist for one more cycle. Deletions have a way of surfacing the last few live edges, the ones the map missed, in the form of a build failure on a Friday afternoon. Let the deletion sit in review for a day longer than feels necessary. Let the person who holds the whole picture scan it against the edge map. Then merge it, and watch the next deploy the way a sailor watches weather.
The agent will have done most of the typing. The team will have done the deciding. That split is the entire method, and it is the only version of an agent-assisted migration that ends with the old code actually gone and the new code actually trusted.

