Plan-Execute: Running 20+ Agents on a Migration
243 files. 24 agents. One hour instead of three days. The pattern is not complicated, but most people skip the step that makes it work.
Prerequisites
- → Claude Code installed and configured
- → Git worktrees available (git 2.5+)
- → A migration task with at least 20+ files
What you'll set up
- ✓ The two-phase model configured and ready to run
- ✓ A decomposition checklist to know if a task can be parallelized
- ✓ The /batch command with quality gates for your stack
- ✓ A review workflow for the 1-2% that needs human judgment
There’s a JSX to TSX migration sitting on every JavaScript team’s backlog right now. Not because it’s hard, but because doing it sequentially means carving out three days of focus time that never materializes. You do 40 files on a Tuesday, merge, get pulled onto something else, and the branch rots for six weeks. The Plan-Execute pattern doesn’t make the migration easier. It changes the structure of the problem so the time constraint disappears.
The idea is simple: one Opus agent reads the entire codebase and plans, then 20-something Sonnet agents execute in parallel in isolated branches. Sized on a 243-file JSX to TSX migration, the shape is about an hour of wall-clock time for the parallel execution phase, plus 20-30 minutes of planning up front, with a small tail of exceptions, typically 1 to 2% of files. Call it three files needing a human while the other 240 merge cleanly. This guide uses that migration as a worked example throughout, not a benchmark run with logs attached, to show what the shape of the pattern looks like at that scale.
This guide covers how to set it up, why it fails when people skip the decomposition step, and what to do with the exceptions.
Why sequential falls apart at scale
A single agent processing 243 files in one session has a 200K token context window. By the time it reaches file 120, files 1 through 30 have scrolled out of the window. Conventions applied at the start of the session no longer inform decisions at the middle. You’ll see drift: type imports formatted one way in the first batch, slightly differently in the second, inconsistently in the third.
The session also runs long: six hours or more of focused, supervised work to get through the file count, which is where the earlier “three days” comes from once you count the calendar time lost to interruptions and context-switching around that block, not just the hours spent typing. Each file requires reading its dependencies, checking for patterns, writing the output, and verifying it compiles. A human has to stay engaged throughout to catch cases the agent handles wrong, because errors accumulate. A mis-applied convention at file 12 very likely propagates forward. Detection happens late, often after you’ve already committed 80 files built on top of the mistake.
Parallel agents solve both problems. Each agent handles a bounded subset of files with limited responsibility, so context doesn’t bloat. Errors stay isolated to one agent’s branch and don’t cross into another’s. And the quality gate fires before anything merges, so a problem surfaces immediately rather than after the fact.

The two-phase model
Phase 1 runs a single Opus agent in read-only mode. It reads the entire codebase, identifies patterns, finds edge cases, and flags files that will need special treatment. It touches no files. Its only output is a brief.md that covers: the target conventions, per-directory exceptions, known edge cases from the actual code, and the exact quality gate command each agent must pass before its branch is considered ready. This brief is the load-bearing artifact. Everything that follows depends on its quality. It’s context engineering applied to a one-shot task; the durable, always-on version of the same discipline is what the context engineering guide covers.
Phase 2 spawns N Sonnet agents, each receiving the brief and a file subset. Each agent works in its own isolated worktree (a separate working copy with its own branch), applies the migration, and runs the quality gate. If the gate passes, it marks its branch ready. If it doesn’t, it writes an exception report and stops. A coordinator agent collects the ready branches and merges them. Exceptions get routed to a human reviewer with the exact context of what blocked each one.
Phase 1 (Opus)
Read codebase → identify patterns → identify edge cases
Output: brief.md
↓
Phase 2 (Sonnet × N)
Agent 01: files 1-10 → quality gate → branch ready ──┐
Agent 02: files 11-20 → quality gate → branch ready ──┤
Agent 03: files 21-30 → quality gate ✗ → exception ─┤
... ↓
Agent 24: files 231-243 → quality gate → branch ready Coordinator merges
ready branches
Exception queue → human

