Migrations are the hardest case for an AI coding agent

Greenfield development gives a coding agent a clean signal. There is one architecture, the codebase demonstrates it, and copying the surrounding patterns is usually the right instinct. A migration inverts that. For the entire duration of the project the repository contains two architectures at once, and the one you are trying to eliminate is almost always the better represented of the two.

This is not a temporary annoyance. It is the defining condition of the work. A migration that took two years to schedule will take at least as long to execute, and for all of that time the agent's most reliable heuristic, follow the surrounding code, points in exactly the wrong direction.

The dominant pattern in a repository may be precisely the pattern you are paying to remove.

Legacy code is misleading context

Retrieval and context engineering have made agents much better at finding what exists in a codebase. During a migration that improvement cuts against you. A more thorough search returns more examples of the legacy structure, because there are more of them. The better the retrieval, the more confidently the agent reproduces the architecture you are leaving.

Suppose the system you are moving away from is organized as Controller → Service → Repository → Database, and the target is API → Domain → Port → Adapter. Both are present. Both compile. Both pass their tests. Nothing in the repository marks one as historical and the other as intended, because that distinction does not live in code. It lives in a decision that was made in a meeting and recorded, at best, in a document nobody compiles.

Frequency is not architectural intent

An agent asked to add a feature will inspect the codebase and find, say, two hundred instances of the legacy pattern and twenty of the target. It concludes that the legacy pattern is how this system is built. As a statistical claim about the current repository, that is correct. As an architectural claim about what should be written next, it is exactly backwards.

This is the gap that migration governance has to close. What exists in the codebase is a description of the past. What should be created next is a decision, and decisions are not recoverable by counting.

Four states, not two

Most migration tooling treats the problem as binary: old code and new code. That framing is what forces agents to guess, because the interesting cases are the ones in between. Four states describe almost any migration, and each maps onto a decision an enforcement layer can resolve against a path.

StateMeaningWhat an agent may do
LegacyValid historically. No longer the model for new work.Read it. Modify it in place. Do not extend it.
TargetWhat new and migrated code follows.Build here by default.
TransitionalAdapters, dual writes, compatibility layers, strangler boundaries that exist only during the cutover.Permitted, but only at the declared boundary and only until the boundary closes.
Forbidden reintroductionA pattern that must not return to a component once that component has migrated.Blocked in the migrated tree. Still allowed where migration has not reached.

A naming caution, because the words collide. The fourth state is a decision that stays status: accepted and carries path selectors covering the migrated tree. That is not the same thing as an ADR whose status: is superseded, which is how a record is retired from the active set and stops being enforced at all. The pattern is superseded; the decision forbidding it is active.

The fourth state is the one that gets skipped, and it is the one that causes the most damage. Once a module has been moved, nothing in the codebase prevents the next agent from reintroducing the old pattern into it. The module now contains target-architecture code, so the agent has no local signal that the old approach was ever wrong here.

Legacy architecture Target architecture Transitional boundary Path applicability resolves the rule Verdict, per path

The same rule resolves differently depending on which tree the agent is editing

Transitional decisions are the ones that expire

A compatibility adapter is not a mistake. It is a deliberate, temporary exception, and it needs to be recorded as one. The failure mode is not that teams create adapters. It is that the adapter's expiry is never written down anywhere a machine can read it, so the adapter quietly becomes permanent architecture.

A transitional decision should state its boundary and its termination condition. Both live in the ADR's prose, not in a field the compiler reads: there is no expiry attribute, so retiring the exception on time is a human step. What the corpus guarantees is that the exception is visible and attributable while it lasts. When the component behind the adapter finishes migrating, the decision is marked status: superseded, which removes it from the active set, and the target decision governs that path. The retired record stays in the corpus as the reason the exception existed, which is the part that ordinarily gets lost.

Sequencing is a constraint, not a plan

Migrations have ordering requirements. Component B cannot move until component A has moved. Those requirements usually live in a project plan, which agents do not read, rather than in the decision corpus, which they can be checked against.

There is no ordering primitive to reach for here. What a corpus can hold is the consequence of the ordering: while component A has not moved, a rule forbids the target pattern in component B's tree, and that rule is retired as part of A's migration. The ordering is still enforced by whoever retires the rule. What the corpus adds is that an agent is checked against it in the meantime, rather than discovering the constraint through a merge conflict three weeks later.

Documentation does not survive a long migration

Every organization running a migration has a document describing the target architecture. It is usually accurate on the day it is written. The problem is not accuracy. It is that a document is advisory, and an agent that never reads it is not violating anything.

Prompt-level instruction has the same weakness in a sharper form. You can tell an agent about the target architecture at the start of a session, and that instruction is real until the context is compacted, the session restarts, a different agent picks up the work, or the task runs long enough that the instruction is outranked by more recent material. Over a migration measured in quarters, every one of those happens repeatedly.

Retrieval tells an agent what exists. Governance tells it what should survive.

Enforcement has to be scoped by path

The reason migration governance needs more than a global rule list is that the same rule must produce different verdicts in different parts of the repository. Forbidding the legacy pattern everywhere breaks the legacy tree, which is still running in production. Allowing it everywhere is what created the problem.

Path applicability resolves this. A rule that carries explicit include and exclude selectors governs the migrated tree while staying silent where migration has not reached, and reports which of the two happened on every check. An agent working across a half-migrated repository then gets a specific answer for the file it is editing rather than one blanket rule it has to reason around.

What to encode first

You do not need to model the whole migration before this is useful. Start with the boundary agents cross most often, which in practice is nearly always persistence or service-to-service calls. Two or three scoped decisions cover the majority of incorrect pattern copying.

  • The persistence boundary. New and migrated modules reach data through the target abstraction.
  • The dependency direction. Migrated code does not acquire new imports from the legacy tree.
  • The adapter boundary. Only the declared adapter may call the legacy interface.
  • The reintroduction rule. A pattern removed from a migrated component does not come back to it.

Each of these is a sentence a staff engineer can write in a minute and an agent can be checked against for the rest of the migration.

Conclusion

Adding autonomous coding agents to a migration raises a specific risk that greenfield work does not carry: the agents are statistically likely to rebuild the architecture the migration exists to remove, and they will do it at machine speed while every individual change looks locally reasonable.

The fix is not a better prompt or a larger context window. It is making the distinction between historical architecture and current architectural intent machine-readable, scoping it to the paths it governs, and enforcing it at the moment an agent acts.

The architectural drift demo shows the same enforcement path on a week of agent-produced changes.