Skip to content

Add migrate skill: repo-wide changes via a codemod, migrate callers, delete the legacy path, verify each step #14

Description

@thisguymartin

Problem

Repo-wide changes (rename an API, swap a library, change a data shape, move a package) are the work agents are best positioned to do and most often botch: half the callers migrated, the old path left behind "for now", a hand-edited sweep that missed the generated files, no proof the behavior held.

pstack has the principles: principle-build-the-lever (build the tool, don't hand-edit), principle-migrate-callers-then-delete-legacy-apis, principle-sequence-verifiable-units. The refactoring playbook owns "structure changes, behavior doesn't". What's missing is the workflow that strings them together for the large case, where the change touches dozens of files and must land in reviewable pieces.

Proposed skill

Skill Use it when
migrate A change is too wide to edit by hand (an API rename, a library swap, a schema or type change, a package move): build the codemod, run it, migrate every caller, delete the legacy path, and prove behavior held at each step.

Flow: inventory -> design the target + the lever -> codemod -> callers in verifiable units -> delete legacy -> diff-behavior on the whole.

  1. Inventory. how on the thing being migrated. Enumerate every call site, including generated code, tests, docs, scripts, and templates. Count them. This number is the done predicate: it goes to zero.
  2. Design. architect when the target shape is contested (new type, new interface). Decide the migration order: shim first (old and new coexist) vs big-bang. Default to shim + sequenced units so each PR is green on its own.
  3. Build the lever. Write the codemod: ast-grep, ts-morph, gofmt -r, gopls rename, a Semgrep autofix, or a script. Run it on one file, review the diff by eye, then on all. The codemod is committed as the first artifact so a reviewer can rerun it. Hand edits are allowed only for the cases the codemod can't express, and each one is listed.
  4. Migrate in units. Group call sites into verifiable units (per package, per service). Each unit: run codemod -> build -> tests -> the project's verification skill on the affected surface -> commit. tdd for any caller whose behavior the migration could change.
  5. Delete the legacy path. Only when the inventory count is zero. Remove the shim, the old API, the compatibility layer. This step is not optional and not deferred to "a follow-up".
  6. Prove the whole. diff-behavior trunk vs head over the load-bearing scenarios. blast-radius for anything outside the diff. interrogate when the design was contested.
  7. Ship. make-pr-easy-to-review: codemod commit first, then generated commits, then hand edits, then deletion. Reviewers check the codemod, not 80 files.

Output: the codemod, the inventory (before/after counts), one PR or a stack, the verification record per unit.

Real-world use cases

1. Go: swap log for slog across a service. 140 call sites in 30 packages. Migrate writes a gopls/ast-based rewrite for the common patterns (log.Printf -> slog.Info with key/value args), lists the 9 sites it can't express (format strings with computed widths), migrates per package with go test ./pkg/... and the verification skill's log-format scenario, then deletes the log wrapper package. Reviewer reads one codemod and 9 hand edits.

2. TypeScript: rename a domain type field. Order.customerId -> Order.buyerId across API, DB layer, and 3 consumers in a monorepo. ts-morph codemod; the DB column gets a shim (both names for one deploy), the consumers migrate as separate units, then the shim and old column go. diff-behavior proves order creation and lookup are unchanged.

3. Library swap. moment -> date-fns. Inventory finds 62 sites plus 4 in templates the grep missed. Codemod handles 50; 12 are hand-migrated and listed. tdd adds tests for the timezone-sensitive ones because the libraries differ there. moment is removed from package.json in the final commit, not "later".

4. Package move. internal/util split into internal/strutil and internal/timeutil. gopls rename + goimports, per-consumer units, blast-radius on the CLI that imports both.

What already exists and how this differs

  • refactoring playbook: the contract (structure changes, behavior doesn't). Migrate is the large-scale, tool-driven form of it.
  • principle-build-the-lever, principle-migrate-callers-then-delete-legacy-apis: migrate is those principles as a workflow with a done predicate.
  • architect: called for the target shape when contested.
  • diff-behavior (separate issue), blast-radius, interrogate: verification of the result.

Guardrails

  • No hand-edit sweeps. A hand edit needs a one-line reason in the list.
  • Legacy deletion is part of the skill, not a follow-up issue.
  • Each unit is green on its own before the next starts. No "fix it all at the end".
  • Writers get a worktree. No implicit timeout, no fallback lanes.

Acceptance

  • plugins/pstack/skills/migrate/SKILL.md, shared tree, with the unit template and the commit-order convention.
  • README row and docs/reference.md entry; typescript-best-practices cross-link where it names codemod tools.
  • Bun tests, strict typecheck, static invariants, plugin validation pass.
  • Live test on the installed candidate in each affected harness: a scratch repo with 20+ call sites of an API, run migrate, confirm codemod committed, inventory reaches zero, legacy removed, tests green at each unit. Record installed version, surface, action, observed result in the PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions