Anthropic、Claude Code で大規模コード移行を解説
Anthropic は、Claude Code を活用して大規模なコード移行プロジェクトを成功させるための具体的な 6 つのステップと、テストスイートの再構築を含む実用的なフレームワークを公開した。
キーポイント
強力な判定基準(Judge)の構築
移行の成否を測るためには、元コードと新コードを同等に評価できる「判事」が必要であり、内部依存のない外部テストへの再分類や、破損検出能力の確認が不可欠である。
ルールブックと依存関係マッピング
移行前に翻訳規則(ルールブック)を定義し、リファクタリングが必要な箇所を特定するギャップインベントリを作成することで、作業の順序と優先順位を明確化する。
反復的なワークフローの実行
一度きりの実行ではなく、結果に基づいてルールやワークフローを見直し、出力を破棄して再実行するプロセスを繰り返すことで、最終的に安定した移行を実現する。
AI エージェントの役割分担
Claude を用いてテストの分類や変換を行い、敵対的エージェントを用いてテストの弱体化を検証するなど、複数の AI エージェントを協調させて品質を保証する手法が示された。
重要な引用
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.
Use adversarial agents to verify the rewritten tests don't weaken the assertions.
影響分析・編集コメントを表示
影響分析
この記事は、LLM を単なるコード生成ツールとしてではなく、大規模なシステムアーキテクチャ変更を管理・実行する自律的なパートナーとして活用するための具体的なプラクティスを提示しています。特に「テストの再構築」や「反復的な検証プロセス」に焦点を当てている点は、AI 導入における実務家の最大の懸念である「品質保証とリスク管理」に対する有力な回答となっており、企業レベルでのコード移行プロジェクトの標準化に寄与する可能性が高いです。
編集コメント
Claude Code を用いた大規模コード移行の具体的な手法を、単なるツール紹介ではなく「プロセス設計」の観点から解説しており、実務レベルでの AI エージェント活用における重要な指針となる内容です。特にテストスイートの再構築と反復検証の重要性を強調している点は、AI 導入における品質担保の課題に対する鋭い洞察と言えます。
コードの大規模移行を成功させる 6 つのステップ
*以下のプロセスは、複数の言語やシナリオに通用するように一般化されています。詳細については、Jarred のブログをご覧ください。
事前準備
移行プロジェクトを開始する前に最も重要なのは、「合格・不合格を判定する厳格な基準(ジャッジ)」を用意することです。これがないと、いつ完了したのかの判断基準や、成功の定義が曖昧になってしまいます。
このジャッジは、元のコードと移行後のコードを公平に比較評価できるものでなければなりません。注意すべき点は、元の言語で書かれたテストスイートの多くが、内部関数に依存している場合があることです。しかし、その内部関数は移行先のコードには存在しない可能性があります。
こうしたジャッジを構築するには、以下の手順を踏みます。
- 既存のテストを分類する: Claude を使って、外部呼び出しとして表現できるテストと、移植できない内部機能に依存するテストを区別します。
- 移植性を高めるよう書き直す: 外部からアクセスされるテストを、元のコードと移行後のコードの両方で実行可能なアサーション(検証)に変換します。さらに、敵対的なエージェントを使って、書き直したテストがアサーションの強度を弱めていないかを確認します。
- ジャッジを検証する: まず元のコードで実行し、合格することを確認します。次に、意図的に壊したコードで実行して、確実に不合格になることを確認します。バグを見逃すようなジャッジでは意味がありません。
Jarred のケースでは、TypeScript で書かれた大規模なテストスイートが存在しましたが、これは特殊な例です。多くのプロジェクトには当てはまりません。Mike が Python から TypeScript への移行を行った際は、7 つの実際のユースケースを網羅する「パリティ・ハネス(同等性検証用フレームワーク)」を作成し、動作に少しでも変化があればバグとして扱うという厳格な基準を設けました。
各段階に入る前に、以下の図を参考にすると理解しやすくなります。このアプローチは主に Jarred の手法に基づいていますが、各工程にレビューと承認ゲートを設けています。Mike も同様の全体構造とループワークフローを採用しましたが、彼は移行の全工程を一度完遂し、その結果を踏まえてルールやワークフローを見直した上で再実行しました。このプロセスは、3 回目の実行まで毎回出力を破棄して繰り返されます。

ステップ 1 — ルールブック、依存関係マップ、ギャップインベントリの作成

