What you need to know
- "Migrate everything" is the failure mode. A large repository does not fit in any agent's context window, so the agent loses the cross-file invariants that keep the system correct and ships code that compiles, reads plausibly, and quietly behaves differently.
- Characterisation tests come before any migration. Snapshot what the current code actually does, then gate every agent change on those snapshots. This single step is what turns "trust the agent" into "the agent's change is rejected unless behaviour is unchanged".
- Work in phases with a human checkpoint at each one. Inventory and dependency graph, pilot one module, expand in waves, integrate, cut over — never one giant pull request.
- Parallelise with isolation, not with chaos. Per-module subagents in their own git worktrees and branches, merged back through review, with an audit log of every conversion.
- Scope the agent to the mechanical work. Agents excel at syntactic transforms, scaffolding and test generation; they fail on architectural judgement, ambiguous business logic and concurrency. Keep humans on the judgement calls.
This guide is the migration-at-scale companion to a handful of pieces you should read alongside it. The discipline of writing the target down before the agent touches a line is covered in our spec-driven development guide; the habit of making the agent plan before it edits is in our plan-first Claude Code workflows; the steering document that keeps every agent on the same conventions is in our AGENTS.md and CLAUDE.md guide. Here the question is narrower and more uncomfortable than "can an agent write code" — it is "can an agent change a hundred thousand files of code you depend on without breaking the part you cannot afford to break?"
Why "ask the agent to migrate everything" fails
The naive approach is seductive because the small version of it works. You point an agent at a single file, ask it to port the syntax from one framework to the next, and it does a clean job in seconds. So you scale the instruction up — "migrate the whole repo to the new framework" — and the wheels come off. Three things break, and they break quietly, which is the dangerous part.
The first is context. Even the long-context models of 2026 cannot hold a 100,000-file monolith in working memory at once. The agent sees a window onto the codebase, not the whole thing. When it rewrites a module, it does so without the parts of the system that the module silently depends on — the caller three packages away that relies on a particular return shape, the serialiser that assumes a field is always present, the cron job that reads a side effect. Indexing and retrieval tools help the agent find related code, but retrieval is not comprehension; the agent still reasons over a sample, not the full graph.
The second is lost cross-file invariants. Large systems are held together by promises that live in no single file — an interface contract honoured by twenty implementers, an ordering guarantee, a shared enum whose numeric values are written to a database. A human maintainer carries these in their head and feels the wince when one is about to be violated. An agent migrating file-by-file with a partial view has no such instinct. It will happily change a method signature in one place and update three of the five call sites, because the other two were outside its window.
The third, and the one that should keep you up at night, is silent behavioural change. A language port or a framework upgrade is full of edge cases where the old and new worlds disagree subtly: integer division, time-zone handling, null-versus-empty, default sort stability, floating-point formatting. The migrated code compiles. It passes a casual read. The pull request looks reasonable. And then a UK fintech's interest calculation is off by a rounding mode, or an Indian SaaS firm's invoice dates shift by a day at a DST boundary, and nobody notices until a customer does. The agent did not fail loudly; it failed plausibly, which is far worse.
The most expensive migration bugs are not the ones that crash. A crash gets caught in CI on day one. The killers are the changes that pass every test you happen to have and alter behaviour you never thought to test — a rounding mode, a sort order, an off-by-one in a date boundary. If your only safety net is "the build is green", you are trusting the agent to be correct on exactly the cases you did not anticipate. The next two sections exist to remove that trust from the equation.
The context problem: index first, then scope tightly
Because the whole repository will never fit in one window, your job is to manage what the agent sees, deliberately, rather than hoping retrieval picks the right files. There are three moves that matter.
Understand before you change
Spend the first phase getting the agent — and yourself — to understand the codebase rather than edit it. Ask it to map the module boundaries, draw the dependency graph, and write a plain-English description of what each subsystem does and what it depends on. Modern agents index a codebase into a semantic representation so that natural-language questions retrieve the most relevant code; lean on that to interrogate the system before you touch it. The output of this phase is not changed code, it is a shared mental model and, crucially, a dependency ordering that tells you which modules are leaves (safe to migrate first) and which are load-bearing trunks (migrate last, with the most care).
Work module-by-module, not repo-wide
Once you know the dependency graph, scope every unit of work to a single module. When you run an agent inside a subdirectory, it scopes its default reads to that subtree, which keeps its context focused and its blast radius small. A module is a unit you can hold in your head, test in isolation, and review in one sitting. Repo-wide instructions ask the agent to be globally correct in one shot; module-scoped instructions ask it to be locally correct many times, with you checking the seams between modules. The second is tractable; the first is a coin toss.
Give the agent a steering document
The agent needs to know the rules of your migration, not the generic ones: the target framework version, the idioms you have standardised on, the libraries that are banned, the error-handling convention, the fact that this codebase uses British spelling in user-facing strings but US English in code. A steering file — CLAUDE.md for Claude Code, or the tool-agnostic AGENTS.md — is read at the start of every session and keeps every agent and subagent on the same conventions across a long migration. Without it, each module gets migrated to a slightly different house style and your "migration" becomes a second cleanup project. Our guide to steering agents with AGENTS.md and CLAUDE.md walks through what to put in it.
Characterisation tests first — the single most important step
If you take one thing from this guide, take this: before you migrate a single line, lock the current behaviour in tests. A characterisation test — the term is Michael Feathers's, and it is also known as a golden-master or approval test — does not assert what the code should do. It asserts what the code currently does. You run representative inputs through the legacy code, capture the outputs exactly as they are, warts and all, and freeze them as the reference. From that moment, any migration that changes an output fails the test, and you find out at the moment of change rather than from a customer three weeks later.
This is the step that converts the entire migration from an act of faith into an engineering process. With characterisation tests in place, you do not need to trust the agent's port to be faithful — you need only run the snapshot. If the new module produces byte-identical output to the old one across your captured inputs, the port is behaviour-preserving by construction. If it differs, you have caught a real regression, and you can decide whether the difference is a bug to fix or an intended change to bless. Either way, the decision is yours and it is informed, not the agent's and silent.
Here is the pattern in Python. You capture the legacy output once, store it as the golden master, and then assert against it forever after. Code stays in US English.
# characterization_test.py — pin current behavior BEFORE migrating.
# Phase 1: run the legacy code over representative inputs and snapshot
# its output as the "golden master". Phase 2: after the agent migrates
# the module, the SAME snapshot must still match, byte for byte.
import json
from pathlib import Path
from legacy import process_invoice # the code about to be migrated
GOLDEN = Path("golden/invoice_outputs.json")
# A representative, edge-case-heavy set of inputs. The harder the better:
# DST boundaries, zero amounts, rounding edges, empty vs null fields.
CASES = [
{"amount": "100.005", "currency": "GBP", "tax_rate": "0.20"},
{"amount": "0.00", "currency": "INR", "tax_rate": "0.18"},
{"amount": "999999.99","currency": "GBP","tax_rate": "0.00"},
{"date": "2026-03-30T00:30:00", "tz": "Europe/London"}, # DST edge
]
def capture_golden():
"""Run ONCE against the untouched legacy code to record the truth."""
GOLDEN.parent.mkdir(parents=True, exist_ok=True)
outputs = [process_invoice(**case) for case in CASES]
GOLDEN.write_text(json.dumps(outputs, indent=2, sort_keys=True))
def test_behaviour_is_unchanged():
"""Run in CI AFTER migration. Any difference fails the build."""
expected = json.loads(GOLDEN.read_text())
actual = [process_invoice(**case) for case in CASES]
assert actual == expected, "Migration changed observable behaviour"
Two disciplines make this work. First, the input set has to be adversarial: a characterisation test is only as good as the edge cases it exercises, so deliberately include the rounding boundaries, the DST transitions, the empty-versus-null cases, the maximum-length strings. Second, capture the golden master from the untouched legacy code and commit it before the agent runs — a snapshot taken after migration proves nothing. You can even ask the agent to generate the characterisation tests for you, since enumerating edge cases is exactly the kind of mechanical work it is good at; just be sure the snapshot itself is recorded against the original code.
Make the characterisation suite a hard merge gate before you let any migration branch land. Configure CI so a branch from a migration agent cannot merge unless the golden-master tests pass. This is what lets you run agents at scale without reading every diff line by line: the test gate, not your eyeballs, is what guarantees behaviour is preserved. Your review then focuses on the things tests cannot catch — readability, architectural fit, and the handful of intended behavioural changes you have explicitly blessed.
A phased migration plan
Large migrations succeed when they are boring and incremental and fail when they are heroic and all-at-once. The plan below has a human checkpoint at every phase — not as bureaucracy, but because each phase produces an artefact a human must approve before the next phase is allowed to build on it. Writing this plan down before you start is itself the spec-driven discipline from our spec-driven development guide.
| Phase | What the agents do | Human checkpoint |
|---|---|---|
| 1. Inventory & dependency graph | Map modules, build the dependency graph, identify leaves vs load-bearing trunks, write characterisation tests for the modules in scope | Approve the migration order and confirm the test suite genuinely pins current behaviour before anything changes |
| 2. Pilot one module | Migrate a single low-risk leaf module end to end; surface every rough edge in the steering doc, the test harness and the prompts | Review the pilot diff in full; this is where you calibrate trust and fix the process, not just the code |
| 3. Expand in waves | Migrate batches of independent modules in parallel, each in its own branch, each gated on its characterisation tests | Review per-wave via the audit log; spot-check diffs; bless or reject behavioural changes the tests flagged |
| 4. Integration | Wire migrated modules together, migrate the load-bearing trunks last, run cross-module and end-to-end tests | Approve the integrated system against full-system characterisation and integration tests, not just per-module ones |
| 5. Cutover | Switch traffic to the migrated system, keep the legacy path warm for rollback, monitor behavioural and performance deltas | Own the go/no-go decision; decide rollback criteria and the window before the legacy path is retired |
The discipline that ties the phases together is the same one our plan-first workflows guide argues for at the level of a single task: make the agent produce and you approve a plan before it edits. At migration scale, the phase is the plan, the checkpoint is the approval, and the artefact each phase produces — a graph, a pilot diff, a wave, an integrated build, a cutover runbook — is the thing you sign off on.
Every article here is written by a Verified Builder. Want your name on the next one?
AI Tech Connect lists AI engineers, founders and researchers across India and the UK — and the people hiring browse it to find them. Adding your profile is free.
Become a Verified Builder →Parallelism: per-module subagents, isolated worktrees, an audit log
The waves in phase three are where AI agents earn their keep, because independent modules can be migrated at the same time. But "at the same time" is exactly where parallel agents trip over each other unless you isolate them. The pattern is the same one our subagent orchestration guide sets out, applied to migration.
Give each module its own subagent. In Claude Code, each subagent runs in its own fresh, isolated context window, and only its final result returns to the parent session — so the reasoning for migrating the payments module never pollutes the context of the agent migrating the notifications module. That isolation keeps each agent focused and stops one module's mess from leaking into another's.
Give each agent its own git worktree and branch. Context isolation handles reasoning; worktrees handle files. If two agents edit the working tree at once you get corrupted, interleaved changes; if each agent operates in its own worktree on its own branch, their edits are physically separate and you merge them back through ordinary pull-request review. This is the difference between parallelism and a race condition.
Keep an audit log of every conversion. For each file an agent touches, record the file path, which agent and prompt produced the change, whether the characterisation tests passed, and the resulting commit. This log is what makes a large batch reviewable: instead of staring at ten thousand changed lines, a human reviews a ledger, spot-checks the diffs that look unusual, and trusts the test gate for the rest. It is also your forensic trail when something does slip through — you can see exactly which conversion introduced it.
Here is a per-module migration harness that ties the safety net to the parallel mechanics. For each target file it runs the agent transform on a dedicated branch, runs that file's tests, accepts the change only if the tests are green, and otherwise logs the failure and skips — leaving the file for a human.
# migrate_module.py — per-file migration harness.
# For each target file: branch, run the agent transform, run THAT file's
# tests, accept only if green, else log and skip. Each module runs in its
# own git worktree so parallel runs never touch the same working tree.
import subprocess
import json
from pathlib import Path
AUDIT = Path("audit/migration_log.jsonl")
def run(cmd):
return subprocess.run(cmd, capture_output=True, text=True)
def tests_pass(test_path):
# The file's characterisation + unit tests are the gate.
result = run(["pytest", "-q", test_path])
return result.returncode == 0
def record(entry):
AUDIT.parent.mkdir(parents=True, exist_ok=True)
with AUDIT.open("a") as log:
log.write(json.dumps(entry) + "\n")
def migrate_file(src_file, test_path, agent_cmd):
branch = f"migrate/{src_file.replace('/', '-')}"
run(["git", "checkout", "-b", branch])
# Hand the single file to the coding agent (e.g. a Claude Code subagent).
run(agent_cmd + [src_file])
if tests_pass(test_path):
run(["git", "add", src_file])
run(["git", "commit", "-m", f"migrate: {src_file}"])
record({"file": src_file, "status": "accepted", "branch": branch})
return True
# Tests failed: discard the change, log it, leave the file for a human.
run(["git", "checkout", "--", src_file])
run(["git", "checkout", "-"])
run(["git", "branch", "-D", branch])
record({"file": src_file, "status": "skipped_tests_failed"})
return False
# Each module is driven in its OWN worktree:
# git worktree add ../wt-payments migrate/payments
# so parallel module runs never collide in the working tree.
Where agents excel and where they fail
The whole strategy rests on putting agents on the work they are reliably good at and keeping humans on the work they are not. The line is fairly clear, and pretending it is not is how migrations go wrong.
| Agents excel at | Agents fail at (keep a human in the loop) |
|---|---|
| Mechanical, syntactic transforms — API renames, import rewrites, idiom translation between languages | Architectural judgement — whether a monolith should split here or there, what the new module boundaries ought to be |
| Scaffolding — generating the new module layout, boilerplate, build files and configuration for the target framework | Ambiguous business logic that is documented only in the code itself, where the "why" was never written down |
| Test generation — writing characterisation and unit tests for code that arrived without any | Cross-cutting concerns — concurrency, transaction boundaries, locking, ordering and security invariants that span modules |
| Repetitive bulk edits across many files where the change is regular and rule-based | Judgement calls about whether a behavioural difference the tests caught is a bug to fix or an intended improvement |
The practical reading of this table: let the agent do the thousand boring rewrites and write the tests, and reserve your own attention for the module boundaries, the gnarly business rules and anything touching concurrency or money. An agent that is asked to decide how to decouple a monolith is being asked to do the one thing on the right-hand column; an agent asked to apply a decoupling you designed is doing the left. The orchestration patterns for keeping that division clean across many parallel agents are in our dynamic-workflows guide.
Treat the agent as a tireless junior engineer who is brilliant at mechanics and has no instinct for consequences. You would never let such a person redesign your service boundaries unsupervised, but you would happily hand them ten thousand rote conversions behind a passing test suite and a code review. Scope the migration so the agent only ever does the junior-engineer work, and the failure modes shrink to the ones your tests and review already catch.
What the speed-up actually looks like
It is worth being honest about the numbers, because the marketing around AI migration is loud and the reality is conditional. McKinsey's QuantumBlack reports that generative AI can accelerate technology-modernization timelines by roughly 40 to 50 per cent and reduce costs derived from technology debt by around 40 per cent, with a top-15 global insurer case study citing more than 50 per cent improvement in code-modernization efficiency. These are real, reported figures from specific engagements — but they are reported estimates, not a guarantee that your monolith will halve its timeline.
What determines whether you land near those numbers is exactly the discipline this guide is about. A migration with good characterisation-test coverage, a clean dependency graph and a tight phased plan will see the agent fly, because the agent is doing mechanical work behind a net that catches its mistakes. A migration with no tests, a tangled graph and a "migrate everything" prompt will see the agent generate enormous volumes of plausible-looking change that a human then has to verify by hand, and the speed-up evaporates into review time and rework. The agent multiplies whatever process you give it; give it a disciplined process and the multiplier is large.
"We ported a fifteen-year-old billing service from one framework to the next. The first attempt was a single agent and a 'migrate the service' prompt — it produced a beautiful pull request that was wrong in three places we only found in staging. The second attempt, we spent two days writing golden-master tests against the live outputs first, then ran per-module agents in worktrees behind that gate. Same agent, same model. The difference was night and day: the tests caught every regression at the moment of change, and the only diffs I read closely were the ones the log flagged as behavioural. The tests were the project; the migration was almost a side effect."
— Aisha, Verified Builder · London, UKNext steps
- Index and graph the codebase first. Get the agent to map modules and dependencies, and identify which modules are leaves and which are load-bearing. Migrate in dependency order.
- Write characterisation tests before touching anything. Snapshot the legacy outputs over adversarial inputs and commit the golden master against the untouched code. Make the suite a hard merge gate.
- Pilot one low-risk module end to end. Use it to calibrate trust and to harden your steering doc, harness and prompts before you scale.
- Expand in parallel waves with isolation. One subagent, one worktree and one branch per module; merge through review; record every conversion in an audit log.
- Integrate, then cut over with a rollback path. Migrate the trunks last, test the whole system, and keep the legacy path warm until the migrated one has earned the traffic.
For primary documentation, see Anthropic's Claude Code subagents docs and its Claude Code overview, the characterisation-test reference for the Feathers golden-master pattern, Google's Gemini Code Assist enterprise overview for large-scale cross-file changes, and McKinsey QuantumBlack's AI for IT modernization write-up for the modernization figures. Tool behaviour and context limits move between releases — check the versions you are running.