Multi-File Refactors That Agents Commonly Botch
The pull request looked clean. Fourteen files changed, tests green, a tidy commit message explaining that a helper had been extracted into a shared module. Two days later the staging build fell over because the helper had been imported through a path alias that only resolved inside one team's directory. Then a background job started silently writing nulls into a column that used to be populated by the old inline logic. Then someone noticed that the mobile client was still calling an endpoint whose handler had been renamed, and the fallback was returning an empty object instead of an error.
None of these were the refactor. The refactor was fine. What got botched was the blast radius: the set of places that quietly depended on the old shape of things and were never told the shape had changed.
Autonomous coding agents are unusually good at the visible part of a multi-file refactor and unusually bad at the invisible part. They can rename a symbol across forty files before a human finishes reading the diff. They can move a function, split a class, and update every direct caller in seconds. The failures cluster in a narrower band, and after watching enough of these land in review queues, the band starts to look predictable.
Renames that only touch the files the agent opened
The most common failure is a rename or signature change applied to the files the agent happened to read, rather than the files that actually reference the changed symbol. An agent working from a task description like "rename fetchUser to getUser" will typically grep, find a dozen hits, update them, and declare victory. The hits it missed are the ones that matter: string-based dynamic imports, dependency injection registries keyed by name, test fixtures that reference the function through a mock factory, monitoring dashboards that filter on the old name, and documentation that has drifted out of the repo into a wiki.
Static analysis catches some of this. It does not catch the wiring that happens through configuration files, environment variable names, or the ORM's convention of deriving a table name from a class name. A rename that changes the class name also changes the inferred table name, and the agent has no reason to suspect the database layer is doing string manipulation on the class name at runtime.
The import graph the agent never drew
Multi-file refactors are mostly import graph surgery. Moving a function from module A to module B sounds local until the graph is inspected: A might import from C, B might not, and every file that imports the function from A now needs either an updated import or a re-export shim. Agents routinely choose one of two extremes. They either leave a re-export in A ("for backwards compatibility") and forget to remove it six months later, or they remove the re-export immediately and break every downstream consumer in a different repository that the agent cannot see.
The right call depends on whether the codebase is a monorepo with a build system that will catch the break, or a set of packages published independently with semver. An agent working file-by-file inside a single checkout has no way to know which situation it is in unless the task description spells it out. Left to its own judgment, it usually optimizes for the local build passing, which is the wrong objective for a published package.
Circular imports introduced by extracting a shared helper
Extraction refactors are where agents produce the most impressive-looking diffs and the most subtle runtime breakage. The pattern: two modules share some logic, the agent pulls it into a third module, and both originals import from the new one. That works until the shared logic turns out to call back into one of the originals, either directly or through a chain of two or three modules. Now module A imports the helper, the helper imports module B, and module B imports module A. Depending on the runtime, this either throws at load time or, worse, resolves to a partially initialized module and produces undefined in a place that is only exercised on a code path the tests do not cover.
Python and JavaScript handle circular imports differently, and both handle them badly enough that the bug usually surfaces in production rather than in CI. An agent that does not run the full application, only the unit tests for the touched modules, will not see it. The tests import the modules directly; the application imports them through a router that triggers the cycle.
Behavior changes smuggled in as "mechanical" moves
A refactor is supposed to preserve behavior. Agents are trained on a corpus where refactors often do not, and they absorb the pattern. A function gets moved and, while being moved, its error handling is "cleaned up" from a try/catch that logged and continued into one that rethrows. The diff is small, the commit message says "move validation into shared module," and the reviewer sees a file deletion and a file addition and assumes the bodies match.
The mechanical way to catch this is to compare the moved function against its original, line by line, ignoring whitespace. Almost nobody does this on a large diff, and agents are not in the habit of producing a "pure move" commit separate from a "behavior change" commit. The result is a refactor PR with an embedded bug fix or an embedded regression, and the reviewer approves it on the premise that nothing should have changed.
Test files updated to match the new code instead of the old behavior
This one is subtle and genuinely dangerous. When an agent refactors production code and a test fails, the agent has two options: fix the production code, or update the test. Given a task framed as a refactor, and given that the test is "just" asserting on an internal detail, the agent frequently updates the test. The suite goes green. The behavior the test was guarding is now unguarded.
Agents are especially prone to this with mocks. A function that used to take three arguments now takes two, and the test's mock assertion on the call signature gets rewritten to match. The mock is now asserting that the code calls itself the way the new code calls itself, which is tautological. The test has been converted from a behavioral check into a mirror.
Configuration, migrations, and the files outside the source tree
Source code is the easy part. The refactors that bite hardest touch the files an agent is not in the habit of reading: the CI workflow that references a script path, the Dockerfile that copies a directory the agent just renamed, the database migration that seeds a table using a model the agent just moved, the feature flag registry that hardcodes a module path, and the deployment manifest that mounts a config file whose schema the agent changed.
An agent told to "move the billing module under services/" will move the Python files and update the imports. It will not think to check whether the Docker build copies billing/ explicitly, whether the CI job runs pytest billing/, or whether the production entrypoint references billing.app:main. Those references are strings in YAML and Dockerfiles, and the agent's search is usually scoped to the language it is editing.
The signature change that ripples through typed boundaries
Adding a required parameter to a function is a one-line change at the definition and a potentially dozens-of-files change at the call sites. In a statically typed language, the compiler finds them. In a dynamically typed one, the agent has to find them by search, and its search is only as good as the call patterns it imagines. Keyword arguments, partial application, decorators that wrap the function, and frameworks that call the function by name from a registry all defeat a naive text search.
The failure mode is a TypeError in a code path that is not covered by the tests the agent ran. This is the class of bug that survives review because the diff looks complete: every call site the agent found is updated, and the ones it did not find are, by definition, not in the diff.
What reviewers can actually check in ten minutes
The practical defense is not to read the whole diff. It is to ask three questions of the agent's output and verify each one directly. First: what is the complete list of files that reference the changed symbols, and how was that list generated? If the answer is "grep," the reviewer should run the grep themselves with the flags the agent did not use, including case-insensitive, whole-word, and multi-line. Second: are there any string literals in config files, YAML, Dockerfiles, or migration scripts that contain the old names? A targeted search across non-source file types takes two minutes and catches the deployment breakage. Third: are the test changes asserting on the old behavior or the new implementation? If a mock's expected arguments changed, that is a signal to read the test carefully.
None of this requires understanding the refactor. It requires understanding where refactors leak, which is into the seams between source code and everything else.
The import that resolves in tests and fails in production
One last pattern deserves its own section because it produces the most confusing incidents. An agent extracts a module and imports it with a relative path that happens to work because the test runner's working directory is the repo root. The application, however, is launched from a different directory, or the module is loaded through a package manager that does not preserve relative paths, or the build step rewrites the import graph in a way that assumes absolute package paths. The test suite passes locally and in CI. The deploy fails at startup with a module-not-found error that points at a file that clearly exists.
The fix is a one-character change to an import path. Finding it requires reproducing the deployment's working directory and import resolution, which is exactly the context an agent does not have and rarely asks for. A reviewer who has been burned once will run the application entrypoint, not just the tests, before approving any refactor that adds a new module. That single habit catches more of these than any static analysis tool.

