← The ADLC library
Failure modes · 1

Speed without a definition of done gets you to the wrong place faster

The ticket said "improve the search experience." Nine days and eleven pull requests later, the team had shipped something nobody wanted. Nothing had gone wrong, exactly.

The ticket said “improve the search experience on the catalogue page.” It had been sitting in the backlog for two quarters, which is usually a sign that nobody has thought hard about it recently. Someone picked it up on a Monday, pointed an agent at it, and by Wednesday there were four pull requests open. By the following Thursday there were eleven, all merged, and the catalogue page had fuzzy matching, a typeahead dropdown, synonym expansion, a recently-searched list, and a relevance model that reranked results by a blend of popularity and text score.

It was, by any reasonable standard, good work. The code was clean. The tests passed. The performance was fine.

It was also completely wrong, because the actual complaint behind that ticket, expressed six months earlier in a support thread that nobody had linked, was that customers searching by internal SKU got zero results. Fuzzy matching made that worse. SKUs are exact strings. Fuzzing them turns a precise miss into a confident wrong answer.

Nobody was careless here. The engineer read the ticket. The agent built what the ticket described. The reviewer checked that the code did what the pull request said it did. Every individual step was defensible. The system still produced eleven merged pull requests of work that had to be substantially reverted, and it did it in nine days, which is faster than the old process could have produced the same mistake.

That is the failure mode. Not bad code. Not laziness. A team moving at high speed in a direction nobody had actually specified.

Vagueness used to be free

Here is the thing that makes this feel new: for fifty years, vague requirements were survivable, and the reason they were survivable is that they were absorbed by a human being who was slow enough to notice.

Think about what actually happened when a developer picked up “improve the search experience.” They read it. They frowned. They went and looked at the search code. They noticed the SKU field was being tokenised. They asked someone. Or they did not ask anyone, but they had been on the team eighteen months and they remembered the support thread, and they built the narrow fix. Or they built the wrong thing but only got two days into it before a standup conversation redirected them.

The vagueness was still there. It just had a long, slow, human buffer between it and the codebase, and that buffer had a very useful property: it was full of doubt. Humans hesitate. Hesitation is a form of error correction, and we never accounted for it because it was free.

The human buffer

  • Reads it, frowns, goes and looks
  • Notices the SKU field is tokenised
  • Asks someone, or remembers the thread
  • Gets two days in before a standup redirects
  • Full of doubt, and doubt corrects errors

No buffer at all

  • Produces a reasonable interpretation
  • Implements it thoroughly, with tests
  • Hands over a clear summary
  • All before the frowning stage would have ended
  • Confident, because nothing produces doubt
Hesitation was a form of error correction and we never accounted for it, because it was free. The words you supply are now load-bearing in a way they never were.

Agents do not hesitate. That is largely what makes them useful. Point one at “improve the search experience” and it will produce a genuinely reasonable interpretation of that phrase, implement it thoroughly, write tests for it, and hand it to you with a clear summary, all before anyone would have finished the frowning stage. The interpretation is confident because there is nothing in the system that produces doubt. The output is correct with respect to the words it was given.

Which means the words you give it are now load-bearing in a way they never were.

The compounding part

If this were a one-shot problem it would not be worth an article. Wrong ticket, wrong feature, revert it, move on. Teams have always done that.

What makes it a failure mode rather than an incident is that the output of a vague ticket becomes the input to the next thing. The eleven pull requests did not just add a feature. They added:

A typeahead component that other pages started importing, because it was there and it was good. A synonym table that someone populated with two hundred entries, by hand, over a week. A relevance scoring module that a second team wired into their own listing page. Documentation describing the new search behaviour. Two follow-up tickets that assumed the new architecture. A dashboard tracking typeahead engagement.