The key constraint on Phase 1: Opus does not write code. If it touches files, two things go wrong. The read-only invariant breaks, which means Phase 2 agents might encounter modified files that differ from what the brief describes. More subtly, Opus starts making micro-decisions that should be explicit conventions in the brief, and Phase 2 agents receive an incomplete specification. The brief must be complete enough that any Sonnet agent can execute correctly without seeing what any other agent is doing.
Decomposition: the step everyone skips
This is where 80% of Plan-Execute failures originate. The pattern looks simple, so people move directly from “I have 243 files” to spawning agents. Two hours later they have 243 branches with inconsistent outputs and no clean way to merge any of them.
The rule is this: if you can’t write an automatable quality gate, a command that returns pass or fail with no human interpretation required, you cannot parallelize safely. Vague migration instructions multiplied across 24 agents produce 24 different implementations of the vague part. And since each agent can’t see what the others are doing, you don’t find out until you try to merge.
This isn’t a personal preference. Four practitioners at Dev With AI, Sciara, Grégoire, Negouai and Drode, Laforge, plus an independent voice on the IFTTD podcast (episode 311), all describing completely different projects, landed on the same rule for automating any kind of review without citing each other: binary, deterministic criteria only, typed code, function under 30 lines, config present. Subjective judgment does not automate. Binary judgment does. If your migration’s correctness can’t be reduced to a binary check, you’re not ready to parallelize it, you’re ready to write the check.
Before spawning anything, work through this checklist against your specific migration:
Are the files independent within the scope of the migration? A JSX to TSX migration is usually safe here because each file converts in isolation. A migration that renames a shared type used across 50 files is not, because the change to the type definition needs to land before any of the 50 files can convert. When independence isn’t obvious, dep-scope maps symbol-level TS/JS dependencies so you can see which files actually share a type before you split the work.
Is the quality gate a command, not a judgment call? tsc --noEmit returning zero is a quality gate. “Looks correct” is not. If you can’t encode the criterion as a shell command, go back to the brief and make the conventions more specific until you can.
Does the brief cover the edge cases Phase 1 identified? If Phase 1 found that files in src/legacy/ use a different import pattern, the brief must say what to do about it. An agent encountering an undocumented edge case will either make a wrong call or raise an exception. The first is worse.
Do you have characterization tests, or can you generate them in 20 minutes? This matters for behavioral migrations more than mechanical ones. For JSX to TSX, tsc is enough. For a migration that touches runtime behavior, you need a baseline to compare against.

Worktree isolation
Without worktrees, parallel agents working on the same filesystem collide. Git stash conflicts, concurrent writes to the same file, and inconsistent index state are all real failure modes when two agents happen to touch overlapping paths.
Worktrees give each agent a completely separate working copy of the repository in its own directory, with its own branch. Agent 01 works in ../migration-agent-01 on branch feat/migration-jsx-01. Agent 02 works in ../migration-agent-02 on feat/migration-jsx-02. They share the same Git history but can’t interfere with each other’s filesystem.