この段階では、移行の基盤となる 3 つの要素を整備します。具体的には、単なる翻訳ではなくリファクタリングが必要な箇所のインベントリ(目録)、コード変換のためのルールブック、そして移行作業の実施順序を決める依存関係マップです。
これらを作成する順番は重要です。まずルールブックを策定し、その後にギャップインベントリを作成します。なぜなら、ギャップインベントリとは「ルールブックのデフォルト設定ではカバーしきれない部分」を定義するものだからです。両者は統合監査を通じて互いに検証されます。
ルールブック
ルールブック の具体的な構成は、移行開始時に下す重要なアーキテクチャ上の判断によって決まります。最も重要なのは、新コードが既存の構造を踏襲するものとするか、それとも完全に再設計するものとするかという点です。
前者(Jarred氏の場合)であれば、ルールブックは主に型や言語間の慣習を翻訳するためのルックアップテーブルと、難易度の高いコンポーネントのギャップを把握するためのインベントリリストから構成されます。後者(Mike氏の場合)であれば、それは設計ドキュメントとなります。
Jarred氏はClaudeとの対話を通じてルールブックを作成し、曖昧さが生じうる各領域に対する方針を定めました。また、自身の直感に基づき、8 つの主要な失敗モードカテゴリごとにレビューを行うよう特別に設計された 8 つのサブエージェントも活用しています。
依存関係マップ
並列移行で作業を効果的に分割するためには、ファイル間の依存関係を理解しておく必要があります。どのファイルを最初に移行し、どのファイルを同じバッチに含めるべきかを把握するためです。一部の言語やコードベースでは、これを容易にする明示的なマニフェストが存在しますが、レガシーシステムや C/C++、Python といった多くの人気言語では、これらの依存関係を手動で発見してマップ化する必要があります。
Claude Code を活用すれば、エージェントをデプロイしてこのマップを作成・実行する決定論的なスクリプトを実行できます。移行キットに含まれるプロンプト では、レビューと修正のループを回すワークフローを採用しています。*注:スタートキットは本記事で説明したプロセスの汎用的なテンプレートであり、今回の特定のポート移行で実際に使用された環境とは異なります。*
ギャップの整理と懐疑的なレビュアー
新しい言語には、古い言語とは異なる要件を満たす必要があります。例えば、Zig から Rust への移行では「手動メモリ管理」が大きな違いでした(C や C++ も同様です)。具体的には以下のようになります。
Zig
fn readConfig(allocator: std.mem.Allocator) ![]u8 {
const buf = try allocator.alloc(u8, 1024);
// ...buf にデータを埋める...
return buf; // コール側で必ず解放する必要があるが、コメントにしか書かれていない
}
// 'defer allocator.free(buf)' を忘れた呼び出し元でもコンパイルは通る——メモリリークは実行時になって初めて発覚する。Rust
fn read_config() -> Vec<u8> {
let buf = vec![0u8; 1024];
// ...buf にデータを埋める...
buf // 所有権が呼び出し元に移動し、メモリは自動的に解放される
}
// 一度移動した後にこれを使う?二度解放する?どちらもコンパイルエラーになる。
// 解放を忘れる?解放するコード自体が存在しない——drop は自動的に行われる。一方、Python から TypeScript への移行では、「インターフェースと契約」の存在が大きなギャップとなりました。Python では、受け取るオブジェクトの形状や返り値の型について宣言する必要はありませんが、TypeScript ではそれが必須です。具体例を見てみましょう。
Python
def register(handler):
handler.setup()
return handler.run({"retries": 3})
# .setup() と .run() メソッドさえあればどんなオブジェクトでも通る。実際に渡されるのはどのオブジェクトか?コードベース全体を読み込まないとわからない。TypeScript
interface RunResult { ok: boolean }
interface Handler
{ setup(): void;
run(opts: { retries: number }): Promise<RunResult>;
}
function register(handler: Handler): Promise<RunResult> {
handler.setup();
return handler.run({ retries: 3 });
}コンパイル前に契約書(ルール)を明文化しておく必要があります。Jarred と Mike は、この暗黙知を把握するために「ギャップインベントリファイル」を作成しました。Jarred は事前にこれらのギャップを整理するアプローチを取りましたが、Mike はまず翻訳を実行し、その後に監査を行ってギャップを特定する方法を選びました。状況によっては、両方の手法を組み合わせる必要があるかもしれません。
ギャップインベントリファイルの作成には、以下の Claude Code プロンプトサンプルが役立ちます:Claude Code プロンプトでギャップインベントリを作成する。
ステップ 2 — ルールの耐性テスト

このステップは、大規模な移行に向けた「試運転」のようなミニ移行です。
ここでは Jarred が 3 つの異なるアプローチを試しました。1 つ目はルールブックに従って 3 ファイルを翻訳するエージェント、2 つ目は「シニア Rust エンジニアのように振る舞う」エージェントで 3 ファイルを翻訳するもの、そして 3 つ目は差分(diff)を分析して新しい翻訳ルールを作成するエージェントです。この段階で、もし全 1,448 ファイルに拡大していたら深刻な問題を引き起こしたであろう 2 つの重大な課題を発見できました。
プロンプトは以下のような内容になります:こちら。
この種の負荷テストは、2 つの翻訳結果をファイル単位で一行ずつ比較できる「構造維持型」の移行にのみ有効です。マイクのようにルールブック自体を再設計する場合は、代わりに設計ドキュメントに対して敵対的なレビューアーによる攻撃を行い、その後、使い捨ての完全な実行(エンドツーエンド・ラン)で検証するのが等価なテストとなります。
いずれにせよ、翻訳済みのファイルはすべて破棄してください。今回の目的は漸進的な改善ではなく、ルール自体を洗練させることです。
ステップ 3 — 全ファイルを翻訳する

