Ask Cursor, Claude Code or ChatGPT to “clean this up” or “refactor this,” and you’ll usually get something: new folders, renamed symbols, a fashionable pattern, maybe a dependency bump. Sometimes it’s even an improvement.
Often it’s an unreviewable rewrite. The diff mixes structure with silent behaviour changes. Tests go green because they now assert the new world, not because yesterday’s behaviour was locked. Reviewers approve the vibe and miss the logic edit in the noise.
AI isn’t bad at refactoring. “Clean this up” just skips every step that makes structural change safe and asks for a finished architecture in one shot. Here’s a loop I use instead — and two full prompts you can paste today.
The loop: map → safety net → small slice → prove → describe
map → safety net → small slice → prove → describe
- Map — inventory concrete smells with locations; pick one PR-sized first slice; write what’s out of scope.
- Safety net — characterization (or existing) tests that pin today’s behaviour before you move code.
- Small slice — one extract, one responsibility split, one rename batch — not a redesign.
- Prove — same tests green after; public contracts unchanged unless this PR is explicitly a migration.
- Describe — PR text that states the behaviour contract, reading order, and residual risk.
If you only remember one rule: don’t let the model edit production code until something green locks the behaviour you’re about to move.
Free prompt 1: smell map before any edit
Paste this when you’re staring at a messy module and tempted to say “rewrite it.”
You are a senior engineer planning a safe refactor. Do not change any code yet.
## Context
- Paths in scope: {{TARGET_PATHS}}
- Goal (one sentence): {{GOALS}}
- Hard constraints (API freeze, no new deps, deadline): {{CONSTRAINTS}}
- Time / PR budget for the first pass: {{TIME_BUDGET}}
- What I know about tests here: {{TEST_COVERAGE_NOTES}}
## Process
1. Inventory concrete smells with file:line or a short quote. Cover different
categories (long function, duplication, mixed responsibilities, dead code,
leaky abstraction, hidden side effects). Cap at 12.
2. Score each: payoff (H/M/L), risk (H/M/L), test safety (none/thin/adequate),
size (S/M/L).
3. Rank by (payoff ÷ risk), then size. Prefer high-payoff / low-risk / S first.
4. Propose ONE first slice: done-when, smells included, safety net required
before edits, explicit out-of-scope, suggested PR title.
5. Stop. No edits. No rewrite proposal.
## Output
### Smell inventory (table)
### Ranking
### First slice
### Open questions (max 5)
## Rules
- Every smell needs a location. "This is messy" is not a smell.
- Do not invent coverage % or hours saved. Unknown → say unknown, raise risk.
- Prefer surgery over rewrite.
Two details matter. Locations stop vibes-based cleanup. Out of scope on the first slice stops the model from boiling the ocean in the same breath as the plan.
Free prompt 2: characterization before the extract
Once you’ve picked a slice, pin behaviour before moving code:
You are building a characterization safety net before a refactor.
Do not change production code.
## Context
- Code under edit: {{TARGET_CODE}}
- Behaviours that must stay the same: {{PUBLIC_BEHAVIOURS}}
- Known quirks / edge cases: {{KNOWN_EDGE_CASES}}
- Existing tests: {{EXISTING_TESTS}}
- Test command: {{TEST_COMMAND}}
## Process
1. List observable contracts (returns, errors, side effects, ordering).
Mark each MUST-PRESERVE or ALLOWED-TO-IMPROVE.
2. Gap-analyze against existing tests.
3. Add the smallest tests that pin current behaviour — including quirks.
If something looks like a bug, still characterize it and label the test
CHARACTERIZATION. Do not "fix" it in this step.
4. Run {{TEST_COMMAND}} on the CURRENT code. All new tests must pass before
any structural edit.
## Rules
- No production edits.
- Do not weaken or delete existing assertions.
- Prefer outputs/side effects over mock call-order tests.
- If you can't run tests here, label them UNVERIFIED and give exact commands.
Characterization is deliberately boring. That’s the point. You’re buying a tripwire for the extract you’re about to do.
Then make the slice boring too
When you finally extract or split:
- Quote the exact chunk, its inputs/outputs/side effects, then move it in steps: add new function → switch caller → delete old body.
- Ban drive-by churn in writing: no whole-file reformat, no unrelated renames, no dependency bumps in the same PR.
- Keep public signatures stable unless this PR is the migration (then use expand/contract: new alongside old → migrate callers in batches → remove old).
A useful interrupt when the agent starts redesigning:
List every file change not required by the approved slice. Revert those.
Did any user-visible behaviour, wire format, error code or log field change?
Yes/No per item. If Yes, split it out of this refactor PR.
Renames: expand/contract, not flag day
Repo-wide renames are where AI is both strongest and most dangerous. A single replace-all feels productive and produces a diff nobody can review — and it misses the dynamic call site in another package, the config key, or the old mobile client.
Prefer expand/contract:
- Introduce the new name alongside the old (shim if you need a public export).
- Migrate callers in directory-sized batches, each leaving CI green.
- Update docs and generated clients.
- Remove the old name only when inventory search returns zero.
Ask the model for an inventory table first (definition / internal / public / tests / config / generated), then execute step one only in that session. Paste remaining counts into the next session. That handoff beats one mega-chat that “just finishes it.”
A miniature walkthrough (illustrative)
Fictional and simplified — no real metrics.
You have a 1,200-line PricingService that mixes list price, discounts, tax, FX and invoice formatting. Constraint: keep price(cart) for one release.
-
Map: extract tax ranks high payoff / medium risk; deleting a commented
legacyCouponPathis tempting but secondary. - Net: two characterization tests lock half-unit rounding and “unknown country” errors on current code.
-
Slice: new
TaxCalculator;pricedelegates; signature unchanged; suite still green. - Describe: PR reading order = characterization tests → new type → delegating methods → unchanged exports. Dead-code delete waits for a later HIGH-confidence pass.
Nothing magical happened. The prompts kept the first change small enough that a human could still own the merge.
Know when not to start
Skip AI-led cleanup when you can’t state the goal in one sentence, when there’s no time for a safety net, during a freeze/incident, or when the real need is a bug fix smuggled as “refactor.” Stop mid-session when the diff grows past the slice, when tests are “fixed” by deleting assertions, or when you couldn’t explain the change yourself.
Also park the work when you don’t own the module and can’t get a reviewer who does, or when the model’s first suggestion is a rewrite. Return to the smell map and pick a leaf extraction instead.
The shape that works
| Part | Purpose |
|---|---|
| Role | “Senior engineer planning/performing a safe refactor…” |
| Inputs | Paths, goal, constraints, real test command |
| Process | Map → net → slice; plan before wide edits |
| Output contract | Tables for smells, confidence grades, reading order |
| Rules | No drive-bys, no fake metrics, no test vandalism |
Try it on a cleanup you already finished: give the model only the original messy files and constraints, run the smell-map prompt, and see whether the first slice is something you’d still approve — with a safety net called out before edits.
I packaged the eight refactoring prompts I use (smell map, characterization net, extract, god-class split, rename/migration, dead-code cleanup, strangler-fig, PR guide) plus two checklists, a Cursor rule and a PDF guide, as the **AI Refactoring Prompt Pack: https://lirenplr.gumroad.com/l/ypcsuc. Everything above works without it.
— Pluto · @Mars_Peakul
Top comments (0)