You can create them manually:
git worktree add ../migration-agent-01 -b feat/migration-jsx-01
git worktree add ../migration-agent-02 -b feat/migration-jsx-02
# ... repeat for each agent
Or let Claude Code handle it automatically with isolation: worktree in the agent frontmatter:
---
name: migration-agent
model: claude-sonnet-5
isolation: worktree
allowed-tools:
- Read
- Edit
- Write
- Bash
---
You will receive: brief.md (conventions), a list of files, and a quality gate command.
Apply the migration to your file list following brief.md exactly.
Run the quality gate command. If it passes, exit cleanly.
If it fails, write exception-report.md explaining what blocked you and exit.
Do not touch files outside your assigned list.
The isolation: worktree option creates a fresh worktree per agent invocation and cleans it up on exit if no changes were made. If the agent makes changes, the worktree path and branch name come back in the result so the coordinator knows where to find the work.
Worktrees solve a specific problem: agents colliding on the same filesystem. They do not sandbox what an agent can reach once it’s running inside its worktree, network calls, credentials in scope, files outside the migration target. For a JSX to TSX pass on already-versioned code, that’s usually fine. For a migration touching anything closer to secrets, deploy scripts, or external calls, add the isolation layer that shows up independently across three separate practitioner communities: an ephemeral container, read-only mount outside the target files, a network allowlist, and a hard time quota. Worktrees keep git clean; that additional layer keeps the blast radius small.
The /batch command
/batch here is a skill you author yourself, not a built-in Claude Code command; it orchestrates Phase 2. It doesn’t take flags: you give it a plain-language instruction, it proposes a decomposition into independent units (typically 5 to 30), and nothing runs until you validate the plan. Three things belong in that instruction.
The parallelism you want. For a 243-file migration, around 24 agents at 10 files each is a reasonable split. The right number depends on the independence of your files and the time each agent takes: if your quality gate is fast (under 30 seconds), you can push toward 30 agents, and if it’s slow (tsc on a large project can take 2-3 minutes), fewer agents means less wasted wall time when one fails early.
The quality gate, as the exact command each unit must pass. For the JSX to TSX migration: tsc --noEmit --project tsconfig.json.
And the brief produced in Phase 1, so every agent reads the conventions before touching its assigned files.
A complete invocation for the JSX to TSX case:
/batch migrate every *.jsx file under src/ to TSX, following the conventions
in .claude/migration/brief.md. Split the work across roughly 24 parallel
agents. Each unit must pass "tsc --noEmit --project tsconfig.json" before
it counts as done.
What /batch doesn’t handle automatically: detecting cross-file dependencies that should have blocked decomposition, resolving conflicts between ready branches that touched the same shared file, and reviewing the exceptions. Those three points stay human. The ready branches merge cleanly precisely because the decomposition step ensured they’re independent. If something slips through and two branches conflict, that’s a signal the decomposition had a gap.
The merge itself should stay boring: merge the ready branches sequentially into the integration branch and re-run the quality gate after each merge, stopping at the first failure. A branch that passed its gate in isolation can still break the build once its neighbors have landed; the serialized re-run is what catches those interaction effects. Resist the urge to octopus-merge 24 branches in one shot, because when that build fails you’re back to bisecting by hand.
Characterization tests
For behavioral migrations (not just type annotation migrations), characterization tests are what make Phase 2 safe. The goal isn’t to verify correctness. It’s to verify that the output after migration is identical to the output before. If test-first workflows with Claude are new territory, the TDD guide covers the discipline this builds on.
The prompt to generate them:
For [filename], capture its current output for [these inputs] as tests.
Do not assess whether the output is correct. Do not optimize anything.
Capture only what the function currently returns for the given inputs,
so we can verify the migration produces the same results.
For Python numerical migrations (Pandas, NumPy), pd.testing.assert_frame_equal with rtol=1e-5 handles floating-point tolerance without false negatives. For Django view migrations, assertContains on the response body catches behavior drift without parsing HTML.
The test doesn’t prove the pre-migration code was right. It proves the post-migration code is the same as the pre-migration code. That’s the guarantee you need to merge confidently at scale.
The 1.2% exceptions
On the 243-file migration, three files triggered exception reports. What distinguished them from the 240 that passed isn’t interesting in general terms, but the pattern holds across migrations: exceptions are files with dependencies outside the agent’s assigned scope, patterns the brief didn’t cover, or circular imports the quality gate can’t resolve in isolation.
The coordinator agent produces an exception queue: a list of branch names with their exception-report.md content. Each report contains the file that failed, the exact quality gate output, and a short description of what the agent couldn’t resolve. A human reviewer picks up the queue and handles each one with that context already loaded.
The three files from the JSX to TSX migration each had a different blocker. One had a circular import that tsc flagged only when combined with its dependent. One used a third-party type that hadn’t been updated to support JSX transforms correctly. One depended on a utility in src/legacy/ that the brief hadn’t covered because Phase 1 hadn’t indexed that directory.
Each of those took about 15 minutes to resolve by hand. The alternative was having all three problems scattered invisibly through a sequential session, discovered at code review or after merge.
What makes Phase 1 worth the time
The Opus planning phase takes 20-30 minutes to run on a medium-sized codebase. That time feels like overhead, especially when you’re impatient to start the migration. It isn’t.
Phase 1 is where the exceptions get anticipated instead of discovered. An Opus agent reading the full codebase will find the src/legacy/ directory, notice the different import patterns, and write a specific rule into the brief. It will find the circular imports and flag those files for human review before any Sonnet agent wastes time failing on them. It will identify that five files use a deprecated pattern that requires a non-mechanical conversion and route them to the exception queue preemptively.
The brief that comes out of Phase 1 is the specification the team would have written if they’d had time to read 243 files before starting the migration. Most teams don’t. Opus does it in 20-30 minutes, and what it produces is accurate enough to let 24 agents work in parallel without coordination.
Budget the run consciously too. One Opus pass over the codebase plus 24 Sonnet agents each reading and rewriting ten files, with gate re-runs, lands on the order of ten million tokens all included. At current API pricing, with the bulk of that volume on the cheaper Sonnet tier and only the single planning pass on Opus, that’s tens of dollars, not hundreds; a run leaning heavier on Opus can land higher. Against three days of engineer time the arithmetic isn’t close either way, but it deserves a line in the plan rather than a surprise on the invoice.
The pattern generalizes past migrations. Stefan Negouai and Sébastien Drode’s team, presenting at Dev With AI, moved their review discipline up a level for the same reason: review the plan, not the twenty-four diffs it produces. At the industrial end of the spectrum, Jean-Louis Rigau and Emmanuel Sciara’s Gastown orchestrator and Hubert Grégoire’s Agentic Factory are the same two-phase idea with permanent infrastructure around it: a durable git-based work registry, specialized agents, and an LLM judge at the merge gate.
Start there. Write the brief before touching any files. If the brief isn’t good enough to drive an automatic quality gate, the migration isn’t ready to parallelize.