Refactoring strategies in Cursor
PLA safe refactoring strategy in Cursor turns one large restructuring into a sequence of small, behaviour-preserving moves, each committed only after the unchanged test suite, the type checker and a public-API check pass. The agent does the edits; frozen tests, an API report and mutation testing prove nothing changed, so review reads evidence instead of every line.
This page is for developers and tech leads who own code that already has tests. Your user-service.ts has grown to 800 lines covering authentication, profiles, notification preferences and account deletion. Every change collides with someone else’s, and the last big split broke three downstream services and took a week to stabilise. You want the agent to do the mechanical work without a second week like that.
For code without tests, start with refactoring legacy code in Cursor, which builds the safety net first. For framework or library swaps, see the framework migration workflow.
What you get from agent-driven refactoring in Cursor
Section titled “What you get from agent-driven refactoring in Cursor”- A refactor contract: five gates, with exact commands, that define “behaviour did not change”
- A one-move-per-commit loop the agent follows, with prompts you can paste as-is
- A decision table for the common refactoring patterns: which to hand the agent, which to give a codemod, and the extra gate each one needs
- An evidence summary a reviewer can sign off without reading the whole diff
- Recovery steps for the ways agent refactors go wrong, starting with the agent editing tests to get green
What makes an agent refactor safe?
Section titled “What makes an agent refactor safe?”A refactor changes structure and keeps behaviour. That definition gives you the test: if the same tests pass against the new structure and the public surface is identical, behaviour held. The danger with an agent is that it can change either side of that equation. It can edit a test until it passes, or “tidy” an error path while it moves code. So the contract fixes what the agent may not touch before the first edit.
| Gate | What it proves | Command (TypeScript example) |
|---|---|---|
| Tests frozen | The oracle did not move | git diff --name-only refactor-base -- '*.test.ts' '*.spec.ts' prints nothing |
| Suite green | Behaviour the tests pin is intact | npm test |
| Types and lint | No broken call site or dead import | npx tsc --noEmit and npm run lint |
| Public API unchanged | Consumers see the same exports and signatures | npx api-extractor run --local, then git diff on the report in etc/ |
| Oracle strong enough | The tests would notice a behaviour change | npx stryker run --mutate "src/services/user-service.ts,src/services/user/**/*.ts", score not lower than baseline |
The first gate matters most. A refactor that also edits tests has no independent check left, which is the argument behind protecting the oracle. If a test genuinely has to change, for example because it imported a private helper by path, make that change in its own commit, before the refactor, and review it as a human. Tag the baseline only after that commit (step 1 below), so the frozen-tests gate compares against the approved tests and can pass.
Set up the refactor contract before the first edit
Section titled “Set up the refactor contract before the first edit”-
Get a green baseline. Run the whole suite on a clean branch and commit. A refactor that starts red cannot prove anything. Commit any approved test-only change next, then mark the point every gate compares against:
git tag refactor-base. -
Check that the tests would catch a change. Line coverage says a line ran, not that a test would fail if it changed. Install the real package first, because a bare
npx strykerdownloads the deprecated 0.xstrykerpackage:npm i -D @stryker-mutator/core, thennpx stryker initto write the config and pick your test runner (checked against 10.0.0 on 2026-09-30). Run it on the code you are about to move, with the same glob the final gate uses so both scores cover the same code:npx stryker run --mutate "src/services/user-service.ts,src/services/user/**/*.ts". Record the mutation score and the killed and survived counts. Surviving mutants in the code you are moving are the gaps; close them with characterization tests before refactoring. Oracle strength explains how to read the report. -
Snapshot the public surface. For a package with one entry point, install
npm i -D @microsoft/api-extractor(7.59.3 on 2026-09-30; the unscopedapi-extractorname on npm is a placeholder, not this tool). It reads built declarations, so emit them first with your build ornpx tsc --declaration --emitDeclarationOnly, then runnpx api-extractor initand pointmainEntryPointFilePathinapi-extractor.jsonat the entry.d.ts.npx api-extractor run --localwrites an.api.mdreport of every export and signature, inetc/by default. Commit it. For an application, the equivalent is your contract or route tests. -
Write the refactor rule. Add a project rule for the duration of the refactor, so every agent session inherits the same limits. Create it as a project rule (see the linked page for where Cursor stores it) and delete it when the refactor merges:
During the user-service refactor:- Never edit files matching *.test.ts or *.spec.ts. If a test fails, stop and report it.- One structural move per step. No renames, formatting or "cleanups" in the same step.- Keep src/services/user-service.ts exporting everything it exports today (re-export).- After each step run: npx tsc --noEmit && npm test. Report the result before continuing. -
Ask for the move list, not the code. Use the first prompt below in Plan Mode, which produces a plan before writing any code. You review the plan; the plan becomes the spec for the rest of the session.
Run the refactor one move per commit
Section titled “Run the refactor one move per commit”With the plan approved, switch to Agent and execute it step by step. The loop is deliberately boring:
- The agent makes one move from the plan and updates the imports it affects.
- The agent runs
npx tsc --noEmit && npm testand reports the result. - You glance at
git diff --stat. The file list should match the plan’s list for that step. More files means the step was too big. - Commit with the step number in the message. A bad step 7 of 10 now costs you one
git revert, not the whole refactor.
Cursor’s checkpoints are local snapshots of the agent’s changes; commits are the record that survives the session and the one a reviewer sees. Checkpoints and branching covers how the two layers fit together.
Which refactoring pattern should the agent do?
Section titled “Which refactoring pattern should the agent do?”Not every refactor is agent work. A purely mechanical change across 200 files is better done by a tool that cannot be creative. The agent earns its place where the change needs judgement: grouping functions, rewriting control flow, updating call sites that do not all look alike.
| Pattern | Who does the edit | Main behaviour risk | Extra gate beyond the contract |
|---|---|---|---|
| Rename a symbol | The editor’s Rename Symbol, or a codemod; the agent only for string references (routes, docs, log keys) | Dynamic lookups by name | git grep -n oldName returns only intentional hits |
| Extract module | Agent, one group of functions per step | Shared module-level state, import-order side effects | npx knip --exports --cycles shows no new cycle or orphaned export |
| Change a signature (positional to options object) | Agent for call sites, or ts-morph for a very large count | Swapped arguments that still type-check | Type check plus one test per changed call site’s branch |
Callback to async/await | Agent, one module per step | Errors swallowed or thrown twice, changed ordering | Mutation score on the module does not drop |
| Move code between layers | Agent after a plan | Layer rules broken quietly | A dependency-cruiser rule, see fitness functions |
The signature change is where type-checked code fools you: createUser(email, password, firstName, lastName, role, teamId) with firstName and lastName swapped at one call site still compiles. The options object removes that class of bug going forward, but only if each call site is migrated correctly now.
Replace the 81% baseline with the score you recorded in the contract step.
Compare two refactor designs in parallel worktrees
Section titled “Compare two refactor designs in parallel worktrees”When there are two sensible shapes, for example splitting by feature or splitting by layer, do not argue about them in the abstract. Cursor’s Worktrees let an agent work in an isolated Git checkout, so two agents can each build one design from the same baseline without touching each other’s files. Judge them by the same gates, then by what the diff does to the dependency graph: the version with fewer cross-module imports and no new cycles usually wins. Keep one; delete the other worktree and its branch.
Prove behaviour did not change without reading every line
Section titled “Prove behaviour did not change without reading every line”At the end, a reviewer should be able to approve from evidence and a short spot-check. Ask the agent to assemble it, and to run the gates rather than describe them.
The reviewer then checks three things by hand: the “not a pure move” list, the first commit (does the new structure match the plan?), and one randomly picked step. The rest is covered by gates that would have gone red. For how the summary fits a team’s review policy, see the evidence bundle. If your team uses Bugbot, it adds an automated review pass on the PR; it complements the gates and does not replace them. The Cursor code review workflow covers the wider review layers.
Put the gates in CI as well. A green run locally proves the agent’s machine; the same commands on the pull request prove the branch.
When an agent refactor goes wrong
Section titled “When an agent refactor goes wrong”The agent edits a test to make it pass. The frozen-tests gate prints a file name. Revert that file with git checkout refactor-base -- path/to/file.test.ts, then ask the agent why the test failed. A failing unchanged test means the move changed behaviour; that is the finding, not an obstacle.
One step touches far more files than the plan said. The git diff --stat check catches it. Check git status first, because the next command discards every uncommitted change. Then run git restore . && git clean -fd -- src/ (or git reset --hard <last-step-commit> && git clean -fd): git restore alone leaves new untracked files such as authentication.ts behind, and a stale module can still pass tsc and the tests. Then split the step: “move three functions” instead of “move the authentication group”.
Behaviour changes inside a “cleanup”. Agents like to tidy while they move: a catch that now logs and continues, a default value added “for safety”. Mutation testing is the gate that notices, because a weakened error path produces surviving mutants. Revert the step and repeat it with “move verbatim” in the prompt.
Circular imports appear after an extract. The typical cause is a barrel file (index.ts) that the new modules import from while it also re-exports them. npx knip --cycles names the loop. Import from the concrete module inside the package; keep the barrel only for outside consumers.
Old code stays behind next to the new copy. The agent sometimes copies instead of moving, and both versions pass tests. npx knip --exports flags the orphaned export. At repository scale this is worth watching: GitClear’s June 2026 report “The Maintainability Gap”, seen here through a search extract (secondary and correlational), found moved code falling from 13% to 3.8% of changed lines while copy/paste rose 41%. See keeping an agent-written codebase healthy.
A long session drifts from the plan. After many steps the agent starts combining moves. Start a new chat, paste the plan, and say which step is next; the commits tell it what is done.
Teammates’ branches conflict with the refactor. Announce the refactor, keep each pull request to a few steps, and merge them quickly. Re-exports from the old file keep in-flight branches compiling while they rebase.
Where to go next with refactoring in Cursor
Section titled “Where to go next with refactoring in Cursor”- Refactoring legacy code in Cursor — when the code has no tests to freeze yet
- Testing patterns in Cursor — build the suite this workflow depends on
- Large codebase strategies — scoping prompts and context when the refactor spans a big repository
- Refactoring with Claude Code and refactoring with Codex — the same contract in the other tools