# Refactoring strategies in Cursor

URL: https://developertoolkit.ai/en/cursor-ide/build/refactoring-strategies/
Description: Split, rename and restructure tested code with Cursor's agent one small step at a time, with gates that prove behaviour did not change.

A 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](/en/cursor-ide/build/legacy-refactor/), which builds the safety net first. For framework or library swaps, see the [framework migration workflow](/en/cursor-ide/build/migration/).

## 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?

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](/en/workflows/test/protect-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

1. **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`.

2. **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 stryker` downloads the deprecated 0.x `stryker` package: `npm i -D @stryker-mutator/core`, then `npx stryker init` to 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](/en/workflows/test/oracle-strength/) explains how to read the report.

3. **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 unscoped `api-extractor` name on npm is a placeholder, not this tool). It reads built declarations, so emit them first with your build or `npx tsc --declaration --emitDeclarationOnly`, then run `npx api-extractor init` and point `mainEntryPointFilePath` in `api-extractor.json` at the entry `.d.ts`. `npx api-extractor run --local` writes an `.api.md` report of every export and signature, in `etc/` by default. Commit it. For an application, the equivalent is your contract or route tests.

4. **Write the refactor rule.** Add a [project rule](/en/cursor-ide/quick-start/project-rules/) 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:

   ```text
   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.
   ```

5. **Ask for the move list, not the code.** Use the first prompt below in [Plan Mode](/en/cursor-ide/build/agent-modes-deep-dive/), which produces a plan before writing any code. You review the plan; the plan becomes the spec for the rest of the session.

**Copy-paste prompt: refactor plan with a verification step per move**

```text
Read @src/services/user-service.ts and every file that imports from it. Do not edit anything yet.

Produce a refactoring plan that splits the file into src/services/user/{authentication,profile,notifications,account-deletion}.ts
while src/services/user-service.ts keeps re-exporting the same public API.

For each step give:
1. The single structural move (for example "move login, logout, refreshToken, validateToken to authentication.ts").
2. The files that change, including every import site.
3. The verification command: npx tsc --noEmit && npm test.
4. Any behaviour risk you see: module-level state, import-order side effects, circular imports, functions that share a private helper.

Order the steps so the riskiest move comes last. List any exported symbol that no file imports, but do not delete it.
Ask me up to three questions if the grouping is ambiguous.
```

## 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:

1. The agent makes one move from the plan and updates the imports it affects.
2. The agent runs `npx tsc --noEmit && npm test` and reports the result.
3. 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.
4. 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](/en/cursor-ide/build/checkpoints-branching/) covers how the two layers fit together.

**Copy-paste prompt: execute one step of the approved plan**

```text
Execute step 1 of the plan only: move login, logout, refreshToken and validateToken from
@src/services/user-service.ts to src/services/user/authentication.ts.

Rules:
- Move the code verbatim. Do not rename, reformat, or change error handling.
- Re-export the four functions from src/services/user-service.ts so existing importers keep working.
- Do not touch any *.test.ts or *.spec.ts file.
- Run: npx tsc --noEmit && npm test

Then report: files changed, the command output summary, and anything in the moved code that depends
on state still living in user-service.ts. Stop after this step and wait for me.
```

## 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](/en/workflows/test/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.

**Copy-paste prompt: positional parameters to an options object**

```text
Refactor createUser in @src/services/user/account.ts from
createUser(email, password, firstName, lastName, role, teamId)
to
createUser({ email, password, firstName, lastName, role, teamId }: CreateUserInput).

1. Find every call site with a search, list them with file and line, and tell me the count before editing.
2. Update the call sites in batches of at most five files. After each batch run: npx tsc --noEmit && npm test
3. For each call site, map arguments by position from the OLD signature. If any call passes a variable whose
   name does not match the parameter it lands in (for example lastName passed as firstName), stop and show me.
4. Do not edit test files. If a test calls createUser positionally, list it; I will update tests in a separate commit.
```

**Copy-paste prompt: callbacks to async/await without behaviour drift**

```text
Convert @src/services/data-service.ts from (err, result) callbacks to async/await.

Keep behaviour identical:
- Every callback error path becomes a thrown error of the same type and message. No catch block may swallow an error.
- Keep the order of side effects (writes, logs, emitted events) exactly as it is today.
- Exported functions keep their names. If an exported function must change from callback to Promise,
  keep a callback-compatible wrapper and list it for later removal.
- Do not edit tests.

After the change run: npx tsc --noEmit && npm test && npx stryker run --mutate "src/services/data-service.ts"
Report the mutation score next to the baseline of 81% and list any surviving mutant in an error path.
```

Replace the 81% baseline with the score you recorded in the contract step.

## 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](/en/cursor-ide/build/checkpoints-branching/) 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

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.

**Copy-paste prompt: pre-PR evidence summary**

```text
The refactor is complete. Run each command below and paste the real output summary, not a description:

1. git diff --name-only refactor-base -- '*.test.ts' '*.spec.ts'    (must print nothing)
2. npx tsc --noEmit
3. npm run lint
4. npm test
5. npx api-extractor run --local && git diff --stat -- '*.api.md'   (must show no change)
6. npx knip --exports --cycles
7. npx stryker run --mutate "src/services/user-service.ts,src/services/user/**/*.ts"

Then write a PR description with: the plan's steps and their commits, a table of the gate results,
the mutation score against the baseline, and a list of anything that is NOT a pure move
(for example a re-export wrapper kept for compatibility). If any gate fails, fix it and rerun
from gate 1. Do not edit tests to make a gate pass.
```

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](/en/workflows/ship/evidence-bundle/). If your team uses [Bugbot](/en/cursor-ide/ship/bugbot/), it adds an automated review pass on the PR; it complements the gates and does not replace them. The [Cursor code review workflow](/en/cursor-ide/ship/code-review/) 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

**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](/en/workflows/operate/codebase-health/).

**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

- [Refactoring legacy code in Cursor](/en/cursor-ide/build/legacy-refactor/) — when the code has no tests to freeze yet
- [Testing patterns in Cursor](/en/cursor-ide/test/testing-patterns/) — build the suite this workflow depends on
- [Large codebase strategies](/en/cursor-ide/build/large-codebase-strategies/) — scoping prompts and context when the refactor spans a big repository
- [Refactoring with Claude Code](/en/claude-code/build/refactoring/) and [refactoring with Codex](/en/codex/build/refactoring/) — the same contract in the other tools