By the time the SKU problem surfaces, the wrong thing is load-bearing. Reverting eleven pull requests is easy. Reverting a week of somebody’s manual synonym curation, a dependency from another team, and a set of expectations you have already set with stakeholders is not easy at all. Speed did not just get you to the wrong place. It got you to the wrong place, and then it built a small settlement there while you were looking elsewhere.

I think this is the part teams consistently underestimate. They model the cost of a wrong build as the cost of the build. It is actually the cost of the build plus the cost of everything that has attached itself to the build in the interval, and that interval is now short but the attachment rate is high, because everything downstream also got faster.

Eleven pull requests mergedClean code, passing tests, fine performance. Trivially revertible at this point.
A typeahead other pages importIt was there and it was good. Now there is a dependency.
Two hundred synonyms curated by handA week of somebody's work, attached to the wrong foundation.
A second team wires in the scoring moduleNow another team's roadmap assumes the architecture.
Docs, follow-up tickets, a dashboardExpectations set with stakeholders. The wrong thing is load-bearing.
Reverting eleven pull requests is easy. Speed got you to the wrong place and then built a small settlement there while you were looking elsewhere.

What a definition of done actually has to do now

The phrase “definition of done” has been around for decades and it has mostly meant a checklist: tests written, docs updated, PR reviewed, deployed to staging. Process hygiene. Useful, boring.

That version of the definition of done does not help you here at all. Every item on that checklist was satisfied by the eleven wrong pull requests.

What is needed is something narrower and more awkward: a statement of what must be observably true when the work is finished, specific enough that you could check it without asking the author what they meant. Not “improve search.” Something closer to “a query that exactly matches a product SKU returns that product as the first result, and queries that do not match any SKU behave as they do today.”

Notice three properties of that second version.

It is checkable. You can write a test for it. More importantly, someone reading the diff can decide whether it is satisfied without a conversation.

It is narrow enough to be wrong. “Improve the search experience” cannot fail. Any change that touches search can be argued into satisfying it. A criterion that cannot fail is not a criterion, it is a mood. The SKU version can be unambiguously not met, which is exactly what makes it useful.

It carries the constraint, not just the goal. The second half, “queries that do not match any SKU behave as they do today,” is the part that would have prevented the fuzzy matching. Most acceptance criteria specify what should change and say nothing about what should not. Agents read that silence as permission, and honestly, so do enthusiastic humans.

That third one is where I would put my effort first if I were trying to fix this on a team tomorrow. Teams get better at writing goals fairly quickly once they see the problem. They stay bad at writing boundaries for a long time, because boundaries feel like distrust, and nobody enjoys writing a sentence that reads as “please do not be clever here.”

CheckableSomeone reading the diff can decide whether it holds without a conversation.
Narrow enough to be wrong"Improve search" cannot fail. A criterion that cannot fail is not a criterion, it is a mood.
Carries the constraint, not just the goal"Queries that match no SKU behave as they do today" is the half that would have prevented the fuzzy matching.
Teams get better at writing goals quickly. They stay bad at boundaries for a long time, because a boundary reads as distrust.

The objection: this is just requirements engineering with extra steps

It is, sort of, and I want to take that objection seriously rather than swatting it, because the people making it usually have scar tissue from the last era of specification-heavy process and their instincts are not wrong.

We spent a long time learning that big up-front specification does not work. Requirements documents rot. Detailed specs written before contact with the code encode assumptions that turn out to be false, and then you have a document arguing with reality and the document usually wins for longer than it should. Agile was in large part a reaction to that, and it was a correct reaction.

So the reasonable fear is that machine-verifiable acceptance criteria are waterfall wearing a new hat.

The distinction I would draw is about scope and timing rather than rigour. A specification document tries to describe a system before it exists. Acceptance criteria describe one change at the moment it is about to be made, by someone who has already looked at the code. The failure mode of specs was that they were written too early by people too far from the work. Criteria written at pickup time, by the person doing the pickup, do not have that property.

