Overruled Precedent
Matching the code around a change is right almost every time. This is about the change where it is a trap, and why the agent cannot see it coming.
You are good at this. That is the problem.
You read the function. You read its callers. You take in the three or four examples nearest to the thing you have been asked to change, you infer the precedent they set, and you follow it. You have done this countless times, across countless repositories, and you are right almost every time, because the code around a change is usually the best guide to how the change should be made. Matching the room is not a lazy shortcut. It is most of what makes you useful.
Then comes the one change where the room is wrong.
How we do it, and how we used to
The ticket is small. An order can now be returned, so handle that case the way the handlers around it handle theirs. You look around. The three beside it each set order.status to a string, so you set order.status to a string too, and in that instant you are doing exactly what a careful engineer would do with what is in front of you. The tests pass. You hand it back.
What you cannot see is that months ago the team stopped writing order.status. A mistyped status string had slipped through and silently broken fulfillment for a week, so they moved order state onto a typed state field, an enum the compiler checks, where a misspelling no longer compiles. The handlers you copied are the old ones, the ones nobody has migrated yet. Fulfillment reads state now, not status. So the returned order you so carefully set just sits there, because as far as the part that matters is concerned, its state was never set. The convention you matched is not the rule. It is the thing the rule was written to end. The precedent had been overruled. The code just never heard.
You could not tell the difference, because from inside the file there is no difference to see. "This is how we do it" and "this is how we did it until we decided to stop" look identical in code. They read the same. They pass the same tests. Only one of them is still true, and which one is not written anywhere you were looking.
It was not carelessness
Be precise about what went wrong, because it was not that you were sloppy. You did not skim. You did the correct thing with the information you had, and the information you had was missing its most important piece.
The decision that governed this change lived in a conversation you were not part of, made by people who have since moved to other teams, recorded, if it was recorded at all, in a thread that scrolled out of reach long ago. It was never in the code in front of you, because code is, by default, the result of a decision with the reason filed off. By the time it reaches you it is pure convention, and convention is the one thing that cannot tell you when it is meant to be broken.
A comment would not have saved the next one
Suppose you had somehow known. Suppose someone had told you, this once, write state, not status, and you had done it and left a comment saying why. That helps, for a while. Then the comment drifts out of date, or the code moves, or the next change is three files away where your comment is not, and the next worker through, another agent, or a new hire, or you next week with no memory of today, reads the room again and matches it again.
There is a stronger move than a comment, and where you can make it you should: deprecate the field. Mark order.status gone, write the reason into the deprecation itself, and every reference to it, the reads as much as the writes, lights up in the editor and in review carrying both the conclusion and the reason. When a decision comes down to a single symbol and someone thought to annotate it, the type system is a fine place to keep it. Use it.
But look at what that took: a symbol to hang the warning on, and a person who already decided to hang it there. Most of what a company decides has neither. Which customer segment this pricing tier is meant for. Why the reconciliation window is five minutes and not one. Which of two services is allowed to write this row. None of those is a field you can deprecate or a rule a compiler can check. They were settled once, for reasons that mattered, and they live now where the order-status decision lived before someone happened to encode it: in a head, in a thread, in a doc nobody opens. Those are the changes you keep walking into, because those are the ones with nothing in the code to stop you.
Rare per change, constant per codebase
None of this would matter much if it happened once. It does not happen once. You do not make one change, you make hundreds, across every corner of a system no single person holds in their head, and on nearly all of them matching the room is exactly right and you should keep doing it. The trap-change is rare as a fraction of your work and constant as a fact of the codebase, because a system large enough always has some room that is wrong somewhere, and you touch all the rooms, all day, faster than any reviewer can follow you from one to the next.
What used to make this survivable was that changes were slow, and most of them passed through the few people who happened to still hold the reasons. You are not slow. And you hold no reasons.
The decisions a type cannot hold
The order-status decision was the lucky kind. It had a symbol, so a deprecation could carry it, and it should. Most are not that kind. So picture the harder version, the change governed by a decision with nothing in the code to attach to. What would have reached you there?
Not a warning to be more careful. There was nothing to be careful about; nothing in view was wrong. Not a longer instruction file either. You were already doing what a good general instruction would tell you to do, which is match the surrounding style. A rule that says "follow the conventions of the code around you" would have walked you straight into the trap, because the trap was the convention.
The only thing that reaches it is the decision itself, whole and with its reason. The handlers around you all write this row from the service that used to own it. Ownership moved to the other service six months ago, settled in a thread with reasons, and there is no field to deprecate and no compiler that can tell. So write it from the new owner, and here is why the line moved, put in front of you at the moment you reach for the old one, and not on the thousand changes that never touch it. Something that knows which room is the exception, and speaks up only there, with the reason attached.
That is what Brief does. It holds the decisions a company has made and the reasons underneath them, and it surfaces the one that governs the change you are making, right as you make it, so the exception announces itself instead of sitting in plain sight disguised as ordinary code. It does not ask you to stop matching the room. It tells you, on the rare change where it counts, that this room is the one you were meant to break.
You will match the room again tomorrow, thousands of times, and you should, and almost every time you will be right. So the question was never whether you are careful enough, because you are. The question is whether, on the one change where the room is wrong, anyone put the real decision where you would actually see it, or whether they left you to copy the neighbors and call it done?
Frequently asked questions
Why does my AI coding agent follow the wrong pattern in my codebase? Because it infers conventions from the code nearest to a change and matches them, which is the right move most of the time. It fails on changes governed by a decision that points against the surrounding code, because that decision usually is not written in the code itself. The agent copies the neighboring pattern faithfully, not realizing the neighbors are the exact thing the team decided to move away from.
Why does an AI agent make changes that look right but are wrong? Because "how we do it" and "how we used to do it" look identical in code. A change can match every surrounding convention, pass the tests, and read cleanly while still violating a decision that lives only in someone's head or an old discussion. The wrongness is invisible at the point of the change, which is exactly why it survives review and ships.
How do I get an AI coding agent to follow our team's conventions and decisions? Put the governing decision in front of the agent at the moment of the change, rather than relying on it to infer intent from the surrounding code or to find a rule buried in a long instruction file. A product context layer like Brief stores each decision with its reason and surfaces the relevant one for the specific change, so the exceptions announce themselves instead of hiding as ordinary code.
Is a CLAUDE.md or rules file enough to fix this? A rules file helps for a small number of stable, always-apply instructions, but it cannot carry the growing set of decisions that apply only to particular changes. A few of those a type system can hold: when a decision comes down to one symbol, a deprecation with a reason will carry it to everyone who touches that symbol, and you should use it. But most decisions have nothing to attach to, no field to deprecate and no compiler to check, such as which segment a tier serves or which service owns a write. Those have to be selected by which decision governs the change and surfaced at that fork, which is what a flat file read top to bottom cannot do.
← Back to Blog