refactoring
Repo-wide codemods with AI: automated large-scale refactoring
Learn how AI-powered codemods automate large-scale refactoring across entire repositories with guardrails for safety and consistency.

You've renamed a core interface, and now 147 files need updating. Or you're migrating from an old logging library to a new one, touching every service. Manual find-and-replace is error-prone; regex is brittle; and writing a custom AST transformer for a one-off task feels like overkill.
AI-powered codemods change this equation. Instead of hand-coding tree transformations or hoping a regex won't catch false positives, you describe the change in plain language and let the agent generate small, testable diffs across hundreds of files. The catch: repo-wide changes need guardrails—every edit must compile, pass lint, and ideally pass tests before you merge.
Why traditional codemods fall short at scale
Classical codemods—tools like jscodeshift, comby, or language-specific AST rewriters—work well for mechanical transformations when you can predict every edge case. Renaming a function? Straightforward. Switching from logger.info() to log.Info() in Go? Easy if the import paths are consistent.
They break down when:
- Context matters. You want to update
fetchUser()calls, but only the ones that don't already pass an options object. - Syntax varies. Some files use class components, others hooks; some have trailing commas, others don't.
- The rule is fuzzy. "Move this config from environment variables to a YAML file, updating all callsites" isn't a regex or a simple AST pattern.
Hand-writing transforms for these cases is possible, but slow. You end up spending an hour debugging your jscodeshift script for a refactor you'll run once. AI codemods let you trade upfront engineering time for iteration: describe the change, inspect a few sample diffs, adjust the prompt, and apply.
The agent loop for safe, iterative codemods
Goatfied's agent loop is built for this pattern: plan → constrain → edit → validate → retry. You give the agent a high-level instruction—"replace all uses of OldCache with NewCache, updating import paths"—and it generates a plan, proposes diffs, and runs compile/lint/test checks before finalizing.
The key insight: you don't apply all 147 edits at once. The agent works file-by-file or in small batches, and every batch must pass validation gates. If a change introduces a type error, the agent sees the compiler output and retries with a fix. If a test fails, it can roll back or adjust.
This looks like:
goatfied codemod \
--scope "src/**/*.ts" \
--instruction "Replace Logger.log() with log.info(), updating imports from './logger' to './lib/log'" \
--validate "npm run typecheck && npm run lint"
The agent identifies affected files, generates diffs, runs typecheck and lint after each batch, and stops if validation fails. You review the plan, inspect a sample diff, and approve the run. The result is a stack of small, reversible commits—one per file or logical group—that you can merge incrementally or revert if something unexpected surfaces in CI.
Constraining scope to avoid runaway changes
Repo-wide codemods can touch dozens of files in one pass. Without constraints, you risk the agent rewriting test fixtures, generated code, or vendored dependencies you didn't mean to touch.
Explicit scope filters are essential:
- Path globs.
src/**/*.gotargets application code;**/testdata/**excludes fixtures. - Exclude patterns. Skip
node_modules,dist,*.pb.go(protobuf-generated files), or any directory where edits would be overwritten by a build step. - File-level guards. Some teams add a comment
// codemod: ignoreto files that should never be auto-modified (e.g., legacy compatibility shims).
Goatfied's constraint system enforces these at planning time. If you specify --scope "internal/**/*.go", the agent won't touch vendor/ or cmd/, even if those files import the symbol you're refactoring. You can layer additional constraints:
--scope "internal/**/*.go" \
--exclude "**/mocks/**" \
--exclude "**/*_test.go"
This prevents accidental edits to mocks or tests while refactoring production code. You can run a second pass targeting **/*_test.go with a slightly different instruction ("update test assertions to match the new NewCache.Get() signature") if needed.
Breaking codemods into verifiable stages
Large refactors are safer and easier to review when split into stages. Instead of one 500-file PR, you ship three or four PRs, each validated independently:
1. Add the new API alongside the old. Introduce NewCache without removing OldCache. Tests still pass; nothing breaks.
2. Migrate callsites in application code. The agent updates internal/ to call NewCache, leaving tests and legacy shims untouched. Compile, lint, test. Merge.
3. Migrate tests and mocks. Update **/*_test.go, re-run the suite. Merge.
4. Remove the old API. Delete OldCache, confirm nothing imports it, merge.
Each stage is a small, reversible diff. If stage 2 uncovers an edge case—some files need a compatibility wrapper—you fix it in isolation, re-run validation, and move on. The AI agent doesn't need to understand the full multi-stage plan; you orchestrate the stages, and the agent executes each one as a bounded codemod.
This approach also helps with downstream consumers. If you're refactoring a library that other teams depend on, you can ship stages 1–2 as a minor version bump (old API still works), give teams time to migrate, then remove the old API in a major version. The codemod tooling works the same way; you're just controlling the rollout timeline.
Validating with compile gates and targeted tests
The difference between a codemod that works and one that introduces subtle bugs is automated validation after every edit. Goatfied enforces this by default: every proposed diff must pass compile, lint, and optionally test gates before the agent considers it complete.
For a TypeScript codemod:
--validate "tsc --noEmit && eslint src/"
For Go:
--validate "go build ./... && golangci-lint run"
If the agent produces a diff that makes tsc emit an error, it sees the error message, analyzes the failure, and retries with a corrected edit. You don't merge broken code; you only see diffs that passed all gates.
You can tune validation strictness. For a low-risk codemod (renaming a private helper function), compile + lint may be enough. For a data-model change, add unit tests:
--validate "npm run build && npm test -- --testPathPattern=models"
The agent runs targeted tests after each file or batch. If a test fails, it can inspect the failure, adjust the edit, and retry. This loop—edit, validate, retry—mirrors how a human engineer would approach the refactor, but it runs in seconds instead of hours.
Reviewing AI-generated diffs before merging
Even with validation gates, you should review AI-generated codemods before merging. The agent might produce syntactically correct code that misses business logic nuances—updating all fetchUser() calls, including one that intentionally used the old signature for backward compatibility.
Goatfied's diff output is structured for review:
- Each file gets a separate commit or staging entry, so you can review one change at a time.
- Diffs are small and focused; the agent doesn't reformat unrelated code or reorder imports unnecessarily.
- You can cherry-pick files from the batch, merge the straightforward ones, and hand-edit the tricky cases.
In practice, reviewing 50 auto-generated diffs is faster than writing 50 manual edits. You're looking for logical errors, not syntax mistakes (the compiler already caught those). Scan for unexpected changes—files you didn't mean to touch, or callsites where the new API isn't quite right—and flag them. The rest merges cleanly.
When to write a custom codemod vs. using AI
AI codemods shine for one-off refactors, exploratory migrations, or changes with fuzzy rules. But they're not always the right tool:
- Repeated, mechanical transforms. If you're running the same refactor every week (e.g., updating API client code after every backend change), a hand-coded jscodeshift script is faster and more predictable.
- Performance-critical transforms. AI codemods are interactive; they take seconds per file. For a 10,000-file codebase, a custom AST tool may be faster.
- Compliance or audit requirements. Some teams need deterministic, auditable transforms with zero AI-generated variation. A scripted codemod + manual review satisfies that; an AI codemod may not.
The best workflow: use AI codemods for the first pass, then codify patterns you repeat as traditional scripts. The AI helps you discover the right transformation; the script automates it long-term.