The core idea is one line: you don't fix the code. You fix the loop that keeps producing it.
A migration no longer needs an existential reason. A year of recurring memory bugs, or one chronic build bottleneck, can now be enough to justify it.
Treat the system as an assembly line: small models implement, large models review, compilers and tests judge. Humans design the line and write the rules.
Preserve the old structure, or redesign it? That single decision shapes the rulebook, where the compiler sits, and how the whole pipeline runs.
A language migration rewrites a production system without changing what the system does. Historically, that made it one of the most expensive bets an engineering organization could take.
The difficulty was never just the number of lines. For months or years, the team had to maintain two implementations. Product work slowed, and senior engineers were pulled away from the roadmap. Even after years of effort, the replacement might reach only 90% behavioral parity, and the remaining 10% was often the hardest part: obscure edge cases, undocumented assumptions, and production behavior no test had captured. That risk is why teams tolerate memory bugs and slow builds instead: an imperfect system in production can feel safer than a rewrite that may never finish.
Recent migrations at Anthropic suggest the economics have shifted. In the month leading up to its July 2026 report, engineers used Claude Code, Fable 5, Opus 4.8, and dynamic workflows to migrate ten packages ranging from tens to hundreds of thousands of lines. Two of them show the range.
Jarred Sumner, Bun's co-founder and an Anthropic engineer, migrated Bun from Zig to Rust: roughly one million lines across 1,448 files in under two weeks, with 100% of Bun's existing test suite passing in CI before merge. Mike Krieger, co-lead of Anthropic Labs, took a different path, migrating a Python codebase into about 165,000 lines of TypeScript over a weekend, using hundreds of agents, eight phase gates, and three rounds of adversarial review.
The cost has not disappeared; the risk structure has. More importantly, failure became cheaper: a failed agent-driven run can usually be discarded, and the team improves the process and reruns it. Mike's project began with slow compilation. The Python toolchain required about eight minutes per platform and roughly thirty minutes across the release matrix. After the TypeScript port, the same compile took about two seconds, the binary started six times faster, and a separate deployment pipeline was no longer necessary.
A conventional migration proceeds file by file: an engineer opens a source file, rewrites it, compiles, fixes errors, and moves on. That approach assumes a human can supervise every unit of work.
At a million lines, that assumption fails. The scalable alternative resembles an automated assembly line. Small models perform high-volume translation in parallel, large models review the results adversarially, and compilers, tests, and output diffs act as the judge. Humans design the line, define its rules, and decide what evidence counts as success. They do not turn every screw themselves.
This matters most when the same error appears repeatedly. If twenty generated files mishandle the same pattern, fixing those twenty files by hand only hides the weakness in the process. The correct response is to add one rule to the rulebook and regenerate the affected batch. The generated code is treated as disposable; the workflow that produces it is the durable asset.
You don't fix the code. You fix the process — the loop — that produced the code.
An automated workflow can run for hours or days only if it can answer one question reliably: is the task complete? That answer comes from the judge.
The judge may be a compiler, a test suite, an output diff, or all three. Whatever its form, it must evaluate the old and new systems on equal terms. Without that common standard the loop has no trustworthy exit condition, and existing tests do not automatically provide it: tests written in the old language may call private functions or depend on internal types, describing an implementation rather than observable behavior.
First, classify. Claude sorts the existing tests into those that capture externally observable behavior (command output, API results, file contents) and those coupled to internal structures.
Second, rewrite. The external tests become assertions that run against both implementations. Adversarial review agents then check that the conversion did not weaken any assertion.
Third, test the judge itself. The correct old implementation must pass; deliberately broken versions must fail. If an injected defect escapes detection, the suite is not yet a judge.
The two migrations started from very different positions. Bun had an unusual advantage: its test suite was written in TypeScript, a third language independent of both Zig and Rust, so the same tests ran unchanged against the Rust port. Mike's Python project had no equivalent, so his team built a parity harness of seven real-world scenarios, ran them against both the Python and TypeScript versions, and diffed the outputs, treating any difference as a defect.
Most projects will look more like Mike's than Bun's. That is not a blocker. If no cross-language suite exists, Claude can help build one, and the old system remains the ground truth.
The process is described as six stages, but they are not six unrelated techniques. They are six passes through the same loop. What changes between stages is the judge.
Each pass follows the same shape. A script scans the filesystem for unfinished work and divides it into a queue. Small models take items and implement them in parallel. Two larger models review each result from different adversarial angles; if they disagree, a third agent resolves it. A machine judge then decides pass or fail. A passing item moves to the next stage; a failing one returns to the queue.
Most of the difficult human judgment is concentrated in the first two stages. Once the rules are explicit and stress-tested, the rest is largely a matter of burning down mechanical queues.
// TODO(port) marker for stage 4. When reviewers keep catching the same mistake, the process adds a rule and regenerates that batch; the rulebook only grows, and the code is never hand-repaired.
Judge: review agents (compiler placement, see below)
The gap inventory records what the target language requires but the source never stated. These two snippets make it concrete.
fn readConfig(a: Allocator) ![]u8 {
const buf = try a.alloc(u8, 4096);
return buf; // who frees it? only a comment says
}
// A caller that forgets to free still
// compiles; the leak shows up at runtime.
fn read_config() -> Vec<u8> {
vec![0u8; 4096] // ownership moves, auto-freed
}
// Use a moved value, double-free, or an
// invalid lifetime? None of it compiles.
Zig → Rust: the gap is memory ownership. Zig relies on a human to free memory; Rust requires the relationship to be provable by the compiler. That implicit knowledge must go into the gap inventory first.
def register(handler):
handler.setup()
return handler.run({"retries": 3})
# Any object with .setup()/.run() works.
# Which ones actually pass through? Read
# the whole codebase to find out.
interface Handler {
setup(): void;
run(o: {retries: number}): Promise<Result>;
}
function register(h: Handler) { ... }
// Without the written contract, it
// won't type-check.
Python → TypeScript: the gap is interfaces and contracts. Python accepts any object with the right methods; TypeScript makes you declare the Handler interface — the shape the Python program left implicit.
Mike's project went one step further at stage 6: after the seven scenarios matched, Claude designed its own end-to-end suite and ran it autonomously overnight, finding failures and fixing them for four consecutive nights. It caught edge cases no scenario list would have predicted.
Bun's migration and Mike's used the same general stages. Their most important difference was the opening decision: preserve the existing structure, or redesign the system.
That decision changes nearly every artifact in the workflow, from the shape of the rulebook to how many times the whole line runs.
Under manual economics, running the whole thing three times sounds wasteful. Under agent-driven economics it can be rational: the first two runs exist to improve the design, the rules, and the orchestration, not to ship code. The same six stages still have to be re-arranged around target structure, compile cost, and test conditions.
Bun's Rust version is now in production. It is not free of trade-offs: about 4% of the Rust code uses unsafe, mostly single-line pointer operations at C and C++ boundaries. But the improvements are measurable.
The larger change is the risk model. The old rewrite nightmare was a team spending years to reach 90% parity, then trapped between finishing and reverting. The new process concentrates most of the downside in tokens, API cost, and a few days of runtime: if the output is poor, delete it, repair the judge or rulebook, and run the line again. A recurring defect no longer has to be chased through a thousand files — it can be traced back to the rule that produced it.
This does not make migration effortless, and it does not mean copying a fixed script. Every codebase has a different judge, gaps, dependencies, and target structure, so the first use of Claude should be planning the migration itself. Anthropic has published a migration starter kit, but it is a generalized template rather than the exact machinery the Bun and Python projects ran on; a separate code-modernization plugin targets legacy modernization and framework upgrades rather than language ports.
What's worth reusing is not a template but an engineering stance: don't treat a large migration as countless code edits. Build a loop that keeps producing, questioning, and judging; write human judgment into the rules and machine certainty into the gates; and let the pipeline absorb the scale.