Agent Rules / Agent workflow
Why is find-and-replace a bad rename for coding agents?
Rule AR-28
Rename through the language server's rename, not text replace.
A text replace changes every spelling of a name. A language-server rename changes every reference to one symbol. When two things share a name, or a name is imported under another, the two differ, and the difference is the bug. Ask the server for the edit, apply the edit it returns, then check the result.
Why it matters
Two modules each define total:
# billing/cart.py
def total(items): ...
# billing/report.py
def total(rows): ... # unrelated
# billing/checkout.py
from billing.cart import total
from billing.report import total as report_total
...
charge(total(cart.items))The task: rename cart.total to cart_total. sed -i 's/\btotal\b/cart_total/g' renames the report's function too, rewrites local variables named total in unrelated functions, and edits a docstring that says "total of the order". Then from billing.report import total as report_total breaks, because total no longer exists in report.py.
A rename request is scoped by symbol, not by spelling. The server resolves the name at the cursor to one definition and returns edits for the references it can resolve to that definition.
What the protocol and the servers give you
- LSP. The Language Server Protocol (LSP 3.17) defines
textDocument/renameas a request "sent from the client to the server to ask the server to compute a workspace change so that the client can perform a workspace-wide rename of a symbol." The result is aWorkspaceEditornull, andnullmeans no change was required.textDocument/prepareRename(added in LSP 3.12.0) lets the client ask whether a rename is valid at a position first;nullthere means a rename is not valid at that position. - Pyright. Its documented language server features include "Find References to find all references to a symbol within a code base" and "Rename Symbol to rename all references to a symbol within a code base".
- TypeScript. The language service runs behind
tsserver, whose protocol hasrenameandreferencescommands ("Rename request; value of command field is "rename"" and "Find references request; value of command field is "references"" inprotocol.ts).
What the sources do not say is that a rename reaches every spelling of the name. The server returns the edits for what it resolves. Strings, comments, config files, and dynamic access are outside what a symbol-level rename knows about (see Edge cases).
The steps
- Check a rename is valid here. In an editor or an LSP client:
textDocument/prepareRenameat the symbol, if the server supports it. Anullanswer means don't rename from this position. - Request the rename (
textDocument/renamewith the new name). Read theWorkspaceEdit: count files and edits. - Apply the edit it returns, as one change, not file by file in your own words.
- Search for the old name as text afterwards. What remains is what the server did not rename: ``
bash rg -n -w "total" billing/ tests/ docs/ config/ - Decide each leftover: string key, comment, docs, config, an unrelated symbol that happened to share the name, or a missed reference. Fix or explain every one.
- Run the type checker and tests (AR-29). ``
bash pyright && pytest -q npx tsc --noEmit && npm test - Report the leftovers that you left on purpose (AR-30).
If no language server is available for the file type, a text rename is the fallback. In that case say it was text, give the search you used, and treat the whole rename as unverified until the checker and tests pass.
Worked example
Illustrative session, not a captured run:
task: rename billing.cart.total -> cart_total
prepareRename at cart.py:3 -> range "total", placeholder "total"
rename "cart_total" -> WorkspaceEdit: 3 files, 4 edits (cart.py, checkout.py x2, tests/test_cart.py)
applied as one change
rg -n -w "total" billing/ tests/ docs/
billing/report.py:5 def total(rows) -> different symbol, correct to leave
docs/cart.md:14 `total(items)` -> docs, update
billing/export.py:61 COLUMNS["total"] -> string key into a table, read: refers to report total, leave
pyright: 0 errors pytest: 41 passed
note for report: export.py string key checked by reading, not by testThe sed version would have produced the same first line and none of the rest.
Rule block for AGENTS.md / CLAUDE.md
## Renames
- Rename symbols with the language server (LSP `textDocument/rename`, or the editor's Rename Symbol).
Do not use find-and-replace, sed, or a regex across files.
- After the rename, search the old name as text (`rg -n -w <old>`) and account for every remaining hit:
fix it, or write down why it stays.
- Check string keys, config files, docs and tests for the old name; the server does not rename those.
- Run the type checker and the tests after a rename.
- If no language server is available, say the rename was text-based and list the search you used.Edge cases
- Strings, comments, docs. A symbol-level rename isn't expected to touch them. Search for them afterwards. The LSP rename request returns a
WorkspaceEditcomputed by the server, so what it covers depends on the server. After a rename, run a text search for the old name to find leftovers in comments, strings and docs. - Dynamic access.
getattr(obj, "total"),obj["total"], serialised field names, and ORM column names are strings. Find them with text search and update by hand (AR-07). - Public names used outside the repo. Other packages and consumers aren't in the server's workspace. Plan a deprecation path, not a rename.
- Files outside the server's project. Excluded directories and untyped files may not be in the edit. Read the file count in the
WorkspaceEditagainst what you expected. - Renaming a file or module is a separate operation, since imports and package paths change. Move it with the editor's refactor or
git mv, then run step 4 on the old module name. - Partially typed Python. Where a value's type is unknown, the server may not resolve an attribute access to the symbol and the rename can skip it. Step 4 is how you find those.
References
- LSP 3.17 specification, Rename Request and Prepare Rename Request (
WorkspaceEdit | null,prepareRenameadded in LSP 3.12.0): https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ - Pyright features, Language Server Support: https://github.com/microsoft/pyright/blob/main/docs/features.md
- TypeScript server protocol (
CommandTypes.Rename,CommandTypes.References,RenameRequest,ReferencesRequest): https://github.com/microsoft/TypeScript/blob/v5.6.3/src/server/protocol.ts - TypeScript standalone server (tsserver): https://github.com/microsoft/TypeScript/wiki/Standalone-Server-(tsserver) (page links to
ts.server.protocol.CommandTypesfor the command list) - Pyright command line: https://github.com/microsoft/pyright/blob/main/docs/command-line.md
Changelog
- 2026-10-30: draft 1.
- 2026-10-30: remaining checks closed (claims reduced to documented behaviour)
Related
- AR-25 What to check before changing a function signature?
- AR-26 How does an agent know an edit is finished?
- AR-27 Is a list of callers ever the full list?
- AR-31 Why does my coding agent miss Django signal receivers?
Last verified