The second distinction is that this is not extra work, it is relocated work. That thinking always happened. It happened in the developer’s head between reading the ticket and writing the first line, and it was invisible, and it was slow, and it was also the thing keeping the team out of the ditch. What has changed is that the implementation step no longer waits for it. So it has to happen explicitly, and be written down, or it does not happen at all.

You are not adding a specification phase. You are making visible a phase that used to be smuggled inside the typing.

The pattern in the wild

Once you have the shape of this failure in your head you start seeing variants everywhere.

The performance ticket that said “make the report faster” and produced a caching layer that made the report faster and the data eleven minutes stale, which for that particular report was the entire point of it.

The refactor that said “clean up the payment module” and cleanly deleted a branch that looked dead and was in fact the path taken by one legacy integration that ran monthly.

The “add validation to the signup form” that added validation so thorough that a genuine category of customer, the ones with a single-character surname or an apostrophe in it, could no longer sign up. Correct with respect to the words. Wrong with respect to the world.

In every case the tell is the same: the ticket described a direction rather than a destination. Directions are fine when someone is walking. They are terrible when something is sprinting.

Where this breaks down

I have been arguing this fairly hard, so let me argue the other side, because there are at least three situations where everything above is wrong or actively harmful.

Not all work has a destination. A meaningful share of engineering is exploratory: spikes, prototypes, “let us see what this API can do,” performance investigations where you do not know what you will find. Demanding a machine-checkable definition of done for that work is not rigour, it is a category error, and teams that apply this uniformly end up either writing fake criteria to satisfy the process or quietly avoiding exploratory work because it has become bureaucratically expensive. That second outcome is genuinely bad, and it is invisible when it happens. If your criteria discipline is pushing your team away from investigation, you have made things worse, not better.

Over-specification has its own failure mode, and it is a nastier one. Criteria specific enough to be machine-checkable are also specific enough to be gamed, and an agent optimising to the letter of a criterion will happily satisfy the text while missing the intent. Write “the endpoint returns in under 200ms” and you may get a cache with no invalidation. Write “test coverage above 80 percent” and you get tests asserting that true is true. The tighter you specify, the more you get exactly what you asked for, which is only good if what you asked for was right. This is a real trade, not a solved problem, and I would rather a team understood the trade than believed they had escaped it.

Sometimes the vagueness is doing useful work. On a small team with deep shared context, “improve the search experience” is not actually vague. It is compressed. The person picking it up knows about the SKU thread, knows the team’s taste, knows what “improve” means here. Forcing that team to write out criteria they all already hold is friction with no yield, and they will resent it, correctly. This failure mode scales with the distance between whoever holds the intent and whoever executes it, and agents maximise that distance by definition, but a co-located team of four with two years of shared history has a genuinely different risk profile than a team of forty across three time zones. Do not import the ceremony without importing the problem.

And the honest caveat about detection. Even with good criteria, something still has to notice when merged work does not satisfy them. Criteria written in a ticket that nobody rereads at merge time are documentation, not control. This is the part that tooling can help with, GroundTruth included, but tooling that checks criteria is worthless if the criteria are mood-shaped. The order matters: write checkable criteria first, then automate the checking. Automating the checking of unfalsifiable criteria just produces a green light with no meaning behind it.

The takeaway

The old life cycle had a slow, doubtful, context-rich human sitting between a vague ticket and the codebase, and that human was doing far more error correction than anyone gave them credit for. Agents removed the buffer without removing the vagueness, so the vagueness now goes straight into the diff at full speed and comes back looking finished.

The fix is not more process. It is a definition of done narrow enough that it could fail, carrying its boundaries as explicitly as its goals, written at pickup time by whoever is close enough to the code to know what would be wrong. And then, separately, something that actually checks it at merge rather than trusting that it was checked.

The next piece looks at the other half of this problem: what happens when the work is technically correct, the criteria are met, and the pull request is still wrong, because the confidence with which it arrives is itself the thing that defeats review.