残りのステップでも、実装・レビュー・修正を行うマルチエージェント・ループ・アーキテクチャを同じように適用します。
実作業は小規模なモデルに任せ、レビュー担当には大規模なモデルを割り当てるのが効率的です。例えばマイクは、主要な移行で 12 のサブエージェントを並列実行する際に Claude Sonnet を活用しました。
作業キューの管理は機械的に行います。バッチスクリプトがディスク上に翻訳済みファイルが存在するか確認して完了判定し、残りのファイルをバッチごとに分割して実装エージェントに割り当てます。キューは毎回ディスクの状態から再構築されるため、移行プロセス自体が自動的に再開可能になっています。
この段階では、エージェントが作業範囲を必要以上に慎重に設定してしまう傾向があります。その対策として、コンパイラが次のステップでミスを検出できるという文脈を含めた、明確かつ力強いプロンプト指示を出すことで対応します。
翻訳ツールが自信を持って実行できない箇所は、// TODO(port): <理由> という形式でフラグを立て、ステップ 4 で対応します。これ以降は、ToDo リストが自動的に生成されます。コンパイラーがエラーを列挙し、スモークテストがクラッシュを検出し、テストスイートが失敗を報告するのです。
実装者の作業は、2 人の対照的なレビューアーがそれぞれ異なる文脈で評価します。もし両者の意見に相違が生じれば、その案件は第 3 のエージェントへ回されます。あるレビューアーが複数のファイルで同じミスを繰り返し見つけた場合、ファイルごとの修正ではなく、ルールブックに一文追加して該当バッチを再生成します。このステップを通じてルールブックは成長し続けますが、コードに対して手動の修正(ハンドパッチ)を加えることは決してありません。
このステップにおける重要な設計判断の一つは、コンパイラーの配置場所です。マイクは TypeScript コンパイラーをループ内で実行しました。なぜなら、単位のチェックには数秒しかかからないからです。一方、ジャレッドはコンパイラーをループ内から完全に排除し、次のステップへ延期することを禁止しました。Cargo を使う場合、コンパイルに数分かかるためです。
この段階では、多くの重労働が完了しており、プロンプトの長さが短くなり始めています。
ステップ 4, 5, 6 — コンパイル、実行、動作の一致確認

