migration-loop
A configuration of loop-controller. That skill supplies the loop
machinery — primitive selection, the full guardrail stack, state
externalization. This skill supplies the two things specific to "migrate a
fixed set of targets to completion": a default-FAIL target checklist driven
one entry per iteration, and a proof that no legacy pattern remains (a grep
for the old pattern returning zero), not merely "tests pass." Read
loop-controller for the guardrails; they're inherited, not repeated here.
Why disable-model-invocation: this loop edits and commits code on its own
— a checkpoint commit per migrated target — until the whole set is done. You
want to type /migration-loop (or have the orchestrator dispatch it) — not
have Claude silently start rewriting every file because it noticed an old API.
The 5-part contract
| Part |
This loop |
| trigger |
a finite, enumerable set of targets needs the same transform (a codemod, a Jest-to-Vitest swap, an API version bump, a framework/import rename), plus a way to verify behaviour is preserved — or an explicit /migration-loop (optionally scoped to one path) |
| action |
ONE iteration: pick the next "migrated": false target from the checklist → apply the transform to just that target → run the suite to confirm behaviour-identical → on green, commit a checkpoint naming the target → flip its checklist entry to true |
| proof |
EVERY checklist entry "migrated": true AND the full suite exits 0 AND a grep for the legacy pattern returns zero matches — default-FAIL: assume incomplete until all three artifacts (the checklist, the suite exit code, the grep count) are observed together |
| memory |
migration-checklist.json (one default-FAIL entry per target + the old→new mapping), PROGRESS.md (what's migrated / next), a git checkpoint commit per migrated target |
| stop |
all three proof artifacts hold OR iteration cap OR no-progress for 3 rounds OR budget cap OR an HITL checkpoint for an irreversible target (see Guardrails) |
The proof: checklist + grep-for-old-pattern, default-FAIL
"Done" is not "the suite is green." A green suite proves the targets you
touched still behave — it says nothing about the ones you missed. The thing
that distinguishes a real migration from a partial one is the third artifact: a
grep for the legacy pattern returning zero. Three signals, all required, all
default-FAIL (assume not-done until each is observed):
- The checklist is exhausted — every entry in
migration-checklist.json is
"migrated": true. Each starts false and only flips when that target is
transformed and its checkpoint commit lands.
- The full suite exits 0 — re-run the whole suite, not just the touched
file's tests; the migration is behaviour-preserving only if nothing else
regressed (
loop-controller Step 5, "re-verify the whole").
grep -r '<legacy-pattern>' returns zero across the migrated scope —
the mechanical proof that no call site, import, or usage was skipped. Pin the
exact pattern(s) in the checklist so every iteration (and the final check)
greps the identical string. A non-zero count is a finding: a missed
target, not a passing migration.
Building the checklist/mapping, choosing the pattern(s), and the
"no legacy pattern remains" verification are detailed in
references/migration-checklist.md.
Step 1 — Enumerate the targets and build the default-FAIL checklist
Discover the full target set before looping — a migration over a set you
haven't fully enumerated cannot prove completion. Use Grep/Glob to find every
occurrence of the legacy pattern, dedupe to a target list, and write each as a
JSON entry with "migrated": false, the old→new mapping, and the exact grep
pattern. Store it at the profile-defined path (default
migration-checklist.json). The checklist is the loop's scope: if a target
isn't on it, the loop won't migrate it, and the final grep will catch the gap.
Step 2 — Decide sequential vs fan-out
- Sequential (
/goal, default) — one target per iteration. Use when targets
are interdependent, the transform is subtle, or the set is small enough that
per-target review matters. The Haiku evaluator reads the suite output and the
grep count you surface each turn.
- Parallel (
/batch / dynamic workflow) — for large, mechanically uniform
sets (hundreds of identical codemod sites). Fan out across worktree-isolated
subagents (compose the orchestrator's Workflow mode), each migrating a slice
and committing in its own worktree. Cap build/test parallelism at 1 per
worktree (loop-controller Step 5) — two builds at once destroy the
backpressure signal. The whole-set grep + suite still run once after the merge
as the single proof. The decision rule and the fan-out shape are in
references/migration-checklist.md.
Step 3 — Migrate one target, verify behaviour-identical, checkpoint
Apply the transform to one target (or one worktree slice). Then run the
full suite — a migration is only valid if behaviour is preserved, so the test
gate is the per-iteration verifier. If the suite goes red, the transform changed
behaviour: fix it (or invoke fix-until-green if it's a non-trivial gate
failure) before moving on — never advance the checklist over a red suite. On
green, commit a checkpoint naming the migrated target, then flip its entry to
true. The commit-per-target trail is the loop's undo: git reset --hard to the
last green target is cheaper than rescuing a wedged transform.
Step 4 — Re-run the whole proof, repeat
After the last target, run all three proof artifacts together: the checklist must
be fully true, the full suite must exit 0, and the legacy-pattern grep must
return zero. A non-zero grep with a "fully migrated" checklist means a target
was missed during enumeration — add it and loop. When all three hold on one pass,
the loop is done; report the final checklist, the suite exit code, and the
grep -c output (zero) as evidence.
Guardrails specific to this loop
Inherits the full stack from loop-controller → references/safety.md. The caps
this loop sets:
- Iteration cap — default ~1 per target plus slack (read from
.claude/profile.yaml if set); for fan-out, a hard cap on concurrent worktrees.
Hitting the cap is a stop-and-escalate, not a license to relax the grep or
mark targets done without migrating them.
- No-progress detection — if the same target stays
"migrated": false (or the
legacy grep count stops dropping) for 3 consecutive rounds, stop and
escalate. The same target failing three times means the transform is wrong, not
that it needs a fourth try.
- Never fake completion. Forbidden, each a finding: marking a target
"migrated": true without transforming it, narrowing the grep pattern so it
reports zero while usages remain, or deleting/skipping a test to keep the suite
green over a behaviour change. A zero grep that came from a weakened pattern is
not a migration.
- Reversible code transforms are AFK-safe; irreversible resources are HITL. A
code/codemod migration behind a test gate with checkpoint commits per target is
reversible — fine unattended. A migration that touches an irreversible
resource — a real DB schema migration against live data, a destructive data
backfill, an external system — is an HITL checkpoint (
loop-controller
guardrail 4): pause for the human, and route the schema-migration case to
db-migration-agent, which owns up/down migrations, dry-runs, and rollback.
Choosing the driver primitive
Per loop-controller Step 1, by the target set:
- Default —
/goal for sequential one-target-per-iteration: the proof is
provable from the suite output + the grep count you surface, so
/goal "every entry in migration-checklist.json is migrated:true, the full suite exits 0, and a grep for the legacy pattern returns zero — or stop after N turns." Remember the embedded turn cap — /goal has no native budget.
/batch / dynamic workflow for large parallel sets with worktree isolation
(hence Agent in allowed-tools), composing the orchestrator's Workflow
mode. The whole-set grep + suite run once after merge as the proof.
Using it under the orchestrator
This is the Migration inner loop (archetype 6). The orchestrator dispatches
it for a planned migration, routing per-file failures back to the owning role
by file (a red suite in src/api/ goes to the backend agent). The
orchestrator does not override a stuck loop — if migration-loop escalates
after no-progress, that's a real migration blocker, not a number to paper over. A
fully-migrated checklist informs the build; the qe-agent's qa-report.json
still decides the gate (loop-controller's rule: the loop informs, the gate
decides).
Expand–contract mode (when one change breaks everything at once)
A single mechanical change whose blast radius breaks thousands of call sites in
one motion — rename a column, retype a shared symbol, change a core signature —
can't run as the normal migrate-and-verify loop: every batch is red until the
last one lands. Sequence it expand → migrate → contract instead:
- Expand — add the new form beside the old (new column, overloaded
signature, aliased export). Nothing breaks; the suite stays green.
- Migrate — drive call sites over in blast-radius-sized batches with the
normal checklist loop; each batch stays green because the old form still
exists.
- Contract — when the legacy-pattern grep hits zero call sites, delete
the old form. The final proof adds a grep for the old definition.
Wire the stages as three checklist phases — the contract stage is its own target
whose transform is the deletion. When batches can't stay green alone (deeply
entangled sites), fall back to a shared integration branch that merges only when
the whole set is green. (Adapted from mattpocock engineering/to-tickets
v1.1.0; CB-9.)
Long-run hygiene (wired per loop-controller Step 6)
A long migration is exactly where the Claude 5 long-run rules earn their keep
(drop-in text: model-adaptation → references/long-run-hygiene.md):
- Evidence-backed progress — a checklist entry flips
true only on the
observed pair (suite exit 0 + checkpoint commit hash) for that target; a
PROGRESS.md line without both is fabrication, not progress.
- Don't end an iteration on a promise — each iteration ends with a target
migrated, committed, and flipped, or at a declared stop condition; never with
"next I'll migrate X" and no edit.
- Budget is a harness decision — read caps from
.claude/profile.yaml;
don't show the model a countdown, and don't let a long set trigger
summarize-and-quit — the checklist externalizes exactly where to resume.
- Effort per iteration — mechanical transforms run at
medium/low;
reserve high/xhigh for a transform that reds the suite (tiering:
model-adaptation).
How this differs from its neighbors
- vs.
fix-until-green — that loop drives a failing suite to green; this
loop drives a fixed target set to fully migrated. The suite green is only one
of three proof artifacts here; the load-bearing one is the legacy-pattern grep.
This loop invokes fix-until-green when a transform reds the gate.
- vs.
db-migration-agent — that role owns a single irreversible schema
migration (up/down, dry-run, rollback) as an HITL operation; this loop drives a
set of reversible code transforms to completion and delegates the schema
case to it at the HITL checkpoint.
Reference files
references/migration-checklist.md —
building the target checklist/mapping (the migration-checklist.json
default-FAIL schema with a worked example), the sequential-vs-/batch-fan-out
decision and the worktree-isolation shape, and the "no legacy pattern remains"
verification (choosing the grep pattern, scoping it, and reading the count).
1---2name: migration-loop3description: Migrate a known, enumerated set of targets one at a time until EVERY one is done — one iteration picks the next un-migrated target from a checklist, transforms it, verifies behaviour-identical (the suite stays green), commits a checkpoint, and marks it migrated, with a hard exit that BOTH the suite is green AND a grep for the old pattern returns zero. The only loop over a fixed target set whose proof is "no legacy pattern remains," not just "tests pass." Use for a codemod across a repo, a Jest-to-Vitest swap, an API version bump, a framework migration, an import/dependency rename, or any mechanical transform applied to a finite list of files. Trigger on "migrate everything from X to Y", "run the codemod across the repo", "bump every call site", "swap the framework", "migrate all the tests", "finish the migration", "no old pattern left", "/migration-loop". A configuration of loop-controller.4---56# migration-loop78> **A configuration of [`loop-controller`].** That skill supplies the loop9> machinery — primitive selection, the full guardrail stack, state10> externalization. This skill supplies the two things specific to "migrate a11> fixed set of targets to completion": a **default-FAIL target checklist** driven12> one entry per iteration, and a **proof that no legacy pattern remains** (a grep13> for the old pattern returning zero), not merely "tests pass." Read14> `loop-controller` for the guardrails; they're inherited, not repeated here.15>16> **Why `disable-model-invocation`:** this loop edits and commits code on its own17> — a checkpoint commit per migrated target — until the whole set is done. You18> want to *type* `/migration-loop` (or have the orchestrator dispatch it) — not19> have Claude silently start rewriting every file because it noticed an old API.2021## The 5-part contract2223| Part | This loop |24|---|---|25| **trigger** | a finite, enumerable set of targets needs the *same* transform (a codemod, a Jest-to-Vitest swap, an API version bump, a framework/import rename), plus a way to verify behaviour is preserved — or an explicit `/migration-loop` (optionally scoped to one path) |26| **action** | ONE iteration: pick the next `"migrated": false` target from the checklist → apply the transform to **just that target** → run the suite to confirm behaviour-identical → on green, **commit a checkpoint** naming the target → flip its checklist entry to `true` |27| **proof** | **EVERY** checklist entry `"migrated": true` **AND** the full suite exits 0 **AND** a grep for the legacy pattern returns **zero** matches — default-FAIL: assume incomplete until all three artifacts (the checklist, the suite exit code, the grep count) are observed together |28| **memory** | `migration-checklist.json` (one default-FAIL entry per target + the old→new mapping), `PROGRESS.md` (what's migrated / next), a git checkpoint commit per migrated target |29| **stop** | all three proof artifacts hold **OR** iteration cap **OR** no-progress for 3 rounds **OR** budget cap **OR** an HITL checkpoint for an irreversible target (see Guardrails) |3031## The proof: checklist + grep-for-old-pattern, default-FAIL3233"Done" is **not** "the suite is green." A green suite proves the targets you34*touched* still behave — it says nothing about the ones you *missed*. The thing35that distinguishes a real migration from a partial one is the third artifact: a36**grep for the legacy pattern returning zero**. Three signals, all required, all37default-FAIL (assume not-done until each is observed):38391. **The checklist is exhausted** — every entry in `migration-checklist.json` is40 `"migrated": true`. Each starts `false` and only flips when *that* target is41 transformed and its checkpoint commit lands.422. **The full suite exits 0** — re-run the *whole* suite, not just the touched43 file's tests; the migration is behaviour-preserving only if nothing else44 regressed (`loop-controller` Step 5, "re-verify the whole").453. **`grep -r '<legacy-pattern>'` returns zero** across the migrated scope —46 the mechanical proof that no call site, import, or usage was skipped. Pin the47 exact pattern(s) in the checklist so every iteration (and the final check)48 greps the *identical* string. A non-zero count is a *finding*: a missed49 target, not a passing migration.5051Building the checklist/mapping, choosing the pattern(s), and the52"no legacy pattern remains" verification are detailed in53[`references/migration-checklist.md`](references/migration-checklist.md).5455## Step 1 — Enumerate the targets and build the default-FAIL checklist5657Discover the full target set *before* looping — a migration over a set you58haven't fully enumerated cannot prove completion. Use `Grep`/`Glob` to find every59occurrence of the legacy pattern, dedupe to a target list, and write each as a60JSON entry with `"migrated": false`, the old→new mapping, and the exact grep61pattern. Store it at the profile-defined path (default62`migration-checklist.json`). The checklist *is* the loop's scope: if a target63isn't on it, the loop won't migrate it, and the final grep will catch the gap.6465## Step 2 — Decide sequential vs fan-out6667- **Sequential (`/goal`, default)** — one target per iteration. Use when targets68 are interdependent, the transform is subtle, or the set is small enough that69 per-target review matters. The Haiku evaluator reads the suite output and the70 grep count you surface each turn.71- **Parallel (`/batch` / dynamic workflow)** — for large, *mechanically uniform*72 sets (hundreds of identical codemod sites). Fan out across **worktree-isolated**73 subagents (compose the [`orchestrator`]'s Workflow mode), each migrating a slice74 and committing in its own worktree. **Cap build/test parallelism at 1 per75 worktree** (`loop-controller` Step 5) — two builds at once destroy the76 backpressure signal. The whole-set grep + suite still run once after the merge77 as the single proof. The decision rule and the fan-out shape are in78 [`references/migration-checklist.md`](references/migration-checklist.md).7980## Step 3 — Migrate one target, verify behaviour-identical, checkpoint8182Apply the transform to **one** target (or one worktree slice). Then run the83**full** suite — a migration is only valid if behaviour is preserved, so the test84gate is the per-iteration verifier. If the suite goes red, the transform changed85behaviour: fix it (or **invoke [`fix-until-green`]** if it's a non-trivial gate86failure) before moving on — never advance the checklist over a red suite. On87green, **commit a checkpoint** naming the migrated target, then flip its entry to88`true`. The commit-per-target trail is the loop's undo: `git reset --hard` to the89last green target is cheaper than rescuing a wedged transform.9091## Step 4 — Re-run the whole proof, repeat9293After the last target, run all three proof artifacts together: the checklist must94be fully `true`, the full suite must exit 0, and the legacy-pattern grep must95return **zero**. A non-zero grep with a "fully migrated" checklist means a target96was missed during enumeration — add it and loop. When all three hold on one pass,97the loop is done; report the final checklist, the suite exit code, and the98`grep -c` output (zero) as evidence.99100## Guardrails specific to this loop101102Inherits the full stack from `loop-controller` → `references/safety.md`. The caps103this loop sets:104105- **Iteration cap** — default ~1 per target plus slack (read from106 `.claude/profile.yaml` if set); for fan-out, a hard cap on concurrent worktrees.107 Hitting the cap is a *stop-and-escalate*, not a license to relax the grep or108 mark targets done without migrating them.109- **No-progress detection** — if the same target stays `"migrated": false` (or the110 legacy grep count stops dropping) for **3 consecutive rounds**, stop and111 escalate. The same target failing three times means the transform is wrong, not112 that it needs a fourth try.113- **Never fake completion.** Forbidden, each a *finding*: marking a target114 `"migrated": true` without transforming it, narrowing the grep pattern so it115 reports zero while usages remain, or deleting/skipping a test to keep the suite116 green over a behaviour change. A zero grep that came from a weakened pattern is117 not a migration.118- **Reversible code transforms are AFK-safe; irreversible resources are HITL.** A119 code/codemod migration behind a test gate with checkpoint commits per target is120 reversible — fine unattended. A migration that touches an **irreversible121 resource** — a real DB schema migration against live data, a destructive data122 backfill, an external system — is an **HITL checkpoint** (`loop-controller`123 guardrail 4): pause for the human, and route the schema-migration case to124 [`db-migration-agent`], which owns up/down migrations, dry-runs, and rollback.125126## Choosing the driver primitive127128Per `loop-controller` Step 1, by the target set:129130- **Default — `/goal`** for sequential one-target-per-iteration: the proof is131 provable from the suite output + the grep count you surface, so132 `/goal "every entry in migration-checklist.json is migrated:true, the full133 suite exits 0, and a grep for the legacy pattern returns zero — or stop after N134 turns."` Remember the embedded turn cap — `/goal` has no native budget.135- **`/batch` / dynamic workflow** for large parallel sets with worktree isolation136 (hence `Agent` in `allowed-tools`), composing the [`orchestrator`]'s Workflow137 mode. The whole-set grep + suite run once after merge as the proof.138139## Using it under the orchestrator140141This is the **Migration inner loop** (archetype 6). The orchestrator dispatches142it for a planned migration, routing per-file failures back to the owning role143**by file** (a red suite in `src/api/` goes to the backend agent). The144orchestrator does **not** override a stuck loop — if migration-loop escalates145after no-progress, that's a real migration blocker, not a number to paper over. A146fully-migrated checklist informs the build; the `qe-agent`'s `qa-report.json`147still decides the gate (`loop-controller`'s rule: the loop informs, the gate148decides).149150## Expand–contract mode (when one change breaks everything at once)151152A single mechanical change whose blast radius breaks thousands of call sites in153one motion — rename a column, retype a shared symbol, change a core signature —154can't run as the normal migrate-and-verify loop: every batch is red until the155last one lands. Sequence it **expand → migrate → contract** instead:1561571. **Expand** — add the new form *beside* the old (new column, overloaded158 signature, aliased export). Nothing breaks; the suite stays green.1592. **Migrate** — drive call sites over in blast-radius-sized batches with the160 normal checklist loop; each batch stays green because the old form still161 exists.1623. **Contract** — when the legacy-pattern grep hits zero *call sites*, delete163 the old form. The final proof adds a grep for the old *definition*.164165Wire the stages as three checklist phases — the contract stage is its own target166whose transform is the deletion. When batches can't stay green alone (deeply167entangled sites), fall back to a shared integration branch that merges only when168the whole set is green. (Adapted from mattpocock `engineering/to-tickets`169v1.1.0; CB-9.)170171## Long-run hygiene (wired per loop-controller Step 6)172173A long migration is exactly where the Claude 5 long-run rules earn their keep174(drop-in text: `model-adaptation` → `references/long-run-hygiene.md`):175176- **Evidence-backed progress** — a checklist entry flips `true` only on the177 observed pair (suite exit 0 + checkpoint commit hash) for *that* target; a178 `PROGRESS.md` line without both is fabrication, not progress.179- **Don't end an iteration on a promise** — each iteration ends with a target180 migrated, committed, and flipped, or at a declared stop condition; never with181 "next I'll migrate X" and no edit.182- **Budget is a harness decision** — read caps from `.claude/profile.yaml`;183 don't show the model a countdown, and don't let a long set trigger184 summarize-and-quit — the checklist externalizes exactly where to resume.185- **Effort per iteration** — mechanical transforms run at `medium`/`low`;186 reserve `high`/`xhigh` for a transform that reds the suite (tiering:187 `model-adaptation`).188189## How this differs from its neighbors190191- **vs. [`fix-until-green`]** — that loop drives a *failing* suite to green; this192 loop drives a *fixed target set* to fully migrated. The suite green is only one193 of three proof artifacts here; the load-bearing one is the legacy-pattern grep.194 This loop *invokes* fix-until-green when a transform reds the gate.195- **vs. [`db-migration-agent`]** — that role owns a single irreversible schema196 migration (up/down, dry-run, rollback) as an HITL operation; this loop drives a197 *set* of reversible code transforms to completion and *delegates* the schema198 case to it at the HITL checkpoint.199200## Reference files201202- [`references/migration-checklist.md`](references/migration-checklist.md) —203 building the target checklist/mapping (the `migration-checklist.json`204 default-FAIL schema with a worked example), the sequential-vs-`/batch`-fan-out205 decision and the worktree-isolation shape, and the "no legacy pattern remains"206 verification (choosing the grep pattern, scoping it, and reading the count).207208[`loop-controller`]: ../loop-controller/SKILL.md209[`fix-until-green`]: ../fix-until-green/SKILL.md210[`orchestrator`]: ../../orchestrator/SKILL.md211[`db-migration-agent`]: ../../roles/db-migration-agent/SKILL.md