Keeping AI Coding Agents Inside the Target Architecture During Migrations
A migration leaves legacy and target patterns in the same repository. Mneme HQ scopes each architectural decision to the paths it governs, so agents build toward the architecture you are migrating to.
During a migration, the codebase teaches the wrong architecture.
Suppose the system you are leaving is Controller → Service → Repository → Database and the system you are moving toward is API → Domain → Port → Adapter. While the migration runs, both structures legitimately exist in the repository. That is not a defect. It is what a migration is.
Now ask a coding agent to add a feature. It inspects the codebase, finds two hundred examples of the old structure and twenty of the new one, and concludes that the old structure is how this system is built. Statistically it is correct. Architecturally it has just reproduced the thing you are paying to remove.
The dominant pattern in a repository may be precisely the pattern you are trying to eliminate. What exists in the codebase is not what should be created next, and frequency is not intent.
db.query(Customer).filter_by(id=customer_id)
Path: modern/orders/customer_lookup.py
Applies: migrated modules only. The legacy tree is excluded.
Use: CustomerRepository via RepositoryPort.
→ The pattern the agent copied is the one being removed.
Nothing in the toolchain knows which architecture is the target.
| Approach | Limitation | With Mneme HQ |
|---|---|---|
| Codebase context and retrieval | Reports what the repository contains, and during a migration legacy is most of it | Decisions state which pattern survives, independent of how often it appears |
| Migration plan in the prompt | Advisory, and silently dropped when context is compacted mid-session | Compiled decisions are evaluated before generation in every session |
| Linters and static analysis | Apply uniformly; cannot permit a pattern in legacy code and forbid it in migrated code | Path applicability scopes each rule to the tree it actually governs |
| Code review | Sees the diff after the wrong pattern was already copied, at AI output volume | Enforcement runs before generation and again as a deterministic CI gate |
| Deleting the legacy code | Unavailable until the migration finishes, which is the entire problem | Both architectures coexist under explicit, separately scoped decisions |
Give every migration state its own decision.
A migration has more than two states. Treating it as "old code" and "new code" is what makes agents guess. Four states are enough to describe almost any migration, and each one maps onto a decision the enforcement layer can resolve.
Record the four migration states
Legacy was valid historically and is no longer the model for new work. Target is what new and migrated code follows. Transitional covers the adapters, dual writes, and compatibility layers that exist only during the cutover. Superseded is the pattern that must not return to a component once it has migrated.
Scope each rule to the paths it governs
include_paths and exclude_paths decide where a rule applies. A target rule governs modern/** and leaves legacy/** alone, which is what lets both trees coexist without the agent inferring which one is canonical.
Compile with mneme adr import
The Constraints block of each ADR compiles into typed rules the enforcement layer evaluates. Applicability is resolved against the real target path at check time, and every check reports which rules applied, which were skipped, and why.
Gate generation and CI on the applicable decision
Run mneme check --mode strict before the agent writes, and again in CI. A migrated module that reaches for the superseded pattern fails against the decision that governs its path, no matter which agent produced the change.
What a scoped migration decision looks like.
The decision below supersedes direct database access, but only inside the migrated tree. The legacy tree keeps its existing calls until its components are migrated, so the rule does not fire on code that has not moved yet.
--- id: ADR-042 title: Direct database access is superseded by RepositoryPort status: accepted priority: foundational date: "2026-08-28" scope: code --- Direct database access was the standard in the pre-migration architecture. Migrated modules reach persistence through RepositoryPort. The legacy tree keeps its existing calls until its components are migrated. ## Constraints - FORBID_LITERAL: value: db.query( include_paths: - "modern/**" exclude_paths: - "legacy/**"
$ mneme adr import docs/adr/ Compiled 3 migration decisions. $ mneme check --mode strict PASS [ADR-041] migrated module resolves persistence through RepositoryPort SKIP [ADR-042] legacy/orders/service.py Reason: excluded by exclude_paths. This component has not migrated. FAIL [ADR-042] FORBID_LITERAL "db.query(" Path: modern/orders/customer_lookup.py Reason: direct database access is superseded in migrated modules. Result: FAIL (1 violation, strict mode)
The SKIP line is the part that matters during a migration. The same rule that fails in the migrated tree is deliberately silent in the legacy tree, and the check reports why. An agent working across a half-migrated repository gets a different answer per path instead of one blanket rule it has to reason around.
The enforcement path itself is shown end to end in the repository pattern demo, where a direct SQL call from a service layer is matched to its decision and denied by a strict check.
What changes once migration states are enforced.
Common questions.
How is this different from putting the migration plan in the prompt?
The legacy pattern is still valid in unmigrated modules. Does Mneme forbid it everywhere?
include_paths for the migrated tree and exclude_paths for the legacy tree, which is what lets both architectures coexist without the agent guessing which one governs the file it is editing.