Skip to content

Maintenance Prompts

Maintenance work has an asymmetry that new-feature work doesn't: the correct behavior already exists, and the agent's job is to change everything except that. Agents are bad at this by default — they treat existing code as a draft to improve, not a contract to preserve. Every pattern here exists to invert that instinct: pin behavior first, then allow change.

Refactoring prompts

Three properties, all mandatory: behavior-preserving (stated as a hard rule, not implied), test-gated (the tests that prove preservation are identified — or written — before the refactor), and incremental (steps small enough that a bad one is cheap to reject).

Weak:

text
Clean up src/services/billing.ts, it's gotten messy.

"Clean up" licenses everything: renamed exports, "improved" error messages that break log-based alerting, a changed function signature because the new one is "more idiomatic", and a drive-by fix to a "bug" that three callers depend on. The diff will be 600 lines, mostly defensible in isolation, unreviewable as a whole.

Strong:

text
Refactor src/services/billing.ts (823 lines) to extract the
proration logic into src/services/proration.ts.

Hard rules:
- Behavior-preserving. Every public export of billing.ts keeps
  its exact name, signature, and behavior — including thrown
  error types and messages (our alerting matches on them).
- If you find what looks like a bug, do NOT fix it. Add it to
  a "Suspected bugs" list in your summary and preserve it.

Process — three separate steps, stop after each for my review:
1. Characterization first: run the existing tests for billing
   (test/services/billing.test.ts). List which public behaviors
   have no coverage. Add characterization tests for those —
   tests that pin CURRENT behavior, bugs included.
2. Extract proration functions to proration.ts; billing.ts
   re-exports so no caller changes. Tests must pass unmodified.
3. Update the 4 call sites to import from proration.ts directly;
   remove the re-exports.

Constraints: no renaming beyond the file move, no formatting
changes to untouched code, no new dependencies.
Acceptance: full suite green after every step, zero test files
modified in steps 2–3.

"Zero test files modified" is the sharpest line in the prompt: a refactor that needs test edits is either not behavior-preserving or the tests were coupled to internals — both are findings, not obstacles. And the "don't fix bugs" rule is not pedantry: fixing a bug during a refactor destroys your ability to attribute any behavior change to the refactor, which is the one guarantee the whole exercise exists to provide.

For refactors spanning many files, the same pattern scales up through the legacy refactor playbook.

Debugging prompts

The agent failure mode in debugging is fix-first: pattern-match the symptom to a plausible cause, patch it, declare victory. The patch often shifts the symptom rather than removing the cause — or silences it, which is worse. Force the evidence chain: reproduce → hypothesize → instrument → fix, and forbid fixing before reproducing.

Weak:

text
Users are reporting that invoice totals are sometimes wrong.
Find and fix the bug.

"Sometimes wrong" plus permission to fix immediately yields a Math.round() sprinkled somewhere in the total calculation and a confident explanation about floating point. Maybe that's the bug. You have no way to know, because nothing was reproduced before or after.

Strong:

text
Bug: invoice totals occasionally off by small amounts.
Evidence so far: 3 support tickets, all invoices with a
percentage discount AND multiple line items. Example: invoice
INV-20260412-091 shows 1,247.03; customer computes 1,247.04.

Follow this order. Do not write a fix until step 3 is done.

1. REPRODUCE: write a failing test that reproduces the discrepancy
   using INV-20260412-091's data (fixture: run
   scripts/dump-invoice.ts 20260412-091). If you cannot reproduce,
   STOP and report what you tried — do not proceed to a fix.
2. HYPOTHESIZE: list up to 3 candidate causes ranked by likelihood,
   each with the file/line you suspect and what evidence would
   confirm or kill it.
3. INSTRUMENT: confirm the top hypothesis with evidence — targeted
   assertions or debug output in the failing test, not guesswork.
   State which hypothesis survived and why the others died.
4. FIX: minimal change. The step-1 test flips to green; the full
   suite stays green. Then check: does the same root cause exist
   anywhere else? (grep for the pattern, list occurrences,
   do not fix them in this pass.)

Constraints: no refactoring around the bug, no defensive
try/catch that would hide recurrence, no changes outside the
computation path you proved is at fault.

The STOP clause in step 1 is the load-bearing part. An agent that can't reproduce the bug but is allowed to continue will fix a bug — some adjacent plausible issue — and report success. Non-reproduction is a result; treat it as one. Full incident workflow: Production Debugging.

Migration prompts

Framework and database migrations fail in a specific way with agents: asked to "migrate X to Y", the agent starts converting file 1 of 200, drifts across inconsistencies, invents different solutions to the same problem in file 40 and file 140, and runs out of context mid-way, leaving the repo in a two-dialect state. The counter-pattern is inventory → strategy → incremental slices, three separate sessions.

text
Session 1 — inventory only, no changes:
Inventory every usage of moment.js in this repo. Output
docs/research/moment-inventory.md: call sites grouped by usage
pattern (formatting / parsing / TZ math / durations), count per
pattern, the 5 hardest cases with file:line, anything that
smells like it depends on moment quirks (e.g. mutable dates).
text
Session 2 — strategy, no changes:
Read docs/research/moment-inventory.md. Propose a migration
strategy to date-fns: pattern-by-pattern mapping table, the
order of slices (lowest-risk first), what the codemod can do
mechanically vs. what needs hand-review, and the rollback story
per slice. Wait for approval.
text
Session 3..N — one slice per session:
Execute slice 2 of docs/specs/moment-migration.md: the
formatting call sites in src/components/ (37 files per the
inventory). Mechanical conversion per the approved mapping
table only — if a call site doesn't fit the table, skip it and
list it. Tests green, no behavior change, no other slices.

One slice per session is deliberate context-reset discipline: each session starts from the strategy doc, not from a degraded window (Control Techniques). The full playbook, including DB-specific staging, is at Migration.

What vague maintenance prompts cost

Vague promptWhat actually happensThe missing line
"Clean up this file"Public API renamed "for consistency"; three callers break at runtime"Every public export keeps its exact name and signature"
"Fix the failing test"Test deleted, or assertion loosened to match the buggy output"Fix the code under test; the test's assertions are the spec"
"Fix this bug"Plausible-cause patch; symptom moves elsewhere; no repro exists"Write a failing test that reproduces it first; STOP if you can't"
"Modernize this module"New abstraction layer nobody asked for; 900-line diffInventory → strategy → approved slices
"Migrate to <framework>"60% converted, context exhausted, two dialects in mainOne slice per session, each independently green
"Improve error handling"Blanket try/catch; errors your alerting matched on are now swallowed"Thrown error types and messages are part of the contract"

A field manual for AI-native software engineering.