Agentic Refactoring: Large-Scale Codebase Migrations
Run large-scale codebase migrations with agentic development workflows: break the work into verifiable chunks, use codemods as safety nets, and ship confidently.

Large codebase migrations fail for a predictable reason: teams treat them as one enormous task and reach for a single heroic prompt that's supposed to fix everything at once. The agent produces thousands of lines of changes, the test suite turns red across a hundred files, and nobody can tell which edit caused which failure. Two weeks later the branch is abandoned or force-merged in desperation.
The fix isn't a smarter model. It's a better decomposition strategy. Agentic development workflows shine when each chunk of work is small enough to verify before the next chunk begins. Codemods provide the deterministic safety net, and the agent handles the judgment calls that codemods can't reach. That combination makes large-scale refactors tractable without turning your repo into a long-lived conflict zone.
At Laxaar we've run this process across React Router v5-to-v6 migrations, Jest-to-Vitest transitions, and multi-year Django monolith splits. The pattern is consistent enough to document, and it keeps the repo in a green state throughout.
What you'll learn
- Why large migrations break the standard agentic workflow
- Decomposing a migration into agent-sized chunks
- Codemods as the mechanical safety net
- The verification loop that keeps the repo green
- When to let the agent handle judgment calls
- Tooling: what helps and what gets in the way
- A worked example: migrating an Express API to a typed contract layer
- Frequently Asked Questions
Why large migrations break the standard agentic workflow
A standard agent workflow assumes you can write a task, the agent runs it, you verify the output, and you move on. That loop works well for changes scoped to a few files. It breaks when a migration touches hundreds of files because:
- Context window pressure. The agent can't hold all affected files in context simultaneously. It fills in the gaps with guesses, and those guesses are often wrong in subtle ways that tests don't catch immediately.
- Error amplification. One wrong assumption made in file 10 gets copied faithfully into files 11 through 180. By the time you notice, the mistake is structural.
- Untestable intermediate states. A half-migrated codebase is neither the old system nor the new one. It won't compile, or it compiles but behaves incorrectly, and that makes it impossible to gate each step on a passing test suite.
The opinionated take here is blunt: never give a coding agent a migration task defined as an outcome ("migrate all routes to v6") without first breaking it into steps defined as verifiable states ("after this step, npm test -- --testPathPattern=routes/auth passes"). The agent's job is to reach the next verifiable state, not to complete the entire migration.
Decomposing a migration into agent-sized chunks
Decomposition is the hardest part of the whole process and the part that can't be automated. A human engineer has to look at the migration's scope and define a sequence of states where the codebase is valid at each step.
A practical approach:
-
Map the call graph. Before writing a single prompt, generate a dependency graph of the modules you're migrating. Tools like
madgefor JavaScript orpydepsfor Python give you this quickly. Migration chunks should follow leaves-to-roots ordering: change code with no dependents first. -
Identify the mechanical vs. the judgment calls. Anything that can be expressed as a find-and-replace rule belongs to a codemod. Anything that requires reading context to decide between options goes to the agent. Keep these separate.
-
Write acceptance criteria per chunk. Each chunk should have exactly one test command that proves it's done. No test command, no chunk.
-
Set a file-count limit. A practical ceiling is 15-20 files per agent task. Above that, re-split the chunk.
# Example: listing migration chunks for a React Router v5 → v6 migration
Chunk 1: Leaf route components (no nested routes)
Files: 22 leaf components in src/pages/
Test: npm test -- --testPathPattern=src/pages
Type: Mostly codemod, agent handles prop signature changes
Chunk 2: Layout wrappers that use <Switch>
Files: 4 layout files
Test: npm run e2e -- --spec=navigation
Type: Agent (Switch → Routes is not purely mechanical)
Chunk 3: Auth guards and redirect logic
Files: 3 files
Test: npm test -- --testPathPattern=auth
Type: Agent (Redirect component removal requires logic rewrites)
Codemods as the mechanical safety net
A codemod is a program that transforms code according to explicit rules rather than heuristics. Tools like jscodeshift, ast-grep, and codemod operate on the AST rather than on text, which means they don't accidentally change a variable named Switch in a comment while changing the JSX element Switch in markup.
Codemods handle the high-confidence, low-ambiguity changes: import renames, prop name changes, removed lifecycle methods. They run in milliseconds, produce diffs that are easy to review, and can be re-run if something changes upstream.
// jscodeshift codemod: rename withRouter HOC import to useNavigate hook pattern
// This handles the mechanical part of the migration; the agent handles the body rewrite
module.exports = function transformer(fileInfo, api) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// Remove withRouter import
root.find(j.ImportDeclaration, { source: { value: 'react-router-dom' } })
.find(j.ImportSpecifier, { imported: { name: 'withRouter' } })
.remove();
// Add useNavigate import if not present
const routerImports = root.find(j.ImportDeclaration, {
source: { value: 'react-router-dom' }
});
if (routerImports.length > 0) {
const specifiers = routerImports.get().node.specifiers;
const hasUseNavigate = specifiers.some(
s => s.imported && s.imported.name === 'useNavigate'
);
if (!hasUseNavigate) {
specifiers.push(j.importSpecifier(j.identifier('useNavigate')));
}
}
return root.toSource();
};
The real trade-off with codemods: writing a reliable codemod takes longer than writing a prompt. For a one-time migration, you might choose to invest in the codemod anyway because the safety is worth it. For a team running migrations regularly, a library of internal codemods becomes a genuine asset.
The verification loop that keeps the repo green
The verification loop is what separates an agentic migration from an agentic disaster. The rule is simple: the agent must not begin the next chunk until the current chunk passes its acceptance test.
Here's the loop in practice:
- Run the codemod for the current chunk.
- Commit the codemod output as a separate commit (message:
chore: codemod chunk-N mechanical changes). - Hand the remaining judgment-call files to the agent with the chunk's acceptance test as the stop condition.
- Run the acceptance test. If it passes, commit. If it fails, the agent gets the failure output and iterates.
- Run the full test suite once per chunk, not just the scoped test, to catch regressions.
Step 2 matters more than it looks. Separating codemod commits from agent commits makes review faster: reviewers can approve the codemod diff with a glance and focus their attention on the agent-produced changes where judgment was exercised.
One thing we've learned at Laxaar: don't skip the full test suite run at step 5, even when it feels slow. A leaf-level change that looked isolated will occasionally have a side effect three layers up. Catching it at chunk 5 is much cheaper than at chunk 18.
When to let the agent handle judgment calls
Some changes can't be mechanised. Examples from real migrations:
- A component that uses
history.push()in five different ways, some conditional, some in event handlers, some in effects. Each instance requires reading the surrounding logic to choose the rightuseNavigatepattern. - A Django view that mixes business logic with serialisation. Splitting it requires understanding what the function was actually supposed to do.
- An API route where the old framework's error handling was implicit and the new one is explicit. The agent needs to infer what errors should propagate and what should be swallowed.
For these tasks, give the agent narrow context: the specific file, the chunk's acceptance test, and a description of the old pattern and the new pattern. Don't give it the whole migration spec. The agent performs better with a constrained, answerable question than with an open-ended goal.
Prompt pattern for judgment-call tasks:
"You're migrating src/auth/ProtectedRoute.jsx from React Router v5 to v6.
Old pattern: uses <Redirect to="/login" /> when unauthenticated.
New pattern: uses <Navigate to="/login" replace /> inside a return statement.
The file also uses useHistory for a post-login redirect; replace that with useNavigate.
Stop condition: `npm test -- --testPathPattern=auth/ProtectedRoute` passes.
Do not change any other files. If you see an issue in an adjacent file, note it but don't edit it."
The last two sentences are not optional. Without them, agents helpfully fix adjacent issues and expand the scope of the task in ways that break the verification loop.
Tooling: what helps and what gets in the way
Not all tooling plays well with an agentic migration workflow.
| Tool / Practice | Helps | Gets in the Way |
|---|---|---|
jscodeshift codemods | High-confidence mechanical changes | Requires AST knowledge to write |
ast-grep rules | Fast pattern search and replace | Limited to structural patterns |
| TypeScript strict mode | Surfaces migration errors immediately | Can produce hundreds of errors in intermediate states |
Incremental TypeScript (tsc --incremental) | Faster type checks per chunk | Cache can mask errors across chunks |
| Monorepo isolation | Run tests per package | Cross-package changes need coordination |
| Feature flags | Keep old and new code paths live | Doubles maintenance burden if not cleaned up promptly |
| Large test suites | Catch regressions reliably | Slow feedback loop slows the agent iteration cycle |
One pattern that consistently helps: keep a scratchpad file in the repo root during the migration. Each agent task appends its chunk summary, any deferred issues it noticed, and the acceptance test result. This becomes the migration log and prevents repeated agent context-setting on every new chunk.
A worked example: migrating an Express API to a typed contract layer
Here's a condensed version of a migration we ran: moving an Express REST API from untyped route handlers to a typed contract layer using zod schemas and a thin adapter.
Scope: 47 route handlers, ~8,000 lines of TypeScript.
- Chunk 1 (codemod): Add
zodimports and scaffold empty schema files per route module (11 files, zero judgment, 100% codemod). - Chunk 2 (agent): Write
zodinput schemas for the 12 GET routes based on existing JSDoc and test fixtures. - Chunk 3 (agent): Write schemas for the 18 POST/PUT routes, which required reading business logic to determine which fields were truly required.
- Chunk 4 (agent): Replace
req.bodyandreq.queryaccess withschema.parse()calls and wire error handling. - Chunk 5 (codemod + agent): Remove the old manual validation helpers, now redundant.
Each chunk had a passing integration test as its exit condition. The migration took four working days, the repo was green at every commit, and the final diff was reviewable because each commit told a clear story.
The approach works because agentic development workflows are genuinely good at reading code and writing code, but they need humans to define what "done" means at each intermediate step. That's a design task, not a coding task, and it's where engineering judgment still matters most. Teams working with Laxaar on custom software projects get this decomposition work done as part of the engagement, not as an afterthought.
If you're planning a framework migration or a large-scale refactor, our AI-powered development services and agentic coding capabilities are set up specifically for this kind of structured, verifiable work. You can also browse our portfolio to see how we've approached similar migrations in production codebases.
Ready to scope your migration? Talk to the Laxaar team and we'll map out a chunk plan before any code gets touched.
Frequently Asked Questions
How is agentic refactoring different from just running a coding agent on a large task?
Agentic refactoring is a structured workflow where each chunk of migration work has a defined acceptance test before the next chunk starts. A raw large task hands the agent an open-ended goal and hopes for the best. The structured approach keeps the repo in a valid state at every commit and makes failures local and diagnosable rather than global and confusing.
Do you need codemods for every migration?
No. Codemods are worth writing when a mechanical transformation applies to more than roughly 20 files. Below that, a careful agent prompt with a narrow scope is faster. The threshold shifts if your team already has codemod infrastructure, since the marginal cost of writing another transform is lower.
What's the biggest mistake teams make with agentic migrations?
Skipping the decomposition step and giving the agent the whole migration scope at once. The second biggest is not separating codemod commits from agent commits, which makes code review nearly impossible because reviewers can't tell which changes were mechanical and which required judgment.
How do you handle TypeScript errors in intermediate migration states?
Two approaches work. The first is to use // @ts-expect-error annotations on lines that are temporarily invalid and remove them as each chunk completes. The second is to run tsc only on the files changed in each chunk rather than the whole project. Both avoid the demoralising wall of type errors that comes from running strict TypeScript across a half-migrated codebase.
Can this workflow apply to database schema migrations, not just code migrations?
Yes, with adjustments. Database migrations have the added constraint that intermediate schema states must be backward-compatible with the running application. The chunk boundaries become migration scripts rather than file sets, and the acceptance tests become integration tests against a real database. The principle, verify before proceeding, is identical.
Working on something like this?
Get a fixed scope, timeline, and price within one business day — no obligation.


