In this guide
On this page ⌄
- Key takeaways
- Why refactors go wrong with agents
- The shape of a good refactor prompt
- Characterize before you change
- Break large refactors into safe steps
- Make the agent show its blast radius
- Constraints worth stating explicitly
- Verification the agent must report
- Reviewing a refactor diff
- A worked example: splitting a god file
- Where OpenVisio fits
Refactoring is the task where coding agents look most impressive and fail most quietly. Ask an agent to "clean up the payment module" and you will get a large, tidy diff that compiles, passes most tests, and changes behavior in two places nobody asked about. The agent did what it was told. The prompt just did not say what must stay the same.
A refactor is defined by what it preserves. That makes it different from feature work, where the goal is new behavior, and it changes how you should prompt. This guide covers how to frame a refactor so an agent can execute it safely: the shape of a good prompt, the constraints that matter, how to break large refactors into steps, and the verification you should require before calling it done.
Key takeaways
- State the invariant first: what behavior, interfaces, and outputs must not change. A refactor prompt without an invariant is a rewrite prompt.
- Give the agent the blast radius up front: callers, dependents, and the tests that cover them. Ask it to confirm the list before editing.
- Split large refactors into mechanical steps that each leave the code working, and commit between steps.
- Constrain scope explicitly: which files may change, which may not, and what to do when the agent finds something outside scope.
- Require verification the agent cannot skip: tests before and after, typecheck, and a summary of every changed call site.
Why refactors go wrong with agents
Agents are trained to be helpful, and on an open-ended cleanup "helpful" means improving everything they see. Three failure patterns show up again and again.
Scope creep. The agent renames the function you asked about, then also tidies the three files that call it, upgrades an import style, and rewrites a comment. Each change is reasonable. Together they make the diff unreviewable.
Silent behavior change. The agent "simplifies" a conditional that looked redundant but handled an edge case, or replaces a loop with a method that has different ordering semantics.
Missed call sites. The agent changes a signature and updates the callers it found by text search. The call that goes through a dynamic dispatch, a string-keyed registry, or a different package is left broken until runtime.
Each of these maps to something the prompt can address: scope, invariants, and blast radius.
The shape of a good refactor prompt
Goal: Extract the tax calculation in packages/billing/src/invoice.ts
(lines ~120-210, inside buildInvoice) into a new pure function
calculateTax(lines, region) in packages/billing/src/tax.ts.
Invariants:
- buildInvoice's signature and output are unchanged for all inputs.
- Rounding behavior is identical (uses roundHalfEven).
- No changes to the public exports of packages/billing except adding calculateTax.
Blast radius (confirm before editing):
- Callers of buildInvoice and their tests.
- Any other code that relies on the intermediate tax variables.
Scope:
- May edit: packages/billing/src/invoice.ts, packages/billing/src/tax.ts (new),
packages/billing/src/tax.test.ts (new).
- Must not edit: anything else. If you find a reason to, stop and tell me.
Verification:
- Run pnpm --filter @acme/billing test before any change and record the result.
- After the change, the same tests pass, plus new table-driven tests for calculateTax.
- Typecheck passes.
- Report: files changed, any call sites touched, and anything that looked odd.
Process: investigate and confirm the blast radius first, then propose a plan,
then wait for my approval before editing.
It is longer than "extract the tax logic into its own function". Every line exists to block one of the failure patterns above.
Goal
Name the specific transformation and its location. "Extract", "rename", "inline", "move", "split", and "replace X with Y" are clear. "Clean up", "improve", and "modernize" are not.
Invariants
Say what must hold before and after. For most refactors: external behavior, public signatures, data formats, and error behavior. Call out the subtle ones you know about, such as rounding, ordering, or nullability.
Blast radius
Tell the agent what depends on the code, or ask it to find out and report back before editing. This is where structural context helps most. A tool that traces callers and lists dependents gives a complete answer. Text search gives a partial one. Our guide to impact analysis before refactoring covers this in depth.
Scope
List the files that may change. Add a rule for out-of-scope discoveries: stop and report, do not fix. This one line prevents most scope creep.
Verification
Specify the commands and the expected result. Running tests before the change matters: if a test was already failing, you want to know before the agent "fixes" it.
Characterize before you change
If the code you are refactoring has weak test coverage, the first step is not the refactor. It is pinning current behavior.
Step 1 only. Do not change production code.
Write characterization tests for buildInvoice in packages/billing/src/invoice.ts
that capture current outputs for: domestic order, EU order with VAT, tax-exempt
customer, zero-quantity line, and a discount that exceeds the subtotal.
Assert on the full output object. If current behavior looks like a bug, still
assert it and add a TODO comment naming the case.
Run the tests and confirm they pass against the current code.
Agents are good at writing these tests, and the instruction "assert current behavior even if it looks wrong" is important. Without it, the agent will "fix" what it sees as bugs while writing tests, and you lose the baseline.
Break large refactors into safe steps
A big refactor done in one pass is hard to review and hard to roll back. Ask for a sequence of steps where each step compiles, passes tests, and could be merged alone. Common patterns:
| Refactor | Safe sequence |
|---|---|
| Rename a widely used function | Add new name as alias, migrate callers in batches, remove old name |
| Change a function signature | Add new function, adapt old one to call it, migrate callers, delete old |
| Move a module | Create new location, re-export from old, migrate imports, remove re-export |
| Replace a library | Wrap current usage behind an interface, swap implementation behind it, remove wrapper if desired |
| Split a large file | Move one cohesive group at a time, re-exporting from the original |
Prompt for one step at a time:
We are renaming getUserById to findUserById across the repo in four steps.
This is step 2 of 4: migrate callers in apps/api only. The alias already exists.
Do not touch other directories. Run the api tests and typecheck.
List every call site you changed with file:line.
Commit after each step. If step 3 goes wrong, you revert one commit, not a day of work. The same principles apply at larger scale in safe refactoring in large codebases.
Make the agent show its blast radius
Before approving edits, ask for the list of affected places and check it yourself:
Before editing, list:
1. Every direct caller of buildInvoice (file:line).
2. Every module that imports packages/billing/src/invoice.ts.
3. The tests that exercise those callers.
4. Any indirect use you can find (registries, string references, reflection).
Compare the list to what you know. If the agent is working from text search alone, item 4 is where gaps appear. An MCP code graph server with call tracing and dependent queries makes items 1 and 2 reliable and leaves you to think about item 4.
Constraints worth stating explicitly
Agents will not infer these unless you say them:
- No formatting-only changes outside the lines you are refactoring. They bloat the diff.
- No dependency changes unless that is the task.
- Keep public exports stable or list the exact changes allowed.
- Do not modify tests to make them pass. If an existing test fails after the refactor, stop and report. That test is telling you behavior changed.
- Preserve comments and docstrings unless they become wrong, in which case update them.
- Match local style, even if the agent prefers another.
Several of these belong in your repository's instruction file rather than every prompt. See writing AGENTS.md and CLAUDE.md.
Verification the agent must report
Ask for a short structured report at the end of every step:
Report format:
- Tests before: <command> -> <pass/fail counts>
- Tests after: <command> -> <pass/fail counts>
- Typecheck: <pass/fail>
- Files changed: <list>
- Call sites updated: <file:line list>
- Behavior changes: <none, or list>
- Out-of-scope findings: <list, not fixed>
Then check it. Run the tests yourself or in CI. Spot-check two or three call sites. Read the diff with the invariants in mind: is there any line that could change output for some input?
Reviewing a refactor diff
A refactor review is different from a feature review. You are hunting for unintended change, not evaluating new behavior.
- Look for changed conditionals, reordered operations, and replaced loops. These are where semantics shift.
- Check type changes, especially widened or narrowed types and nullability.
- Look for deleted code that was not moved. It might have handled a case.
- Confirm tests were added or kept, not loosened.
- Check that error handling paths survived the move.
A worked example: splitting a god file
Suppose orders/service.ts is 2,400 lines with order creation, payment, fulfillment, and notifications mixed together.
- Investigate. Ask the agent to group the functions by responsibility and list, for each group, its external callers and internal dependencies. Review the grouping.
- Characterize. Add tests around the public functions most used by callers, if coverage is thin.
- Move notifications first. It has the fewest inbound dependencies. Create
orders/notifications.ts, move the group, re-export fromservice.ts. Tests pass, commit. - Repeat for fulfillment and payment, one group per step, each with its own commit.
- Migrate imports. Update callers to import from the new modules, in batches by directory.
- Remove re-exports once nothing uses them, verified by listing dependents.
Each step is small enough to review in minutes. The agent does the mechanical work; you make the grouping decision and approve each move.
Where OpenVisio fits
The hardest part of prompting a refactor is the blast radius section, because it requires knowing what depends on the code. OpenVisio builds a code graph locally and exposes it to agents over MCP, with trace_calls for callers, get_dependents for importing modules, and get_hotspots for spotting the fragile, high-churn files that deserve extra care. You can also check the same picture yourself in Explore before approving a plan.
Agents can do a lot of refactoring work well. The prompt's job is to make "preserve behavior" concrete enough that the agent can check it, and small enough that you can.

