Large rewrites tempt teams with a clean destination and hide the cost of getting there. While a replacement grows on a long-lived branch, the production system keeps changing. The new implementation drifts, reviewers lose the ability to reason about the whole diff, and the final cutover becomes a test of everything at once.
Coding agents change the economics of mechanical translation, repository exploration, and repeated validation. They do not remove the migration problem. A fast agent can reproduce an implicit assumption just as quickly as it can reproduce a function, and a compiler can accept a program that preserves none of the behavior users depend on.
GitHub's September 2026 engineering report, Migrating the GitHub Copilot runtime to Rust, using Copilot, is a useful current case study. GitHub reports that agents wrote most of more than 800,000 lines of production Rust across 128 pull requests. The work landed incrementally while the original runtime continued to evolve. GitHub also reports dozens of known regressions and emphasizes that compilation did not prove the port was correct.
The durable lesson is not "rewrite in Rust" or "let agents write the code." It is to structure a migration so every slice can prove what it preserves.
Start With a Migration Contract
Before changing implementation language, framework, storage engine, or architecture, write down the behavior that must survive. The migration contract should include:
- public APIs, events, files, schemas, and command-line behavior
- ordering, retry, timeout, cancellation, and lifecycle semantics
- authentication and authorization decisions
- platform-specific behavior and supported environments
- latency, memory, throughput, startup, and resource goals
- observability signals and production rollback conditions
- behavior that is intentionally allowed to change
Separate preservation from improvement. If a slice changes language, data flow, API shape, and user behavior at the same time, a failing test cannot tell you which decision caused the regression. GitHub describes its port as deliberately behavior-preserving and resisted opportunistic redesign during translation. Optimization followed from the new runtime architecture, but it was measured against workload-specific baselines rather than assumed from the target language.
Turn each migration requirement into an observable claim. "Preserve session behavior" is too broad. "A resumed 32-turn session emits the same ordered event types, restores the same durable state, and returns the same error class for an expired credential" can be tested.
Prefer In-Place Slices Over a Big-Bang Cutover
An in-place migration replaces the system one bounded component at a time while the production branch remains shippable. Each slice should:
- name one old component and its callers
- expose or reuse a narrow compatibility seam
- implement the replacement behind that seam
- run existing behavior checks against the replacement
- switch production use in the same reviewed change
- delete the superseded implementation when rollback no longer requires it
GitHub used atomic component replacement: each pull request replaced one TypeScript slice with a thin call into Rust and removed the old production implementation. Existing end-to-end tests then exercised the new code in place. That approach kept changes smaller, reduced drift from concurrent work, and preserved a shippable main branch.
Do not force an oversized component through the same opening. First extract stable subcomponents or contracts without changing behavior. The extraction is one reviewable change; each later port is another. A 30,000-line hub that touches state, events, tools, persistence, and entrypoints is not one migration slice simply because it lives in one file.
Treat the Compatibility Seam as a Product Surface
A compatibility seam is the temporary or permanent boundary that lets old and new components coexist. It may be an interface, protocol, adapter, file format, command boundary, or foreign-function interface.
For every seam, record:
| Question | Evidence |
|---|---|
| What crosses it? | Typed request, response, event, and error inventory |
| What is implicit? | Ordering, numeric representation, nullability, lifecycle, and platform rules |
| Who owns versioning? | Named owner and compatibility policy |
| How is parity tested? | Contract fixtures, replay corpus, differential checks, or end-to-end tests |
| How is failure contained? | Timeout, retry, cancellation, rollback, and observability behavior |
| When can it disappear? | Exit criteria and deletion owner |
GitHub retained one bidirectional JSON-RPC contract across out-of-process and in-process transports. That preserved the SDK-facing dispatch system while changing the hosting boundary. The choice carried measurable serialization cost, but avoided creating a second per-method binding surface across six SDK languages. That is a tradeoff, not a universal prescription: preserve a seam when it reduces migration risk more than it constrains the target architecture.
Build Migration Oracles Before Delegating
A migration oracle is the evidence used to decide whether the new implementation matches the old one. Tests are one oracle, but not every test is trustworthy and not every contract is encoded in tests.
Use several layers:
- Characterization fixtures: capture current inputs, outputs, errors, events, and side effects before translating.
- Contract tests: exercise the compatibility seam from every supported caller.
- Differential tests: run old and new implementations on the same corpus and compare normalized results.
- End-to-end tests: verify the new slice inside the delivered system, not only as an isolated module.
- Static checks: catch missing names, fields, signatures, and types quickly.
- Performance baselines: isolate the part being changed and measure representative workloads.
- Production signals: watch error classes, latency distributions, resource use, and user-visible regressions after rollout.
GitHub's report found that static analysis caught large amounts of ordinary wiring trouble, while the known correctness regressions clustered around changed behavioral contracts, state and lifetime behavior, incomplete migration, host boundaries, and incorrect test oracles. That distinction matters: a clean compile is strong evidence about internal consistency and weak evidence about semantic equivalence.
Freeze or version the oracle with the slice. If an agent edits a failing test until the translation passes, require an explicit explanation of why the old expectation was wrong. Deleting or weakening an end-to-end check during a preservation migration should block the change unless the migration contract authorizes it.
Give Agents Evidence-Bounded Work Packets
Agents work best on migration slices that have a complete local contract. A packet should identify:
- source component and target location
- allowed files and prohibited neighboring changes
- compatibility seam and behavioral invariants
- source and target tests that must remain unchanged
- commands for fast static and focused runtime feedback
- reference implementation or fixtures
- completion evidence and stop conditions
Ask the agent to investigate before translating. GitHub's session data showed much more file reading and repository search than editing, and the hardest central component began with nearly an hour of exploration. For a migration, that is healthy behavior: the agent should map callers, state ownership, event order, cleanup paths, and platform conditions before it proposes a patch.
Parallelize only after the seams are stable. Independent leaves can move concurrently when they own separate files and contracts. Shared protocol definitions, generated bindings, central state machines, and migration-oracle fixtures need one integration owner. A fleet of isolated branches prevents file collisions; it does not prevent two agents from encoding incompatible assumptions.
Review for Parity, Not Plausibility
Migration review asks a narrower question than ordinary feature review: does the new slice preserve the declared contract?
Use a line-by-line or behavior-by-behavior comparison where practical. Reviewers should look for:
- omitted branches, guards, cleanup, and platform flags
- changed numeric, date, null, or serialization behavior
- different event ordering or retry timing
- state moved to the wrong lifetime or ownership boundary
- old callers still bypassing the new seam
- tests copied without proving that their oracle is correct
- new behavior hidden inside a supposedly mechanical port
- obsolete implementation left active after the switch
Run at least one independent review that did not author the slice. An agent can provide a second reading, but its findings still need validation against code and executable evidence. Re-review after rebases and conflict resolution because a clean earlier review does not cover logic silently lost while integrating newer production changes.
Release Slices With Explicit Stop Conditions
Each slice should be small enough to deploy and observe. Define:
- the rollout unit and exposure percentage
- dashboards, logs, traces, and comparison queries
- acceptable error and performance deltas
- a rollback or kill-switch path
- the owner watching the release window
- the time or traffic threshold required before the next slice
Measure the workload the migration is meant to improve. GitHub used deterministic local completions to remove model and network latency from runtime benchmarks, then reported end-to-end client and session scenarios. It explicitly warned that the results were workload-specific and included other changes. Adopt that discipline: state what a benchmark isolates, what changed concurrently, and what it cannot prove.
Migration Checklist
Before implementation:
- Preservation and intentional-change contracts are separate.
- The component boundary and callers are mapped.
- Compatibility semantics include errors, ordering, lifecycle, and platforms.
- At least two independent migration oracles exist.
- Performance claims have a representative baseline.
Before merging a slice:
- The replacement is narrow and reviewable.
- Existing end-to-end checks run against the new path.
- Changed tests have an approved reason.
- Independent parity review found no unexplained drift.
- Rebase and conflict resolution received a fresh comparison.
Before continuing the migration:
- The deployed slice met its observation threshold.
- Regressions are classified by contract, state, omission, boundary, or oracle failure.
- Rollback remains usable until confidence is sufficient.
- Superseded code and temporary seams have named deletion criteria.
Agents can make large migrations economically possible, but speed is useful only when every increment remains understandable. Preserve one contract at a time, keep the production branch shippable, and make the evidence for parity stronger than the agent's confidence.
