When people hear that Bun used dozens of AI agents to migrate from Zig to Rust, the easy takeaway is: “AI can now rewrite huge codebases.”
That is the wrong lesson.
The more useful lesson is architectural: the migration was designed like a distributed system for code change.
The agents were not asked to understand all of Bun and perform a heroic rewrite. They were used as bounded workers inside a workflow with clear inputs, clear outputs, and external verification.
That distinction matters for anyone trying to use agents on real production code.
This post is based on Bun’s published write-up, Rewriting Bun in Rust, and the public migration pull request artifacts, including the .claude/workflows files added during the rewrite.
The Mental Model: Agent Swarm As Build Farm
The best mental model is not a room full of senior engineers debating architecture.
It is a build farm.
You do not run many CI workers because each worker is smarter than one machine. You run them because the work can be split into independent jobs. Test file A can run separately from test file B. Linux can run separately from macOS. One shard can fail without invalidating the whole idea of sharding.
The Bun migration used a similar pattern for code change.
Instead of giving one long-running agent a vague instruction like “rewrite Bun in Rust,” the work was decomposed into many small tasks:
- Port one Zig file to one Rust file.
- Classify pointer lifetimes in one source file.
- Review one generated Rust diff against the original Zig.
- Fix one compiler-error group.
- Analyze one dependency-cycle edge.
- Investigate one failing test or CI failure cluster.
That is why using many agents made sense. The problem had a lot of parallel mechanical work. The agents provided throughput, not magic.
The Real Unit Of Work Was A Bounded Ticket
The important design choice was task shape.
A bad task looks like this:
Rewrite the bundler in Rust.
That task has unclear boundaries, unclear context needs, and unclear correctness criteria. It invites the agent to explore too much, edit too much, and invent too much.
A better task looks like this:
Read PORTING.md.
Read the LIFETIMES.tsv rows for src/foo/bar.zig.
Read src/foo/bar.zig.
Write the Rust draft to the deterministic path src/foo/bar.rs.
Do not read unrelated Zig files.
Do not run builds.
Do not use Git.
Do not touch any other file.
That task is boring. It is also much safer.
The agent does not need to understand the whole runtime. It needs to perform one local transformation under explicit rules.
This is the pattern worth copying: agent work should be shaped like narrow engineering tickets, not open-ended ownership.
The Workflows Were Code, Not Just Prompts
One detail that makes the Bun migration especially useful to study is that the public pull request includes .claude/workflows/*.workflow.js files.
That matters because it shows the agent system was not only a collection of chat prompts. It was closer to an executable job graph for code change.
The workflow names tell the story:
lifetime-classify.workflow.jsclassified Zig pointer fields into Rust ownership and lifetime categories.phase-a-port.workflow.jshandled the first mechanical file-by-file port.phase-b0-cyclebreak.workflow.jsclassified crate dependency back-edges.phase-b0-moveout.workflow.js,phase-b0-movein.workflow.js, andphase-b0-verify.workflow.jshandled cycle-breaking edits and verification.- Later workflows handled build queues, test swarms, CI tasks, deduplication, unsafe audits, and cleanup.
That naming is useful because it shows the migration was not organized around one giant prompt. It was organized around phases with different contracts.
Some workflows were read-heavy analysis passes. For example, lifetime classification asked agents to inspect one source file, find pointer fields, classify ownership, and emit rows into a shared table.
Other workflows were write-heavy transformation passes. The porting workflow wrote Rust files. The cycle-breaking workflows edited crate boundaries.
Later workflows treated compiler errors, test failures, and CI failures as queues.
Each workflow encoded more than an instruction. It encoded the shape of the job:
- what list of tasks to consume
- what files an agent could read
- what path an agent could write
- what actions were forbidden
- what structured output schema the agent had to return
- what phase came next
The most revealing example is phase-a-port.workflow.js. Its shape was simple:
Implement -> Verify -> Fix
We do not need to know the exact Claude Code command that invoked it to understand how it caused changes. The file declares its expected input near the top: a batch of { zig, loc } items and a repository path.
// args: { files: Array<{zig: string, loc: number}>, repo: string }
const REPO = (args && args.repo) || "/root/bun-5";
In other words, the workflow runner could invoke it with a list like “port these 50 Zig files from this checkout.”
Once invoked, the script maps each .zig path to a deterministic .rs path, then runs a pipeline over the batch.
// Deterministic rs path (PORTING.md ground rules) — do NOT let agents pick.
function rsPathFor(zig) {
That comment is the key point: the workflow owns the output path.
const FILES = ((args && args.files) || []).map(f => ({ ...f, rs: rsPathFor(f.zig) }));
For every file in that input list, the workflow launches an implementer agent, feeds the implementer’s result into a verifier agent, and then feeds verifier findings into a fixer agent. The workflow is the coordinator; the agents are the workers.
For each file, the implementer received a bounded task: read PORTING.md, read the matching LIFETIMES.tsv rows, read one .zig file, and write the Rust draft to a deterministic .rs path. The workflow explicitly told agents not to read unrelated Zig files, not to run builds, not to use Git, and not to touch unrelated paths.
1. Read ${GUIDE} — the porting guide. Read the WHOLE file. Every rule is load-bearing.
4. Write the .rs to EXACTLY this path: ${REPO}/${f.rs}
And the guardrails were equally explicit:
Do NOT read other .zig files "for context" — PORTING.md's crate/type maps are authoritative for cross-file refs. Do NOT run builds. Do NOT git anything.
That deterministic path is a small but important detail. If the agent is allowed to choose where output belongs, it has to reason about project structure and can accidentally fork the architecture. If the workflow decides the path, the agent only performs the local transformation.
The verifier then read the porting guide, the original Zig source, and the generated Rust file. Its job was adversarial: find deviations from the guide and from the source behavior. The fixer then applied only those findings.
agent(implementPrompt(f), { phase: "Implement", schema: IMPL_SCHEMA })
agent(verifyPrompt(f, impl), { phase: "Verify", schema: VERIFY_SCHEMA })
The structured output also matters. A workflow can only compose many agents if their outputs are machine-readable enough to route. A vague paragraph is hard to aggregate. A result with fields like status, issues, confidence, or remaining failures can feed the next phase.
That is the important design pattern. The workflow file defined the agent’s boundary, input files, output path, forbidden actions, success schema, and review loop. The intelligence was not only in the model. It was in the job design around the model.
Parallel, But Phased
The workflow was not “spawn many agents and hope.”
It was parallel inside phases and sequential between phases.
Many implementers run in parallel
|
v
Reviewers inspect outputs
|
v
Fixers apply narrow corrections
|
v
Compiler and tests verify reality
This separation matters.
An implementation agent has a natural bias toward making its own output work. A reviewer should have a different job: assume the output is wrong and find the bug. A fixer should have an even narrower job: apply the review findings without rewriting the whole file.
That mirrors how strong engineering teams already work. The author, reviewer, and build system are separate roles. The migration applied that same separation to agentic work.
Why The Agents Did Not Constantly Step On Each Other
Once you run many coding agents, concurrency becomes the first serious problem.
The failure modes are obvious:
- Two agents edit the same file.
- One agent deletes another agent’s work.
- One agent runs a broad Git command.
- One agent applies stale changes over newer changes.
- One agent makes the compiler green by stubbing real behavior.
The Bun write-up describes this class of problem directly: agents used commands like git stash, git stash pop, and git reset --hard. In a single-developer workflow those commands can be useful. In an agent swarm, they are dangerous shared-state mutations.
The response was not to trust the agents more. It was to constrain them more.
The controls were simple and practical:
- Use multiple Git worktrees.
- Give each agent exact file ownership.
- Ban broad Git commands.
- Prevent agents from touching unrelated paths.
- Separate implementation, review, and fix phases.
- Serialize risky apply operations with a lock.
That last point is important. The public pull request included an apply lock using flock, which means analysis and review can happen in parallel while mutation is serialized when needed.
This is distributed systems thinking applied to code editing. Shared state needs coordination. Git is shared state. The filesystem is shared state. Build artifacts are shared state. Treat them that way.
Context Was Managed By Moving Knowledge Out Of The Agent
Large-agent workflows often fail because every agent is given too much context.
The Bun migration used a better pattern: put shared knowledge in durable files, then give each worker only the relevant slice.
Reported durable context included:
PORTING.mdfor Zig-to-Rust translation rules.LIFETIMES.tsvfor ownership and lifetime classifications.- compiler logs as work queues.
- dependency-cycle reports as refactoring queues.
- CI failure lists as debugging queues.
This reduces context pressure in two ways.
First, each agent reads only what it needs. A file-porting agent does not need the whole repository. It needs the porting guide, one source file, and any precomputed lifetime rows for that file.
Second, the workflow gets repeatability. If an agent fails, another agent can pick up the same file and the same rules. The source of truth is not hidden inside a chat transcript.
This is one of the most reusable lessons from the migration:
Global reasoning becomes durable artifacts.
Durable artifacts become small local tasks.
Small local tasks become parallel agent work.
Short-Running Agents Beat Long-Running Agents
The agents in this style of workflow are best understood as disposable workers.
They read bounded input, perform one transformation or review, write bounded output, and exit.
That design avoids several common failure modes:
- Context bloat.
- Forgotten constraints.
- Task drift.
- Broad edits.
- Hard-to-review output.
- Unclear retry semantics.
The long-running source of truth was not an agent. It was the compiler, the test suite, CI, fuzzing, and human supervision.
Agents proposed changes. Tooling judged them.
That is the right relationship.
The Compiler Became A Work Queue
After a mechanical port, the Rust compiler did what compilers do best: it produced a large, specific list of things that were wrong.
Instead of treating that as one giant failure, the team could treat compiler errors as a queue.
The loop looked roughly like this:
Run cargo check
Group errors by crate or file
Assign a bounded group to an agent
Review the proposed fix
Apply the fix
Run the compiler again
This is where Rust was especially useful. The compiler did not prove the program was correct, but it gave the workflow a precise stream of machine-checkable feedback.
For agentic migration, that matters more than people realize. Agents are much more useful when the environment can tell them exactly what broke.
Bun’s JavaScript And TypeScript Tests Were The Migration Oracle
One of the most important details is that Bun’s test suite was written in JavaScript and TypeScript.
That made the tests implementation-independent.
The implementation language changed from Zig to Rust, but the public behavior under test stayed the same: runtime behavior, CLI behavior, bundling behavior, Node compatibility, HTTP behavior, and other user-visible surfaces.
If the tests had been tightly coupled to Zig internals, the rewrite would have invalidated the test harness at the same time as the implementation. That would remove the oracle exactly when it was needed most.
Instead, the existing tests could run against the new Rust-built Bun binary.
That gave the team a practical answer to the most important question:
Does this new implementation still behave like Bun?
The tests also caught semantic drift that the compiler could not catch.
One example from the migration was a side effect inside an assertion. Zig’s assert evaluates its argument. Rust’s debug_assert! removes the expression in release builds. Code that looked equivalent was not equivalent, because the side effect disappeared.
The fix is conceptually simple: perform the side effect outside the assertion, then assert on the result.
let result = dev.client_graph.insert_stale(&rfr.import_source, false)?;
debug_assert!(result == react_refresh_index);
That kind of bug is exactly why external behavior tests matter. The compiler can validate Rust. It cannot guarantee that the Rust preserved every Zig semantic.
Rust Exposed Hidden Architecture Debt
The migration was not only syntax translation.
Zig allowed Bun to operate closer to one large compilation unit. Rust pushed the codebase toward a crate graph. That made dependency boundaries explicit.
Once boundaries became explicit, cycles appeared.
The interesting part is how those cycles were handled. They were not all solved with one generic trick. The workflow classified dependency edges into categories such as:
- Delete dead imports or leftover aliases.
- Move type-only definitions to a lower-level crate.
- Replace a dependency with an opaque pointer when only identity is needed.
- Mark true dependencies as genuine architecture decisions.
This is a good reminder: language migrations surface architectural debt. A new type system or module system does not merely translate old code. It forces old assumptions to become explicit.
The Safety Came From Layers
No single mechanism made the migration safe.
The safety came from layers:
- Mechanical translation reduced design drift.
- Porting rules reduced agent invention.
- File ownership reduced edit conflicts.
- Worktrees reduced workspace collisions.
- Apply locks reduced shared-state races.
- Adversarial review reduced plausible but wrong changes.
- Compiler errors created machine-checkable queues.
- TypeScript tests validated public behavior.
- Cross-platform CI exposed OS and architecture differences.
- Fuzzing and security review continued hardening after functional parity.
This is the engineering lesson. The agents were one part of a larger control system.
The Pattern Engineering Teams Should Copy
Most teams should not begin by asking, “How do we run dozens of agents?”
They should ask better questions:
- Can this migration be made mechanical before it becomes idiomatic?
- Can we write translation rules before generating code?
- Can the work be split into independently checkable units?
- Can each agent have exact read and write boundaries?
- Can shared context live in files instead of prompts?
- Can risky mutation be serialized?
- Can review be separated from implementation?
- Can the compiler, tests, static analysis, or CI act as the truth system?
If the answer is no, more agents may only multiply chaos.
If the answer is yes, agents become useful in a very specific way: they accelerate bounded work inside an engineered workflow.
The reusable pattern looks like this:
Make the migration mechanical first.
Write the rules into durable docs.
Split work into file, crate, error, and test-sized tasks.
Run short-lived workers in parallel.
Constrain filesystem and Git access.
Serialize risky apply steps.
Use adversarial review.
Let compiler, tests, and CI judge output.
Refactor toward idiomatic design after parity.
Continue hardening with fuzzing and security review.
That is less flashy than “AI rewrote Bun.”
It is also much more useful.
Closing Thought
Bun’s Rust migration is a strong case study because it shows both the promise and the limits of agentic engineering.
The agents provided speed. The workflow provided safety. That is the part worth copying.
