Refactor or upgrade safely
Refactors and dependency upgrades are repetitive and mechanical, which makes them good work for an agent. They are also where an agent working in one checkout does the most damage, because the risk sits in the callers it cannot see. This page covers the whole activity: renames, signature changes, moving and deleting code, upgrading internal and third-party dependencies, extracting shared helpers, staging the work in a plan and checking the running services afterwards.
When you'd do this
- You are renaming a function, class, type or field that other repositories import or call.
- You are changing the signature of an internal API or shared client and need every caller updated in the same piece of work.
- You are splitting a module that has grown too large, or moving code into another package.
- You want to delete a deprecated endpoint, flag path or helper, and need to know whether anything still uses it.
- A shared internal library or a third-party dependency needs a major-version upgrade across several services.
- The same logic has been copied into several repositories and you want one shared helper.
If the change alters behaviour across services, for example a new field in an API response that consumers must read, follow Ship a cross-repository change. This page focuses on changes meant to keep behaviour the same.
Before you start
- CodeZero installed in VS Code with Claude Code, Codex, GitHub Copilot or Cursor as the agent, signed in to your organisation.
- Every repository that might import, call or pin the code you are changing connected and indexed. A consumer in a repository that is not indexed cannot be found.
- A test, lint or build command for each repository you expect to change. These become the plan's verification.
- For runtime checks after release: Vörr connected to Claude, ChatGPT or another MCP assistant, with your repositories, observability tools and CI/CD sources connected. Vörr shows each repository's workspace and branch-to-environment mapping; telemetry and deploys are matched to services by name, with a match label. ChatGPT needs a paid plan with Developer mode turned on.
- Write access to each repository you will change. A person approves the plan; the agent cannot approve its own.
1. Rename a symbol across every repository
References in CodeZero are matched by name, across every indexed repository. That reaches repositories you have not cloned, but it also means a different function with the same name can appear in the results. Separate the two before you plan.
- Ask CodeZero where the symbol is defined. If more than one definition comes back, note which one you mean, by repository and file.
- Ask for every reference to it across the organisation. For each result, check whether it imports from the defining package or module. A same-named function defined locally is not a caller.
- Run a text search for the name too. It catches uses no parser sees: string keys in config, mocks in tests, documentation and generated clients.
- Decide whether to keep a deprecated alias under the old name for a release. If consumers release independently, keep it.
- Describe the rename with /plan, naming the alias decision, and review the repositories the plan lists against your reference list.
Where is getUserById defined across the organisation? If there is more than one definition, list each. Then list every reference to the one in users-lib, and mark any result that does not import it from users-lib./plan Rename getUserById to findUser in users-lib and update every caller in every repository. Keep getUserById as a deprecated alias that calls findUser for one release.A good result separates true callers from same-named symbols, with a file and line for each. Your IDE's rename refactoring is still the right tool inside each repository; CodeZero tells you which repositories need it.
2. Change an internal API signature
- Ask CodeZero for the callers of the function you are changing. Callers are heuristic, not compiler-accurate, so treat the list as the place to start reading.
- Read each call site. Note how many arguments it passes, whether it relies on a default, and whether it passes the function on as a callback, where a signature change is easy to miss.
- Ask which tests cover each caller, so the plan's verification exercises the calls that change.
- Choose the transition: add the new parameter as optional, add an overload, or introduce a new function and deprecate the old one.
- Plan the change with the library first and each consumer after, with each repository's own tests as verification.
List every caller of createInvoice(orderId, currency) across the organisation. For each, show the arguments it passes and whether it is called directly or passed as a callback. Which tests cover each caller?/plan Change createInvoice to take an options object { orderId, currency, dueDate }. Keep the positional form working for one release and update every caller to the new form.A good result lists every call site with its current arguments. The type checker in each repository is your second check: after the change, each repository's build should fail where a caller was missed.
3. Split or move a module
- Open Graph at file level on the module you are splitting. Its incoming imports show who depends on which part, and outgoing imports show what each part needs.
- Group the imports by what they use. That grouping is usually the split: the parts imported together belong together.
- Look for import cycles the split would create, where the new modules would import each other.
- If other repositories import the module through a package, list them. A moved path in a published package is a breaking change for them, so plan a re-export from the old path for a release.
- Plan the move with the new modules first, then the re-export, then the updated imports in each consumer.
Show me everything that imports src/utils/billing.ts in any repository, and which of its exports each importer uses. Suggest how to split it so that each importer needs only one of the new modules./plan Split src/utils/billing.ts into money.ts and tax.ts. Keep billing.ts as a re-export of both for one release and update every importer to the new paths.4. Remove dead code and deprecated paths
Deleting is where a clean search misleads most. Code with no references can still be reached: a route called over HTTP by another service, a handler named in a queue binding, a function looked up by name at runtime. No references is evidence, not proof.
- Ask CodeZero for references and callers of the code you want to delete, across the organisation.
- If it is a route, check the declared routes of the repository and the inferred HTTP edges into it in Graph. Calls in other repositories are matched to the route even though they are URL strings, not references.
- If it is a queue handler or is wired up through config, check the queue and config edges as well.
- Run a text search for the name and the path. Look for reflection, string lookups and feature-flag keys.
- With Vörr, check whether the route or handler still receives traffic in production: throughput or error rate for the operation, or log lines that mention it.
- Delete in a plan, with the repository's tests as verification, and keep the commit small enough to revert.
Is anything in any repository still using the /v1/orders/export route or the exportOrdersCsv function? Check references, callers, HTTP calls matched to the route and text matches. Say where the evidence is inferred.Has GET /v1/orders/export on orders-api received any traffic or logged any requests in production in the last 30 days? Say which source answered and whether anything else could have served it.A good result gives zero references and zero matched calls from indexed repositories, plus a runtime source that shows no traffic. If Vörr reports no connector for traces or logs on that service, you have no runtime evidence either way.
5. Upgrade a shared internal library
- Ask CodeZero which repositories depend on the library. The dependencies view for each repository shows what its manifests declare, including the version or range it pins.
- Build a table: repository, pinned version, and whether it uses the APIs that change in the new version. Repositories pinned several majors behind need their own plan.
- Find the callers of each changed API in each consumer, using the same searches as a rename or signature change.
- Order the work library first: release the library, then upgrade consumers. Consumers that use none of the changed APIs can upgrade in one batch.
- Give each consumer its own verification, and its own pull request.
Which repositories declare a dependency on @acme/http-client, and which version or range does each one pin? For each, say whether it calls retry() or the RetryOptions type, which change in v3./plan Upgrade @acme/http-client to v3 in every repository that depends on it. Phase 1 releases the library with the v2 retry signature still accepted; phase 2 upgrades consumers, grouped by whether they call retry().A good result lists every consumer with its declared version and file references for each changed API. Manifests show the declared version or range; the version actually installed depends on the lockfile, so check that in each repository during the task.
6. Upgrade a third-party dependency
- Open Graph with third-party packages turned on. It shows which modules in which repositories import the package.
- Ask CodeZero for the declared version in each repository's manifests.
- Have the agent fetch the package's changelog or migration guide with its own web tools, and list the breaking changes between your pinned versions and the target.
- For each breaking change, ask CodeZero where the affected API is used. Text search works well here for option names and import paths.
- Plan the upgrade one repository at a time or in groups, with tests that exercise the changed calls.
Which repositories and modules import axios, and which version does each declare? Then fetch the axios migration notes from our oldest pinned version to 1.7 and list the breaking changes.For each breaking change in that list, show me every place in any repository that uses the affected API or option, with file and line.A good result connects each breaking change to the call sites it affects. If the agent cannot fetch the changelog, paste it in; do not let it work from memory of an older version.
7. Extract duplicated logic into a shared helper
- Ask CodeZero for code that does the same job across repositories. Go deep, symbol search and text search all match on names and keywords, so search for several likely names and for distinctive lines, such as a regular expression or a constant.
- Read each copy. Copies drift: note where they differ in behaviour, because those differences are the hard part of the extraction.
- Decide where the helper lives, and whether the differences become options or stay in the callers.
- Plan the helper first, with tests that cover every behaviour the copies had, then replace each copy.
Find code in any repository that formats money amounts with a currency symbol. Search for names like formatMoney, formatCurrency and toPrice, and for Intl.NumberFormat with style currency. Show each copy and how they differ./plan Extract a formatMoney helper into shared-utils, covering every behaviour of the copies you found, then replace each copy with a call to it.Matching on names and keywords finds copies that share vocabulary. A copy written with different names may not appear, so treat the list as a strong start, not a complete inventory.
8. Order the phases for a refactor
A behaviour-preserving refactor has its own phase order. The old name or path keeps working throughout, so every consumer can move on its own schedule and nothing breaks between releases.
- Phase 1, the shared code: add the new name or path, and keep the old one as a deprecated alias or re-export that delegates to it. Both must behave identically.
- Phase 2, the consumers: move each consumer repository to the new name or path, each with its own verification and pull request.
- Phase 3, removal: once every consumer has released, repeat the caller search, then remove the alias or re-export as its own small plan.
In this plan, confirm the deprecated alias ships in the same phase as the new name and only delegates to it, that every consumer phase depends on that phase, and that alias removal is a separate follow-up gated on a fresh caller search.For reviewing the plan's repositories and verification, cloning, and running tasks per repository, see Ship a cross-repository change.
9. Check runtime after rollout with Vörr
- Ask Vörr which services sit behind the repositories you changed, and check the match quality of each mapping.
- After each release, ask for the deploy feed for those services across your CI/CD sources.
- Compare each service's error rate and top errors since its deploy with a baseline window. A refactor that keeps behaviour the same should show no new issues.
- If a service shows a new error, ask for its stack and culprit, and the logs around the first occurrence. A missed caller usually shows up as a TypeError, a missing method or an undefined field.
Show deploys in the last 24 hours for users-api, admin-web and support-tools, then compare their error rate and top errors since each deploy with the same window 7 days ago. Flag any new error that mentions findUser or getUserById.Testing strategy, from unit tests around a signature change to contract tests for consumers, is covered in Write and run tests.
Without CodeZero or Vörr
Claude Code does refactors well on its own when every repository involved is cloned. It can search them all with ripgrep, make the edits, run each test suite and open pull requests with gh, and an IDE's rename refactoring is accurate within a project. With vendor MCP servers or CLIs configured, it can check errors and deploys too. The cost is in knowing what to clone, and in what source references cannot show.
| Operation | Claude Code / ChatGPT on their own | With CodeZero and Vörr |
|---|---|---|
| Rename a symbol | Accurate inside cloned repositories, especially with the IDE's rename. A caller in a repository you did not clone is invisible, and an empty search looks the same as a clean one. | CodeZero finds references by name across every indexed repository, cloned or not. You separate same-named symbols. |
| Change a signature | Type checking catches missed callers in each repository you build. You choose which repositories to build. | Callers across the organisation, as a starting list for reading each call site. |
| Delete dead code | Grep finds source references. HTTP and queue consumers are URL strings and topic names in other repositories, not references. | Route-matched HTTP edges, queue and config edges in CodeZero; runtime traffic and logs from Vörr. |
| Upgrade an internal library | Open each consumer's manifest yourself, once you know which repositories are consumers. | Dependencies per repository with declared versions, across the organisation. |
| Upgrade a third-party package | Fetches changelogs and finds imports in cloned repositories well. | Graph shows which repositories and modules import the package; the agent still fetches the changelog. |
| Find duplicated logic | Grep in what is cloned. | Topic, symbol and text search across every indexed repository, matched on names and keywords. |
| Sequence and approval | Plan mode and permission prompts exist, but approval and scope are per-session settings; nothing ties edits to the repositories a plan named. | One plan with phases, repositories and verification, approved by a person, with confirmation before any edit outside the plan's repositories. |
| Runtime check | One MCP server or CLI per tool, each credentialed on your machine, with service names matched by hand. | Vörr answers across connected tools with resolved names, source and match quality, and a deploy feed across CI/CD sources. |
With CodeZero and Vörr
- Callers beyond the checkout (CodeZero)
- Definitions, name-matched references, callers and text matches across every indexed repository.
- Consumers that are not references (CodeZero)
- HTTP calls matched to declared routes, queue producers and consumers, and config references, each with file:line evidence and a confidence on HTTP and config edges, and Verify to record a verdict.
- Dependency picture (CodeZero)
- Declared dependencies and versions per repository, and third-party package imports in Graph.
- Staged, scoped work (CodeZero)
- A plan with phases, per-repository verification and approval, Clone for missing checkouts, confirmation before any edit outside the plan's repositories, and tasks per repository.
- Runtime (Vörr)
- Deploys across CI/CD sources, errors and error rate per source against a baseline, and traffic or log evidence for a path you want to delete.
- Together
- CodeZero shows who uses the code before you change it; Vörr shows whether the services that use it are still healthy after each release. They are used side by side: CodeZero in VS Code, Vörr from your assistant.
Worked example: rename getUserById to findUser across repositories
The task is a naming tidy-up in users-lib: getUserById becomes findUser, to match findTeam and findOrg. Behaviour stays the same. An agent in the users-lib checkout renames the function and its tests, and the build is green. Three services import the old name from the package, and none of them is cloned on this machine.
With CodeZero, the definitions and references come first.
Definitions of getUserById
users-lib src/users.ts:31 exported from @acme/users
legacy-crm lib/customers/lookup.ts:12 local definition, unrelated
References to getUserById from @acme/users
users-api src/routes/profile.ts:22 import + call
users-api src/routes/sessions.ts:48 import + call
admin-web src/pages/UserDetail.tsx:15 import + call
support-tools src/commands/impersonate.ts:9 import + call
users-lib test/users.spec.ts:7 test
Text matches (no parser reference)
support-tools test/__mocks__/users.ts:4 jest.mock key "getUserById"
Not cloned locally: users-api, admin-web, support-tools, legacy-crmlegacy-crm defines its own getUserById and never imports @acme/users, so it is excluded from the plan with that reason recorded. The mock in support-tools is a string key, which only the text search found; it goes into that repository's task. Because the three consumers release independently, getUserById stays for one release as a deprecated alias that calls findUser.
Plan: rename getUserById to findUser
Repositories: users-lib, users-api, admin-web, support-tools
Excluded: legacy-crm (own definition, does not import @acme/users)
Phase 1 users-lib
Add findUser; keep getUserById as a deprecated alias calling it
Verify: npm test && npm run build
Phase 2 consumers (depend on phase 1)
users-api import findUser Verify: npm test
admin-web import findUser Verify: npm run typecheck && npm test
support-tools import findUser; update mock Verify: npm test
Follow-up (separate plan)
Remove the getUserById alias after one release; repeat the consumer search firstTasks offers Clone for the three consumer repositories, and each gets its own pull request. After each consumer release, Vörr shows the deploy and no new errors that mention either name. One release later, the reference search for getUserById from @acme/users returns only the alias itself, and the removal runs as its own small plan.
Vörr, after the admin-web deploy (production)
Deploy admin-web #317 Argo CD 09:42
Errors Sentry admin-web no new issues since 09:42; none mention findUser or getUserById
Error rate Datadog admin-web 0.2% (vs 0.2%, 7 days ago)
Match admin-web explicit (service map)Verify and follow up
- Provenance: every caller and consumer has a file and line you have opened, not only read about.
- Match quality: same-named symbols are removed from the list; low-confidence edges are verified or treated as questions.
- Coverage: every repository that could depend on the code is indexed. For deletions, the runtime source for that service is connected.
- Tests: each repository's declared verification passes, and its build fails where you deliberately removed an old name.
- Plan: every caller from the impact check is in a plan repository or excluded with a reason.
- Follow-ups: schedule removal of deprecated aliases and re-exports, and repeat the caller search before removing them.
Limits
- CodeZero covers indexed, committed code on the default branch. Unindexed repositories, other people's unmerged branches and code outside the organisation are not visible. A local checkout adds your own branch and uncommitted work.
- References are matched by name and callers are heuristic. Neither is compiler-accurate, so your build and type checker remain the final check in each repository.
- Inferred HTTP, queue and config edges come from matching paths, names and config. URLs built at runtime, gateway rewrites and dynamic queue names can be missed.
- Dependencies come from manifests. The installed version depends on each repository's lockfile.
- Duplicate detection matches names and keywords, so a copy written with different vocabulary may not appear.
- Vörr is read-only. It shows deploys, errors and traffic for connected sources; it does not roll back. No traffic is only meaningful for a source that is connected.