一份关于使用 AI 智能体运行大规模代码迁移的分步指南——包括 Bun 将百万行 Zig 代码移植到 Rust 的案例。
- 分类Claude Code企业级 AI
- 产品Claude Code
- 日期2026 年 7 月 16 日
- 阅读时长5分钟
- https://claude.com/blog/ai-code-migration
代码迁移,即将生产级代码库移植到新语言的项目,在不久前还是需要耗费数年的工程。
在过去一个月里,Anthropic 的独立开发者们使用 Claude Fable 5、Claude Opus 4.8 和动态工作流,迁移了 10 个代码包,每个包含数万到数十万行代码。本文将介绍其中两个案例以及从这些项目中总结出的最佳实践。
Bun 联合创始人、Anthropic 技术团队成员 Jarred Sumner 使用 Claude Code 将 Bun 从 Zig 迁移到 Rust。在不到两周的时间内生成了百万行代码,合并前 Bun 现有测试套件在 CI 中的通过率为 100%。合并后出现了 19 个回归问题,现已全部修复。该 Rust 移植版本已于 6 月在 Claude Code 中发布。
Anthropic Labs 联合负责人 Mike Krieger 在一个周末内将一个 Python 代码库迁移到了 16.5 万行 TypeScript。这包括数百个智能体、八个阶段关卡、三轮对抗性审查,以及最终的一致性检查,该检查将每个命令的输出与 Python 原版进行了比对。
Claude Code 的新能力改变了这些长期搁置项目的计算方式。以下是我们现在使用的六步流程,源自这些迁移项目带给我们的经验。
核心洞察是:你不需要修复代码。你需要修复生成代码的那个过程(循环)。
为何以及何时迁移语言
在直接讨论“如何做”之前,有必要先讨论“何时做”和“为何做”,因为围绕这些项目的假设已经发生了变化。
团队启动迁移项目,是因为从最初构建到当前项目之间,技术格局发生了变化。要么是某个已知的权衡变得具有局限性,要么是出现了更好的方法,要么是原有的生态系统正在萎缩。
例如,Jarred 最初选择 Zig,是因为它在提供 C 语言级别性能的同时,又极其简洁,非常适合一位独立创业者在“大语言模型时代之前,于奥克兰一间狭小的公寓里用一年时间编写 Bun”。这种简洁性伴随着已知的权衡取舍,他在这篇文章中对此进行了阐述。
时间快进到 2026 年。Bun 的命令行界面每月下载量超过 1000 万次,并被广泛用于 Claude Code 中。
就在上个季度,这些权衡取舍还不足以成为冻结路线图并投入资源开展跨多季度项目的理由。迁移编程语言可以带来更小、更快、更安全的系统,但没有人愿意为此买单。
软件工程师们此前还不得不应对这些昔日巨型项目所固有的职业风险。你可能需要维护两套并行的代码库长达数个季度甚至数年,而如果最终结果只达到 90% 的等同性,那么你面临的麻烦会比刚开始时更大。
而现在,最坏的情况不过是删除分支,然后重新尝试。
仍然需要有合理的商业理由。虽然百万行级别的代码迁移不再需要在一个为期四年的项目中耗费 300 到 400 万美元的工程资源,但执行起来仍然需要花费数万到数十万美元甚至更多。例如,Bun 的迁移消耗了 59 亿个未缓存的输入 token 和 6.9 亿个输出 token——按 API 定价计算约为 16.5 万美元。Mike 移植工作的主要部分涉及 2700 万个 token。

