Anthropic、Claude Code で大規模コード移行を解説
Anthropic は、自社の AI エージェント「Claude Code」を実際の社内インフラに適用し、大規模なコード移行を自律的に実行した事例を公開しました。
キーポイント
自己改善型コード移行の実施
Anthropic は開発者による手動介入を最小限にし、Claude Code が移行対象のコードベースを理解し、依存関係を解析して自動的にリファクタリングと移行を行うプロセスを実践しました。
大規模プロジェクトでの信頼性検証
単なるテストケースではなく、実際の社内の複雑なコードベースに対して適用することで、AI エージェントが本番環境に近い条件下で安全かつ正確に動作する能力を検証しました。
開発者ワークフローの変革
従来の「人間が指示し、AI が実行」から、「AI が計画を立て、人間が監査・承認する」という新しい開発パラダイムへの移行を示唆しています。
重要な引用
Anthropic runs large-scale code migrations with Claude Code
The process involved the model understanding the codebase, planning the migration, and executing changes autonomously.
影響分析・編集コメントを表示
影響分析
この記事は、AI エージェントが単なる実験段階を超え、実際の企業インフラの維持・刷新において中核的な役割を果たし得ることを示す決定的な証拠となります。開発現場における生産性向上だけでなく、複雑なコードベースの理解と操作という課題に対する解決策として、Claude Code の実用性が業界全体で再評価されるきっかけとなるでしょう。
編集コメント
「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 // 所有権が呼び出し元に移動し、メモリは自動的に解放される
}
// 移動後にこれを使う?二度解放する?どちらもコンパイルエラーになる。
// 解放を忘れる?forget を忘れるような free コール自体が存在しない——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 prompt to create a gap inventory file。
ステップ 2 — ルールの耐性テスト

このステップでは、大規模な移行前の「試運転」として、小規模なマイグレーションを実施します。
ここでは Jarred が 3 つのファイルについて、ルールブックに従って翻訳するエージェント、シニア Rust エンジニアのように振る舞うエージェント、そして差分(diff)を分析して新しい翻訳ルールを作成するエージェントという、3 つの異なるアプローチでテストを行いました。その結果、1,448 ファイルすべてに展開された場合、深刻な問題を引き起こす可能性があった 2 つの重大な課題を発見することができました。
プロンプトは、以下のような内容になる可能性があります:this。
この種の負荷テストは、2 つの翻訳結果をファイルごとに比較できる「構造維持型」の変換にのみ有効です。マイクのようにルールブック自体を再設計する場合は、代替案として設計ドキュメントに対して敵対的なレビューアーを投入し、その後、使い捨ての完全な実行環境で検証を行うテストが必要です。
いずれにせよ、翻訳済みのファイルはすべて破棄してください。今回の目的は漸進的な改善ではなく、ルールそのものを洗練させることです。
ステップ 3 — 全ファイルを翻訳する

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

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