これら 3 つのステップは同じループアーキテクチャを共有しており、人間の判断が必要となる度合いが段階的に低くなるため、まとめて解説します。
ステップ 4 は、移行対象の言語や規模によっては、実際には ステップ 3 に統合されることも珍しくありません。
コンパイラ処理の規模や難易度によっては、エージェントがこれを一切実行しないケースもあります。Jarred の場合は、ワークスペース全体で一度だけコンパイラを呼び出すオーケストレーションスクリプトを実行し、その後に「修正用エージェント(Fixer agents)」がエラーリストを並列処理しながら、敵対的なレビューを行いました。ビルドを再実行して、これを繰り返すのです。
エラーリストを確認することは、調整が必要なシステム全体の課題を発見する上で役立ちます。例えば、Jarred は Zig の遅延コンパイルで許容されていた循環参照のインポートを修正した結果、何千もの Rust モジュールのエラーに直面しました。彼は依存関係を削除するか、移動させるか、境界を再構築すべきかを分類するロジックを実装することで、このループ問題を解決しました。
ステップ 5 もまた、コンパイラエラーリストと同様に機械的な真実の根拠を持っています。それが「スモークテスト」からのクラッシュです。先ほどのループ修正と同じく、ここでは根本原因に基づいて課題をカテゴリ分けし、敵対的なサブエージェントがその原因を検証する仕組みを採用しています。
ステップ 6 は、物語の結末となる部分で、2 つのコードベース間でプログラムの挙動を比較します。
ファイルはすでに翻訳され、コンパイル済み、スモークテストも完了しました。次はこれらのファイルを分割(shard)し、事前準備段階から用意したテストスイートを実行する番です。失敗したテストについては「修正用エージェント」が両方のコードベースを比較レビューして対応します。その修正内容に対しては、敵対的なレビュアーがチェックを行います。
このループの次の段階は、ビルドデーモンです。build daemon はバイナリを再構築できる唯一のプロセスとして機能します。修正担当(フィクサー)がパッチを作成しますが、実際の再構築はデーモンが一括処理して行います。つまり、パッチをまとめて一度だけビルドし、影響を受けたテストだけを再実行して結果をフィードバックする仕組みです。これにより、複数のエージェントが独立して高コストな再構築を実行するのを防ぎ、処理を直列化しています。
同じ失敗が多数のテストで繰り返される場合、修正は上流へ遡ります。バグの原因となったルール自体を修正し、そのルールが影響したファイルだけを再生成すればよいのです。
マイクのこのアプローチは非常に重要です。なぜなら、多くの開発者はすでに整備されたテストスイートを持っておらず、移植も済んでいないからです。マイクは Claude に、新しい移植版と元の Python コードベースの両方に対して 7 つの実世界シナリオを実行する小さなスクリプトを作成させ、その結果を比較(diff)しました。各失敗したシナリオには専用の修正エージェントが割り当てられ、すべてが成功するまでこのループが回されました。
さらに一歩踏み出し、Claude は自らエンドツーエンドのテストスイートを設計し、夜間に自律的に実行しました。壊れた箇所を自動で修正し、4 晩連続で再実行を繰り返したのです。その結果、事前にリストアップされたシナリオでは予測できなかった「紙切り傷」のような細かい問題も発見できました。
ここでの教訓は、テストスイートが欠けていてもこのステップが阻害されるわけではないということです。既存の審判役(リファリー)を受け継げないなら、Claude に作らせればよいのです。どちらにせよ、元のコードベースこそが絶対的な正解(グランドトゥルース)なのですから。
コード移行のベストプラクティス
これまでの実行を通じて、前回とは異なる教訓を毎回得てきました。あなたの次の移行プロジェクトでも、このガイドではカバーしきれない新たな課題が現れる可能性が高いでしょう。しかし、どのプロジェクトでも通用するいくつかの重要な原則があります。
- このガイドを盲目的に鵜呑みにしないでください。各移行は状況が異なります。まずはここをスタート地点とし、具体的な移行計画を立てる前に Claude へ相談して調整することをお勧めします。
- 個々の失敗に一喜一憂しないことです。個々のエラー処理はループの役割です。修正エージェント(Fixer agents)がそれらを解消してくれます。あなたが注力すべきは、パターンや傾向を見極めることにあります。
- レビューは対立的に、検証は機械的に行ってください。対立的なレビュー(adversarial review)なら、長時間実行されるタスクも許容されることが多く、トークン消費の増大に見合う価値があります。審判役にはスクリプト(コンパイラ、差分ツール、テストスイートなど)を任せてください。
- すべてに最大規模のモデルを使う必要はありません。トークンコストはループ内で集中するため、ここは慎重に設計する必要があります。小規模なモデルで大量の実装タスクを処理し、最大規模のモデルはレビュー役や、他のエージェントが従うべきルールを生成する役割に限定しましょう。
- 人的リソースは初期段階に集中投入してください。ルールブックの作成とストレステストが最も時間を要します。それ以降の工程は、主にキュー(待機列)が処理されていくプロセスになります。
- 作業キューは機械的かつ再開可能に設計してください。「完了」の定義は「出力ファイルがディスク上に存在すること」と明確に定めてください。
コードそのものより、レビューループの結果を重視する
Jarred による Bun への移行はすでに本番環境で稼働しています。ただし、どんな移行にもトレードオフはつきものです。例えば、Rust コードの約 4% は「unsafe」ブロック内に存在します。これらは主に C/C++ の境界線で行われる単一行のポインタ操作です。
しかし、新しいコードベースは明らかに改善されています。チームのツールが検出できるメモリリークはすべて解消されました。2,000 回のビルドを繰り返すベンチマークでは、使用メモリの最大値が 6,745 MB から 609 MB に劇的に減少しています。また、バイナリサイズは Linux と Windows の両方で 19% 縮小され、HTTP サービングや次世代のビルド(next build)、型チェックツール(tsc)といった実世界のワークロードにおいて、クロス言語最適化によって 2〜5% の速度向上が実現しました。
長らく先送りしてきた移行プロジェクトについて、改めて計算し直す時期ではないでしょうか。これまで我慢して使ってきたコードベースを選び、Claude にその移行プロセスを問いかけてみてください。
*関連記事*
- 移行用スターターキット:注記として、このスターターキットは上記の工程を一般化したテンプレートであり、今回の特定のポートで実際に使用された環境とは異なります。
- コード近代化プラグイン:言語間の移植ではなく、レガシーシステムの近代化やフレームワークのアップグレード向けに設計されています。
- Claude Code における動的ワークフロー
原文を表示
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
fn read_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 register(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 register(handler: Handler): Promise<RunResult> {
handler.setup();
return handler.run({ retries: 3 }); }
// The contract must be written down before this compilesBoth 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
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み