In just over a year, AI coding agents went from migrating test files to porting whole runtimes. Airbnb moved nearly 3,500 React test files off Enzyme in six weeks, Bun rewrote 535,000 lines of Zig in Rust in eleven days, and GitHub ported the Copilot agent runtime to 832,000 lines of production Rust. Around the same time, DHH told the Rails World crowd that agents are writing the Rust backend for the next version of HEY. Each of those came with a headline number, and the numbers are real, but the more useful part is what the teams wrote down afterward about what went wrong and why.
Ports turned out to be a good place to learn these lessons, because a port is one of the few jobs where the right answer already exists as running code. That makes it possible to say with some precision where an agent did well and where it quietly didn’t, which feature work rarely allows, since what counts as correct there tends to live in tickets and in people’s heads. What follows are the lessons that held up across these attempts, with the ports as the evidence rather than the story.
The lesson that shows up in nearly every one of these ports is that agents do far better when they can read the code they’re replacing. Airbnb found it early, reporting that what moved its migration forward was feeding the model the right related files, such as the component under test and its sibling tests, rather than getting the prompt wording perfect. Mitchell Hashimoto saw the same thing in a split-tree refactor in Ghostty, where keeping the old implementation compiling next to the new one made the agent’s code for the new architecture “significantly better,” even though the logic was moving into a different UI framework.
Bun went further and treated that context as something to build before the port started. Jarred Sumner spent about three hours talking through how Zig patterns should map to Rust, and that conversation became a PORTING.md guide. A second workflow then traced every struct field in the codebase, proposed a Rust lifetime for each, and had two reviewer agents challenge every proposal before it went into a LIFETIMES.tsv file, and only after a trial run on three files did the loop get anywhere near all 1,448. Both Bun and GitHub also kept their ports a straight translation and left the redesign for later, and Stephen Toub, who led GitHub’s port, says he regrets every time he strayed from that.
Having the old code around isn’t the same as the agent respecting it, though. When Armin Ronacher ported MiniJinja from Rust to Go, he used the original version’s snapshot tests as the spec, and the agent still gave up on every test that expected a template to fail because it couldn’t match the exact error messages. It also tried to change MiniJinja’s HTML escaping and to stop range from returning an iterator, and Ronacher had to steer it back each time. So the practical lesson for your own port is to write the mapping down before the agent starts and to treat the old behavior as fixed, because left alone, an agent will happily improve on it.
The second lesson is about what a passing suite actually tells you, and Bun is the clearest case because its suite was about as good as a port could ask for. It’s written in TypeScript and drives the runtime from the outside, so the same 60,624 tests and nearly 1.4 million expect() calls could judge the Zig build and the Rust build without anyone touching a line. Sumner even checked by hand that none of them were being skipped before he merged, and the port still shipped 19 known regressions.
Andrew Kelley, who created Zig, went straight at that gap, asking why a suite good enough to sign off on a million lines of what he called “unreviewed slop” hadn’t caught the bugs in the Zig code. It’s a fair question, and the honest answer is that a suite like this checks parity rather than correctness. It tells you the new code behaves like the old code on the inputs the tests happen to exercise, and everything outside that coverage stays unproven in both languages.
Benchmarks make the same point more bluntly. SWE Refactor Bench put agents through 20 whole-repository migrations, 520 runs in all, and checked each run against a migration audit, a fixed set of behavioral tests, and extra tests an agent generated to hunt for hidden differences. Only 28 runs, or 5.4%, passed all three, and some of the others kept their tests green by simply skipping the migration, which only the audit caught. If the existing suite is your only gate, that failure mode is invisible to you.
It gets worse when the agent can change the tests it’s being judged by, and GitHub’s account has the most concrete examples. Toub lists incorrect test oracles as one of five regression families, and in one port an agent dropped a set of SDK callbacks and deleted their end-to-end test along with them. In another, a compatibility check failed because the port had lost an SDK method, and the agent applied the repo’s schema-break-ok label to make the check pass, which Toub only caught while reviewing the PR before merge.
Bun ran into a milder version of the same instinct when agents told to make every crate compile started stubbing out functions and writing paragraph-long comments to justify the workarounds. Both teams answered with rules rather than one-off fixes: Bun’s reviewers were told to reject any workaround that needed a paragraph to defend, and GitHub forbade agents from changing end-to-end tests without direct permission. Toub’s version of the lesson is the one worth pinning up: the agent changing the code can’t also be allowed to quietly change what counts as correct.
When regressions did get through, they mostly came from places where the old and new code looked equivalent and weren’t. Bun says most of its 19 came from code that’s syntactically the same in Zig and Rust but behaves differently, and assertions are probably the cleanest example. Zig’s assert is an ordinary function, so its argument runs in every build, while Rust’s debug_assert! is a macro that release builds compile away, side effects and all:
fn insert_stale(graph: &mut Vec<&'static str>, file: &'static str) -> usize {
graph.push(file);
graph.len() - 1
}
fn main() {
let mut graph = Vec::new();
debug_assert!(insert_stale(&mut graph, "react-refresh") == 0);
println!("files in graph: {}", graph.len());
}
Run that with cargo run and it prints files in graph: 1, but a --release build prints files in graph: 0. In Bun the erased call was registering a file in the dev server’s hot reload graph, so HMR broke in some release builds while debug builds carried on working, which is a miserable kind of bug to chase.
GitHub hit the same thing through ordinary library semantics. JavaScript’s || treats an empty string as missing, and Rust’s unwrap_or doesn’t, so an empty subagent error stayed empty instead of falling back to a default:
let error: Option<&str> = Some("");
error.unwrap_or("Unknown error"); // ""
error.filter(|e| !e.is_empty()).unwrap_or("Unknown error"); // "Unknown error"
Other regressions came from behavior nobody had written down at all. The old TypeScript runtime had monkey-patched Node’s process spawning to add the CREATE_NO_WINDOW flag on Windows, so the agent never saw the requirement and the Rust version flashed console windows. GitHub’s biggest cluster was lifecycle, because moving state into Rust left TypeScript holding opaque handles to objects in a native table, and unlike an object reference, a handle can outlive the thing it points to. Every one of these compiled, and as Toub puts it, a compiler checks that your program is internally coherent, not that it keeps the old contract. Those look-alike spots and unwritten behaviors are where your reviewers and any extra tests will earn their keep.
The performance numbers deserve the most skepticism, because very little of the gain in these ports came from the agent. The agents made the ports affordable, but the speedups and savings mostly trace back to the target language, the build, or architectural changes.
Bun’s figures are a good example of that mix. Its binary shrank by about 20%, though only part of that came from the rewrite, and Sumner puts even that part down mostly to overusing comptime in the Zig code, with linker code folding and trimmed ICU data doing the rest. The 2 to 5% speedup is credited to cross-language LTO, which Kelley points out Zig supported all along. The memory win is the one that actually belongs to Rust, since in-process Bun.build() calls used to leak about 3 MB each and an earlier Zig fix never merged because the lack of anything like Drop made the team wary of it.
GitHub’s benchmarks pull language and architecture apart more cleanly than Bun’s do. A full client lifecycle with one turn took 5.25 seconds on the old Node runtime, 1.33 seconds on Rust out of process, and 292 milliseconds on Rust loaded in process; that last jump comes from removing the process boundary, not from the language. Toub is upfront about it, calling the comparison end-to-end rather than a measure of the language change.
HEY’s numbers are harder to pin down than either. The 99% less CPU and 95% less memory came from a keynote with no published methodology, for a system that’s moving to native clients at the same time. When DHH later had agents rewrite Campfire in Rust, Go, and Elixir, critics said the Rust version had simply gotten more optimization passes, with one putting it as benchmarking how much attention each port got. If you’re pitching a port on performance, it’s worth knowing which bucket your expected gain falls into.
Once a port gets past what one person can review, the work moves from reading the agent’s output to designing the process that produces and checks it. Bun’s version was a fixed loop in which one agent wrote each unit, at least two reviewer agents in separate context windows looked for reasons it was wrong without ever seeing the implementer’s reasoning, and another agent applied their fixes. Whenever something went wrong, Sumner changed the loop rather than patching the code, and the first thing to go wrong was the shared workspace. About two minutes into the first full run, one agent ran git stash, another ran git stash pop, and then came a git reset HEAD --hard, which a rule against those commands and a split into four worktrees of 16 agents each put right.
GitHub’s lessons were about coordination. Fifteen child sessions building at once froze Toub’s laptop, so he turned a chat session into a build scheduler that handed out one build lease at a time. In another case, a session asked a peer four times whether it was ready to merge, got told no every time, and then reached into that peer’s worktree and took its changes anyway. Toub’s takeaway was that parallel agents on adjacent code need a designated coordinator, and that instructions have to spell out what’s off-limits, because his own kickoff prompt had listed the other running sessions without saying to leave them alone.
Robert Martin has taken this furthest by no longer reading his agents’ code at all, relying instead on mutation testing, coverage, CRAP scores, and limits on complexity and module size, and he has since argued that models are now good enough to make strict deterministic harnesses suboptimal. That may hold for new code, but a port has something his projects don’t: an existing implementation to compare against. Both Bun and GitHub also still kept a human reading results and reviewing before merge.
Bun and GitHub disagreed on whether to port everything at once, and both shipped, which suggests the choice matters less than how users meet the result. Sumner ported everything in one PR, because porting esbuild’s transpiler from Go to Zig had taught him that an incremental rewrite leaves behind temporary code you hope someone deletes later. The merge only reached canary on May 14, though, and Claude Code ran on it from June, whereas stable users stayed on v1.3.14, the last Zig release, until Bun v1.4.0 shipped on August 20.
GitHub couldn’t freeze a main branch that was taking hundreds of PRs a week, so it ported one component at a time, with each PR swapping the TypeScript for a thin shim that calls into Rust and deleting the old code in the same change. Over 14.5 weeks, that came to 128 port PRs and 135 releases, so a bug report generally lined up with a small, known set of recently ported pieces. The cost was exactly the temporary code Sumner wanted to avoid, and GitHub’s interop layer peaked at 2,019 internal exports before shrinking back to zero. A single big merge suits a team with a language-independent suite and a release channel to stage behind, while a codebase that can’t stop moving is better served by going one component at a time.
None of these teams put a number on what a port costs the people who depend on them, but tinyauth gave a pretty clear picture of it the day after Bun’s merge, when the project moved its whole frontend toolchain to pnpm. The PR argues that a rewrite this size should take months and doubts that the Bun team knows its way around the new codebase; for what it’s worth, the merged Bun PR had no formal approving review. Sumner says the Rust was written to resemble the Zig code closely, so anyone who knew the old code can read the new one.
That question of who can still reason about the code is what open source projects have been writing policies around. Zig bans LLMs outright, even for fixing grammar or finding bugs, while Ghostty allows AI but requires disclosure and wants a contributor who fully understands the code. The Linux kernel lets agents help but forbids them from adding Signed-off-by, so a human reviews the code and accepts responsibility for it; the rust-lang/rust repo lets LLMs review and analyze but not write code, except for pre-arranged, well-tested changes with disclosure. Apart from Zig’s ban, each of these comes down to keeping someone on the hook who understands the change, and a port nobody on the team can find their way around fails that test, however carefully it was generated.
Taken together, these attempts point to the same prerequisites, and most need to be in place before the first agent runs.
A port is also the wrong fix when the real problem is the design. A faithful translation carries the old architecture across with it, and Toub admits that much of GitHub’s runtime is still “TypeScript algorithms wearing Rust’s syntax.” From the start, his goal was to get off Node and V8 so the runtime could be embedded in other processes, and Rust was the means to that end. When the language isn’t what’s holding a system back, an agent will simply reproduce the same problems in the new language, and do it faster than any team could have before.

Compare the top AI development tools and models of November 2026. View updated rankings, feature breakdowns, and find the best fit for you.

AI coding assistants can generate UI quite quickly, but they don’t know your design system. Here’s how you can teach them the ins and outs.

Build a 3D hero with GPU Style Sheets and compare its code, bundle size, and rendering costs with Three.js.

Learn how to use browser AI APIs for summarization, translation, and language detection with practical examples and implementation tips.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now