然而,代码迁移的理由不再需要关乎生死存亡。变更日志中长达一年的内存错误补丁,或者一个长期存在的性能瓶颈,如今就足以成为迁移的理由。
编译步骤是 Mike 项目的推动力。他团队负责的内部工具以单个二进制文件的形式交付给用户。使用 Python 工具链生成该二进制文件,每个平台大约需要八分钟,每次发布时,整个构建矩阵的总等待时间长达 30 分钟。移植之后,同样的编译现在只需大约两秒钟,二进制文件启动速度快了 6 倍,并且团队得以淘汰一套独立的部署流水线。
为什么 AI 改变了代码迁移的权衡计算
Claude Fable 5 是我们能力最强、全面可用的模型。Fable 和 Opus 4.8 特别擅长委派、指导和验证与子智能体并行的任务流,同时能寻找多条通往既定目标的路径。
大型代码迁移是这些先进模型特别有效的应用场景,原因如下:
- 工作具有并行性。任务可以在数千个独立单元(如文件和代码包)上并行执行,因此智能体可以同时工作,而无需相互等待。
- 上下文清晰且全面。旧代码为模型提供了极好的规范说明。它也是核心参考,有助于构建供翻译智能体遵循的指南。
- 内置了裁判机制。许多大型代码库都包含测试套件,智能体可用其验证自身工作。当验证是客观的时,智能体的表现最佳,因为模型可以针对一个基准真相持续迭代数天,而无需人工评判质量。
- 队列会自动生成。当编译器或测试运行失败时,该失败项就会成为智能体下一个要修复的任务。
- 它们要求一致性和边界情况处理:整个流程的设计使得偏差无处藏身:审查者会引用每条发现背后的规则,因此违规会变成队列中的一个项目,而不是一次静默的偏离。而当智能体确实遇到边界情况时,修复方案会成为后续每个智能体都必须遵守的规则。
如下文所述,Mike 和 Jarred 都在其迁移过程的关键步骤中使用了 Fable,特别是在一种咨询模式中,该模式使用了多种模型类别来优化 token 消耗。
大型代码迁移的六个步骤
以下流程已进行通用化处理,适用于多种语言和场景。如需更多细节,你可以阅读 Jarred 的博客。
前提条件
在启动迁移项目之前,一个前提条件是拥有一个强大的评判机制,否则你将没有退出条件或衡量成功的标准。
评判机制必须能够平等地评估原始代码和目标代码。用原始语言编写的测试套件通常依赖于目标代码中不存在的内部函数。
要构建这个评判机制:
- 对现有测试进行分类。使用 Claude 来识别哪些测试可以表示为外部调用,哪些依赖于无法移植的内部逻辑。
- 为可移植性重写。将面向外部的测试转换为断言,使其既能针对原始代码运行,也能针对移植后的代码运行。使用对抗性智能体来验证重写后的测试没有削弱断言。
- 验证评判器。在原始代码上运行它,确认测试通过。然后在故意破坏的代码上运行它,确认测试失败——一个无法捕捉破坏的评判器就不是真正的评判器。
Jarred 有一个用第三种语言(TypeScript)编写的大型测试套件,但大多数项目并非如此。对于他的 Python 到 TypeScript 移植,Mike 创建了一个包含七个真实场景的对等性测试框架,并将任何行为变化视为需要修复的错误。
在深入每个阶段之前,这张图可能有助于你理解。这主要遵循 Jarred 的方法论,在每个阶段都设有审查和关卡。Mike 采用了类似的整体结构,使用相似的循环工作流,但他将整个迁移过程端到端地运行,根据结果修订规则和工作流,然后再次运行——每次都丢弃输出,直到第三次运行。
第一步——创建规则手册、依赖关系图和差距清单
在这个阶段,我们正在为迁移奠定基础:一份需要重构而非仅仅翻译的代码位置清单,一份关于如何翻译代码的规则手册,以及一份用于安排迁移实施工作流的依赖关系图。
顺序很重要:规则手册必须先于差距清单。差距清单由规则手册默认规则无法覆盖的内容来定义,两者会在联合审计中一起测试。
规则手册
规则手册的具体形式取决于你必须在开始时做出的关键架构决策。其中最主要的是,新代码将遵循相同的结构,还是会被完全重新设计。
如果是前者(Jarred 的情况),规则手册将主要是查找表,用于在语言之间翻译类型和惯用法,同时指向差距清单以处理较难翻译的组件。如果是后者(Mike 的情况),它将是一份设计文档。
Jarred 通过与 Claude 对话创建了他的规则手册,为每个模糊领域制定了一条策略。他还使用了八个专门设计的子智能体,基于自身直觉对八类常见故障模式进行审查。
依赖关系图
你需要理解文件依赖关系,才能有效拆分工作流以进行并行迁移,从而知道哪些文件应该优先迁移,以及哪些文件应放在同一批次中处理。某些语言和代码库拥有显式清单文件,使这一过程变得简单,但对于遗留代码库以及 C/C++ 和 Python 等许多流行语言,这些依赖关系需要被发现并映射出来。
Claude Code 可以部署智能体来创建并运行一个确定性脚本,以生成此依赖关系图。迁移工具包中的提示词使用了一个工作流来创建审查与修复循环。注意:入门工具包是本文所述流程的通用模板——它并非这些特定移植项目实际运行的版本。
差距清单与怀疑型审查者
新语言与旧语言有不同的要求,这些要求必须得到满足。从 Zig 到 Rust 的差异在于手动内存管理(C 和 C++ 的工作方式相同)。例如:
Zig
fn readConfig(allocator: std.mem.Allocator) ![]u8 {
const buf = try allocator.alloc(u8, 1024);
// ...fill buf...
return buf; // caller must free this — but only the comment says so
}
// A caller that forgets 'defer allocator.free(buf)' still compiles — the leak only surfaces at runtime. Rust
fnread_config() -> Vec<u8> {
let buf = vec![0u8; 1024];
// ...fill buf... buf // ownership moves to the caller; memory is freed automatically }
// Use it after it's moved? Free it twice? Neither compiles. // Forget to free it? There's no free call to forget — drop is automatic. 从 Python 到 TypeScript 的差距在于接口和契约。Python 不需要声明函数将接受什么形状的对象或返回什么类型的契约,但 TypeScript 需要。例如:
Python
def(handler): handler.setup()
return handler.run({"retries": 3})
# Any object with .setup() and .run() works here. Which objects actually get passed in? Read the whole codebase to find out. TypeScript
interface RunResult { ok: boolean }
interface Handler
{ setup(): void;
run(opts: { retries: number }): Promise<RunResult>;
}
function(handler: Handler): Promise<RunResult> {
handler.setup();
return handler.run({ retries: 3 }); }
// The contract must be written down before this compiles Jarred 和 Mike 都创建了差距清单文件来记录这些隐性知识。Jarred 事先整理了这些差距,这也是我们在此采用的做法;而 Mike 则选择先进行翻译,然后通过事后审计来创建差距清单。你可能需要同时采用这两种方法。
请查看这个示例 Claude Code 提示词,用于创建差距清单文件。
步骤 2 —— 压力测试规则
此步骤涉及一次小型迁移,作为更大规模迁移的"试航"。
在这一步中,Jarred 使用一个智能体根据规则手册翻译三个文件,使用另一个智能体"像资深 Rust 工程师一样"翻译三个文件,并使用第三个智能体根据差异来创建新的翻译规则。在此阶段,他发现了两个关键问题——如果这些问题扩散到全部 1,448 个文件中,将会造成大量麻烦。
提示词可能看起来像这样。
这种压力测试仅适用于保持结构不变的迁移,即同一文件的两个翻译版本可以逐行进行比较。如果你的规则手册是一次重新设计——就像 Mike 所做的那样——那么等效的测试是直接让对抗性评审员攻击设计文档,然后通过一次性的端到端运行来验证它。
无论如何,丢弃所有已翻译的文件。目标是完善规则,而不是取得渐进式进展。
步骤 3 —— 翻译所有内容
在接下来的步骤中,你运行相同的多智能体循环架构:实现、评审和修复。
你可以将实现者的工作卸载给较小的模型,而让较大的模型担任评审者。例如,Mike 在为主要迁移任务分派 12 个子智能体时,使用了 Claude Sonnet。
工作队列应该是机械化的。一个批处理脚本通过检查翻译后的文件是否存在于磁盘上来判断哪些已完成,然后将待处理文件切片成批次,分发给实现者智能体。由于队列每次都会从磁盘重建,因此迁移过程天然具备可恢复性。
在这个阶段,智能体可能会对自己承担的工作量过于谨慎。修复方法可以是一条直接而强硬的提示词指令,并附带上下文说明编译器将在下一步中捕获错误。
任何翻译器无法自信执行的内容都会被标记为 `// TODO(port): <reason>`,留待步骤 4 处理。从这时起,待办列表会自动生成:编译器列举错误,冒烟测试发现崩溃,测试套件报告失败。
两个对抗性评审员使用不同的上下文评估实现者的工作,评审员之间的分歧交由第三个智能体处理。当某个评审员在多个文件中反复发现同一类错误时,修复方法不是逐个文件进行。你只需在规则手册中添加一句话,然后重新生成受影响的批次。规则手册在这一步骤中不断增长;代码绝不会针对它进行手动修补。
这一步中一个值得注意的重要设计决策是编译器的放置位置。Mike 在每个循环内部都运行了 TypeScript 编译器,因为它检查一个单元只需几秒钟。Jarred 则完全禁止编译器进入循环,将其推迟到下一步,因为 cargo 需要几分钟时间。
在这一步,大部分繁重工作已经完成,提示词开始变短。
第 4、5、6 步——编译、运行、匹配行为
这三步共享相同的循环架构,且所需的人工判断逐步减少,因此我们将它们放在一起介绍。
例如,第 4 步常常会根据迁移所用的语言和规模,融入第 3 步中。
根据编译步骤的规模和难度,智能体可能根本不执行这一步。Jarred 通过一个编排脚本来执行此步骤,该脚本在整个工作空间中只调用一次编译器。然后,“修复智能体”以对抗性审查的方式并行处理错误列表。构建再次运行,如此反复。
审查错误列表有助于发现可能需要调整的系统性问题。例如,Jarred 遇到了数千个 Rust 模块错误,这些错误是在修复了 Zig 惰性编译所容忍的循环导入后出现的。他通过编码逻辑来分类哪些依赖需要删除、移动或重构边界,从而修复了循环。
第 5 步也有一个类似于编译器错误列表的机械性事实依据:冒烟测试产生的崩溃。同样,循环修复的方法是将问题按类别分组,这里则是根据根本原因对原因进行分组,由对抗性子智能体进行审查。
第 6 步,也是我们故事的结尾,是比较两个代码库中程序的行为。
我们的文件现在已经完成了翻译、编译和冒烟测试。现在是时候将它们分片,并针对它们运行(来自前置阶段的)测试套件了。通过“修复智能体”来处理失败情况,这些智能体会在两个代码库中审查失败的测试。对抗性审查者会检查它们的修复结果。
这个循环的下一阶段是一个构建守护进程,它是唯一被允许重建二进制文件的进程。修复者编写补丁;守护进程将它们分批处理,只重建一次,重新运行受影响的测试,并将结果反馈回来。这样就将最昂贵的操作串行化,而不是让多个智能体独立触发它。
当同一个失败在多个测试中重复出现时,修复就会向上游移动:你修改产生 bug 的规则,只重新生成该规则涉及的文件。
Mike 的方法在这里很重要,因为许多开发者并没有一个完善或移植好的测试套件。Mike 让 Claude 创建了一个小脚本,针对新的移植版本和原始的 Python 代码库运行 7 个真实场景,并对结果进行差异对比。每个失败的场景都会分配一个自己的修复智能体,循环会一直运行直到所有七个场景都通过。
然后他更进一步。Claude 设计了自己的端到端测试套件,并自主运行了一整夜,修复出现的问题,连续四个晚上重复运行。结果,它捕获了任何场景列表都无法预见的小问题。
经验是,缺少测试套件并不会阻碍这一步。如果你无法继承一个裁判,就让 Claude 构建一个。无论如何,你的原始代码库才是事实依据。
代码迁移最佳实践
每一次运行都教会我们一些前一次没有学到的东西。可以肯定的是,你的下一次迁移会教会你本指南无法涵盖的事情。但有一些实践在所有项目中都行之有效:
- 不要盲目遵循本指南。每次迁移都不同。把它当作一个起点,在开始迁移之前,先与 Claude 一起规划你的具体迁移方案。
- 不要关注个别的失败。个别失败是循环的任务。修复智能体会逐步解决它们。你的注意力应该放在模式上。
- 让审查具有对抗性,让验证自动化。对抗性审查允许执行更长时间的任务,通常值得消耗这些 token。让脚本——编译器、差异对比工具、测试套件——来充当裁判。
- 不要对所有任务都使用最大的模型。Token 消耗集中在你的循环中,因此要有意识地设计它们。较小的模型能够很好地处理高并发的实现展开任务;将最大的模型留给审查环节以及任何编写其他智能体将遵循的规则的任务。
- 前置投入人力时间。规则手册和压力测试是最耗时的部分。之后的一切主要就是队列逐步清空。
- 让工作队列变得机械化和可恢复。“完成”应该意味着“输出文件已存在于磁盘上”。
审查循环结果,而非代码
Jarred 的 Bun 迁移现已投入生产,尽管每次迁移都有权衡。例如,大约 4% 的 Rust 代码位于“unsafe”块内,主要是 C/C++ 边界上的单行指针操作。
但新的代码库在可测量方面确实更好。团队工具能检测到的每个内存泄漏都已被修复:一个包含 2000 次重复构建的基准测试,内存占用从 6745 MB 下降到了 609 MB。在 Linux 和 Windows 上,二进制文件体积缩小了 19%。跨语言优化使其在 HTTP 服务以及诸如 next build 和 tsc 等实际工作负载上,速度提升了 2–5%。
考虑一下,是否到了重新计算你长期推迟的迁移项目的时候了。选择一个你一直在容忍的代码库,然后问问 Claude 针对它的迁移过程会是怎样的。
相关
- 迁移入门套件 注意:该入门套件是上述流程的通用模板——它并非这些特定移植项目实际运行时所使用的。
- 代码现代化插件——用于遗留系统现代化和框架升级,而非语言移植
- Claude Code 中的动态工作流
使用 Claude 改变你组织的运作方式
A step-by-step guide to running large code migrations with AI agents — including Bun's million-line Zig-to-Rust port.
- Category
- ProductClaude Code
- DateJuly 16, 2026
- Reading time5min
- https://claude.com/blog/ai-code-migration
Code migrations, projects that port a production codebase to a new language, were multi-year endeavors until recently.
In the last month, individual developers at Anthropic migrated 10 code packages consisting of tens to hundreds of thousands of lines of code using Claude Fable 5, Claude Opus 4.8, and dynamic workflows. In this article we’ll cover two examples along with best practices from these projects.
Jarred Sumner, co-founder of Bun and Member of Technical Staff at Anthropic, used Claude Code to migrate Bun from Zig to Rust. A million lines of code were produced in less than two weeks, with 100% of Bun's existing test suite passing in CI before merge. Nineteen regressions surfaced after merge and have all been fixed. The Rust port was shipped inside Claude Code in June.
Mike Krieger, co-lead of Anthropic Labs, migrated a Python codebase to 165,000 lines of TypeScript over a weekend. This included hundreds of agents, eight phase gates, three adversarial review rounds, and a final parity check that diffed every command's output against the Python original.
Claude Code’s new capabilities change the math for these long-deferred projects. Below is the six-step process we now use, drawn from what these migrations taught us.
The core insight is that you don’t fix the code. You fix the process (loop) that produced the code.
Why and when to migrate languages
Before going straight into the how, it’s worth discussing the when and why because the assumptions around these projects have evolved.
Teams launch migrations because of landscape changes between their initial build and current project. Either a known trade-off has become limiting, a better approach has emerged, or the original ecosystem is shrinking.
For example, Jarred originally chose Zig because it offered C-level performance with radical simplicity, ideal for a solo founder “writing Bun in 1 year in a cramped Oakland apartment pre-LLM.” This simplicity came with known tradeoffs, which he writes about here.
Fast forward to 2026. Bun's CLI is getting over 10 million monthly downloads and is used extensively within Claude Code.
As recently as last quarter, those tradeoffs wouldn’t have been enough to justify freezing the roadmap and committing resources to a multi-quarter project. Migrating languages can deliver smaller, faster, and safer systems, but no one wants to pay for them.
Software engineers have also had to contend with the career risk inherent in these formerly mega-projects. You could maintain two parallel code bases for quarters or years, and if the end result was 90% parity, you had a bigger headache than when you started.
Now, the worst case scenario is you delete the branch and try again.
There still needs to be a justifiable business case. While million line migrations no longer cost $3 to $4 million in engineering resources over the course of a four year project, they still cost tens to hundreds of thousands of dollars or more to execute. The Bun migration, for example, consumed 5.9 billion uncached input tokens and 690 million output tokens — around $165,000 at API pricing. The main portion of Mike’s port was 27 million tokens.

However, the migration case no longer needs to be existential. A year of memory-bug patches in the changelog, or one chronic bottleneck, can now justify it.
The compile step was the impetus for Mike's project. The internal tool his team works on ships to users as a single binary. Producing that binary with the Python toolchain took roughly eight minutes per platform, totaling a 30-minute wait across the build matrix on every release. After the port, the same compile now takes about two seconds, the binary starts 6x faster, and the team was able to retire a separate deployment pipeline.
Why AI changes the code migration math
Claude Fable 5 is our most capable, generally available model. Fable and Opus 4.8 are particularly good at delegating, directing, and verifying parallel workstreams with subagents while finding multiple paths towards stated goals.
Large code migrations are a particularly effective use case for these advanced models because:
- The work is parallel. Work can be executed across thousands of independent units such as files and crates, so agents can work at the same time rather than have one waiting on the other.
- Context is clear and comprehensive. The old code serves as a great spec for the model. It also serves as a core reference to help build the guide for translation agents to follow.
- There is a built-in referee. Many large codebases will include a test suite that agents can use to verify their work. Agents perform their best when verification is objective, because the model can grind against a ground truth for days without a human arbitrating quality.
- The queue writes itself. When a compiler or test run fails, that becomes the next item for an agent to fix.
- They require consistency and edge case handling: The process is built so drift has nowhere to hide: reviewers cite the rule behind every finding, so a violation becomes a queue item instead of a quiet divergence. And when an agent does hit an edge case, the fix becomes a rule every subsequent agent follows.
As we will see below, both Mike and Jarred used Fable for key steps in their migration process, particularly in an advisory pattern that used multiple model classes to optimize token consumption.
Six steps for large code migrations
The process below has been generalized to be relevant to multiple languages and scenarios. For additional details, you can read Jarred’s blog.
Prerequisites
A prerequisite before starting on your migration project is to have a strong judge in place, otherwise you won’t have an exit condition or measure of success.
The judge must be able to evaluate both the original code and the target code on equal terms. Test suites written in the original language will often depend on internal functions that won't exist in the target code.
To build this judge:
- Categorize existing tests. Use Claude to identify which tests are expressible as external calls and which depend on internals that won't port.
- Rewrite for portability. Convert the external-facing tests into assertions that can run against both the original and the port. Use adversarial agents to verify the rewritten tests don't weaken the assertions.
- Validate the judge. Run it against the original code to confirm it passes. Then run it against deliberately broken code to confirm it fails — a judge that doesn't catch breakage isn't a judge.
Jarred had a large test suite written in a third language (TypeScript), but that will not be the case for most projects. For his Python-to-TypeScript port, Mike created a parity harness of seven real-world scenarios and considered any behavior change a bug to be fixed.
Before we get into each stage, this graphic may help you follow along. This mostly follows Jarred’s methodology, with reviews and gates at each stage. Mike followed a similar overall structure using similar loop workflows, but he ran the entire migration end to end, revised the rules and the workflow based on the results, and ran it again — discarding the output each time until the third run.
Step 1 — Create the rulebook, dependency map, and gap inventory
In this stage we are creating the foundations of our migration: an inventory of places where code will need to be refactored rather than just translated, a rulebook for how to translate our code, and a dependency map to order our migration implementation workstreams.
The order matters: the rulebook must come before the gap inventory. The gap inventory is defined by what the rulebook's defaults won't cover, and the two are tested together in a joint audit.
Rulebook
The exact shape of the rulebook depends on key architectural decisions you must make at the start. Chief among them, if the new code will follow the same structure, or if it will be completely redesigned.
If it’s the former (Jarred), the rulebook will primarily be lookup tables that translates types and idioms between languages while pointing to the gap inventory for the harder-to-translate components. If it’s the latter (Mike), it will be a design document.
Jarred created his rulebook by chatting with Claude, forming a policy for each area of ambiguity. He also used eight subagents specifically designed to review for 8 different categories of common failure modes based on his own intuition.
Dependency map
You need to understand file dependencies to effectively break up workstreams for a parallel migration so you know which files to migrate first and which files to contain in the same batch. Some languages and codebases have explicit manifests that make this easy, but for legacy codebases and many popular languages like C/C++ and Python, these dependencies need to be discovered and mapped.
Claude Code can deploy agents to create and run a deterministic script to produce this map. The prompt in the migration kit uses a workflow to create a review-and-fix loop. Note: The starter kit is a generalized template of the process laid out in this post — it's not what these specific ports ran on.
Gap inventory and skeptic reviewers
The new language has different requirements from the old language that must be met. For Zig to Rust the difference was manual memory management (C and C++ work the same way). For example:
Zig
fn readConfig(allocator: std.mem.Allocator) ![]u8 {
const buf = try allocator.alloc(u8, 1024);
// ...fill buf...
return buf; // caller must free this — but only the comment says so
}
// A caller that forgets 'defer allocator.free(buf)' still compiles — the leak only surfaces at runtime. Rust
fnread_config() -> Vec<u8> {
let buf = vec![0u8; 1024];
// ...fill buf... buf // ownership moves to the caller; memory is freed automatically }
// Use it after it's moved? Free it twice? Neither compiles. // Forget to free it? There's no free call to forget — drop is automatic. For Python to TypeScript the gap was interfaces and contracts. Python doesn’t require a contract declaring what shape of object it will accept or what it returns, but TypeScript does. For example:
Python
def(handler): handler.setup()
return handler.run({"retries": 3})
# Any object with .setup() and .run() works here. Which objects actually get passed in? Read the whole codebase to find out. TypeScript
interface RunResult { ok: boolean }
interface Handler
{ setup(): void;
run(opts: { retries: number }): Promise<RunResult>;
}
function(handler: Handler): Promise<RunResult> {
handler.setup();
return handler.run({ retries: 3 }); }
// The contract must be written down before this compiles Both Jarred and Mike created gap inventory files capturing this implicit knowledge. Jarred inventoried these gaps up front, which is what we do here, while Mike chose to translate first and then create the gap inventory by auditing afterwards. You may need to do both.
Check out this sample Claude Code prompt to create a gap inventory file.
Step 2 — Stress-test the rules
This step involves a mini-migration that serves as a “shakedown cruise” for the larger migration.
In this step, Jarred used one agent to translate three files using the rulebook, one agent to translate three files “like a senior Rust engineer,” and one agent to use the diff to create new translation rules. At this stage he caught two critical issues that would have created numerous issues if fanned out across all 1,448 files.
The prompt may look something like this.
This type of stress test only works for structure-preserving migrations, where two translations of the same file are comparable line by line. If your rulebook is a redesign — like Mike's — the equivalent test is attacking the design document directly with adversarial reviewers, then validating it with a disposable end-to-end run.
Regardless, throw out any translated files. The goal is to refine the rules, not make incremental progress.
Step 3 — Translate everything
For the remaining steps, you run the same multi-agent loop architecture: implement, review, and fix.
You can offload implementer work to smaller models and keep reviewers on larger ones. For example, Mike used Claude Sonnet when he fanned out 12 subagents for the main migration.
The work queue should be mechanical. A batch script decides what’s done by checking whether the translated file exists on disk, then slices the pending files into batches for the implementer agents. Because the queue is rebuilt from disk every time, the migration is resumable by construction.
At this stage, agents can be overly cautious with how much work they do. The fix can be a blunt, emphatic prompt instruction with context that the compiler will catch mistakes in the next step.
Anything the translator can’t execute confidently gets flagged with // TODO(port): <reason> to be dealt with in step 4. From here on, the to-do lists write themselves: the compiler enumerates the errors, the smoke tests find the crashes, the suite reports the failures.
Two adversarial reviewers evaluate the work of the implementers using separate contexts and disagreement between reviewers goes to a third agent. When a reviewer keeps catching the same mistake across files, the fix isn't per-file. You add one sentence to the rulebook and regenerate the affected batch. The rulebook keeps growing through this step; the code never gets hand-patched against it.
One important design decision to note in this step is where the compiler sits. Mike ran the TypeScript compiler inside every loop, because it checks a unit in seconds. Jarred banned the compiler from the loop entirely and deferred it to the next step, because cargo takes minutes.
At this step, much of the heavy lifting has been done and the prompts start to get shorter.
Steps 4, 5, 6 — Compile, run, and match behavior
These three steps share the same loop architecture and need progressively less human judgment, so we cover them together.
Step 4, for example, may often dissolve into step 3 depending on the language and size of the migration.
Depending on the size and difficulty of the compiler step, agents may not run this at all. Jarred executed this with an orchestrator script that invoked the compiler once across the whole workspace. “Fixer agents” then ran through the error list in parallel with adversarial review. The build runs again, rinse and repeat.
Reviewing the error list is helpful to catch systemic issues that may require adjustments. For example, Jarred ran into thousands of Rust module errors that surfaced after fixing cyclic imports that Zig's lazy compilation tolerated. He fixed the loop by encoding logic to classify which dependence to delete, move, or restructure the boundary.
Step 5 also has a mechanical source of truth similar to the compiler error list: crashes from the smoke test. Again, the loop fix was to group issues into categories, in this case grouping causes by root cause that are reviewed by adversarial subagents.
Step 6 and the end of our story is comparing the programs’ behavior across the two codebases.
Our files have now been translated, compiled, and smoke tested. Now it's time to shard them and run the test suite (from the prerequisite stage) against them. Tackle failures with "fixer agents" that review the failed tests against both codebases. Adversarial reviewers check their fixes.
The next stage in this loop is a build daemon, which is the only process allowed to rebuild the binary. Fixers write patches; the daemon batches them, rebuilds once, re-runs the affected tests, and feeds the results back. This serializes the most expensive operation instead of letting multiple agents trigger it independently.
When the same failure repeats across many tests, the fix moves upstream: you amend the rule that produced the bug and regenerate only the files that rule touched.
Mike's approach matters here, because many developers won't have a built-out or ported test suite. Mike had Claude create a small script to run 7 real-world scenarios against both the new port and the original Python codebase, and diffed the results. Each failing scenario got its own fix agent, and the loop ran until all seven passed.
Then he went one step further. Claude designed its own end-to-end test suite and ran it autonomously overnight, fixing what broke and re-running four nights in a row. As a result, it caught the paper cuts no scenario list would have predicted.
The lesson is that a missing test suite doesn't block this step. If you can't inherit a referee, have Claude build one. Your original codebase is the ground truth either way.
Code migrations best practices
Every run taught us something the previous one didn't. It’s a safe bet your next migration will teach you things this guide can't. But a few practices held up across every project:
- Don't follow this guide blindly. Each migration is different. Treat this as a starting point, and plan your specific migration with Claude before committing to it.
- Don’t focus on individual failures. Individual failures are the loop's job. Fixer agents burn those down. Your attention belongs on the patterns.
- Make review adversarial and verification mechanical. Adversarial review allows for longer running tasks and is often worth the token consumption. Let scripts — a compiler, a diff, a test suite — be the referee.
- Don't use the largest model for everything. Token spend concentrates in your loops, so design them deliberately. Smaller models handle the high-volume implementation fan-out well; save your largest model for reviewers and for anything that writes rules other agents will follow.
- Front-load the human hours. The rulebook and the stress test are the most time-consuming. Everything after is mostly queues burning down.
- Make the work queue mechanical and resumable. Done should mean "the output file exists on disk."
Review loop results, not code
Jarred’s Bun migration is now in production, although every migration has tradeoffs. For example, about 4% of the Rust code sits inside "unsafe" blocks, mostly single-line pointer operations at C/C++ boundaries.
But the new codebase is measurably better. Every memory leak the team's tooling can detect has been fixed: one benchmark of 2,000 repeated builds dropped from 6,745 MB of memory to 609. The binary is 19% smaller on Linux and Windows. And cross-language optimization made it 2–5% faster across HTTP serving and real-world workloads like next build and tsc.
Consider whether it’s time to re-run the math of your long deferred migration. Pick the codebase you've been tolerating and ask Claude what the migration process looks like for it.
Related
- Migration starter kit Note: The starter kit is a generalized template of the process above — it's not what these specific ports ran on.
- Code-modernization plugin — for legacy modernization and framework upgrades rather than language ports
- Dynamic workflows in Claude Code