Code Migration Planner
Large migrations fail socially before they fail technically: they stall half-finished, leaving the codebase speaking two dialects forever. This skill plans migrations — framework upgrades, language version bumps, pattern replacements, datastore moves — as a sequence of independently shippable waves, each of which leaves the system working and the team no worse off if the whole effort pauses tomorrow.
When to use this skill
- Upgrading a framework or language runtime across a major version boundary
- Replacing a pattern codebase-wide: callbacks to async, an ORM swap, a new auth layer
- Consolidating duplicated systems after two codebases merge
- A previous migration stalled at 60% and you need a plan to finish it — or retreat cleanly
- Estimating whether a proposed migration is worth doing at all
Workflow
- Inventory before opinion. Enumerate every occurrence of the thing being migrated — files, call sites, schema references — with a script or search, never from memory. Record the count; it becomes the progress denominator for the entire effort.
- Classify occurrences into difficulty buckets: mechanical (a codemod can do it), semi-mechanical (pattern plus judgment), and bespoke (each one is its own small project). Estimate the buckets separately; the bespoke tail dominates the schedule every time.
- Choose a strategy explicitly:
- Big bang only when the surface is small and atomically testable
- Strangler — old and new coexist behind a seam — for anything long-running
- Codemod-first when the mechanical bucket is above roughly 70%
- Design the compatibility seam for strangler migrations: an interface, adapter, or flag that lets both implementations run side by side, with a way to observe at runtime which path served a given request.
- Sequence the waves so risk is front-loaded and value arrives early: wave 1 proves the pattern on one gnarly-but-contained area; later waves get increasingly mechanical. Every wave ends green in CI and deployable.
- Write the rollback story per wave before starting the wave. "Revert the change" is acceptable only when the wave contains no data or schema changes; otherwise write the actual down-path, expand/contract style.
- Add a ratchet from wave 1: a lint rule, CI check, or import ban that stops new code from using the old pattern. Migrations without ratchets refill behind you as fast as you drain them.
- Track and publish the burndown — occurrences remaining versus the wave plan — so a stall becomes visible in weeks, not quarters.
Output format
## Migration plan: <old> → <new>
**Inventory:** <n> occurrences across <m> files (counted by: <script/search>)
**Buckets:** mechanical <n1> / semi-mechanical <n2> / bespoke <n3>
**Strategy:** <big bang | strangler | codemod-first>, because <reason>
**Seam:** <interface or flag>, observable via <metric or log>
**Ratchet:** <check>, landing in wave 1
| Wave | Scope | Occurrences | Risk | Rollback |
|------|-------|-------------|------|----------|
| 1 | ... | ... | ... | ... |
**Definition of done:** old-pattern count = 0, seam removed, ratchet retired
**Stop-loss:** if <signal>, pause after the current wave and hold at the seam
Quality bar
- The plan survives a pause: after any wave, nothing is half-broken and the seam is documented
- Progress is measured by the inventory count, not by effort spent or changes merged
- The riskiest integration is scheduled first, not saved for a glorious finale
- "Done" includes deleting the old path and the seam — a migration is not finished at 100% adoption; it is finished at removal
- Someone reading only the plan can say what happens to in-flight data during cutover
Anti-patterns to reject
- Migrating opportunistically "as we touch files" with no ratchet — this converges never
- A seam so heavy it quietly becomes permanent architecture
- Counting the mechanical 70% as progress while the bespoke 30% sits unexamined
- Schema changes and code changes in the same wave when expand/contract could split them
- A completion date with no burndown behind it