Acceptance criteria that close the intent gap
An agent fills every silence in a spec with a guess. Six ways to leave less silence, one worked example, and how to keep the spec short anyway.
Code that passes is not the same as code that does what you meant. The distance between a request in ordinary language and the precise behaviour of a program has always haunted software; generated code widens it at a scale the industry has not seen before, because the thing filling the distance is now fast, confident, and does not share your assumptions. People call it the intent gap. This piece is about the first and cheapest tool against it: how the acceptance criteria are written.
Why it is hard in principle
Two reasons, and neither is a tooling problem.
Intent is underspecified. The person who wants something usually cannot state all of what they expect until a counterexample shows them what they left out, and counterexamples are hard to predict. You do not know what you want until you see what you do not want.
There is no oracle but the user. A test checks code against a specification. Nothing checks the specification against the intention except a person, and only the person whose intention it is. This is the bottleneck under the bottleneck: everything downstream can be automated, and this cannot.
A consequence worth naming: every test, rubric and reward model is a proxy for intent, never intent itself. Put a proxy under optimisation pressure and it stops being a good measure, which is Goodhart’s law in its software form. An agent that makes every test pass and still builds the wrong thing has not malfunctioned. It optimised the proxy you gave it, and the slack between proxy and intent was where it went.
Six techniques
1. Every criterion passes or fails. The test: could two readers disagree about whether it has been met? If yes, rewrite. The words to ban are the ones that feel like criteria and are not: gracefully, fast, user-friendly, appropriate, handle the edge cases. Replace them with numbers and lists. Not “responds quickly” but “responds within two seconds at the 95th percentile”. Not “handles empty input” but “shows message X when the field is empty”.
2. Prescribe, do not describe. “Handle the edge cases for dates” is a wish. It forces the agent to guess whether an invalid date returns null, throws, or falls back to a default, and every guess is an intent gap. The prescriptive form: “If the date is invalid, return 400 with the code INVALID_DATE and store nothing.”
3. Use a template for requirements, scenarios for behaviour. EARS, a notation from Rolls-Royce’s work on engine control requirements, gives five sentence shapes that force you to declare the trigger and the condition:
| Pattern | Shape |
|---|---|
| Ubiquitous | The system shall hash passwords with bcrypt. |
| Event-driven | When the user submits the form, the system shall … |
| State-driven | While the session has expired, the system shall … |
| Unwanted | If the upstream call times out, then the system shall retry … |
| Optional | Where the tenant has single sign-on enabled, the system shall … |
The value is not nicer prose. It is that choosing a pattern makes you notice the state or the event you had not thought about. Given/When/Then does the same for scenarios. Keep EARS for requirements with few preconditions; past three, it stops helping.
4. Write the negative space. Most of the gap sits in what must not happen, because the writer takes it as obvious and the agent takes nothing as obvious. For every feature answer four questions: what must never happen; what happens on bad input (empty, null, too long, wrong format, unexpected characters); what happens when an external system fails; who must not be allowed to do this. A criterion like “do not send a confirmation email while the order is a draft” is worth more than most positive ones.
5. Add one line of why. The anti-Goodhart line. An agent optimises the wording; a reason lets it, and the reviewer, judge the case the wording did not cover. “Lock the account after five failed sign-ins in fifteen minutes. Purpose: brute force, not punishing a typo, so show the remaining wait.” The second sentence changes how the message gets implemented.
6. Make it executable. A criterion in a document is a promise. A criterion that runs is a gate. The cheapest route is a small table of concrete input and expected output, three to five rows including the edges. A table ends arguments about interpretation better than any sentence, and an agent turns it straight into tests.
A worked example
The request as it usually arrives: “Let users change their password.” Rewritten:
Acceptance criteria · change password
Purpose: a signed-in user can rotate their own password without contacting support; safety over convenience. Recovering a forgotten password is a separate flow and not covered here.
AC-1 When the user submits the change form with the correct current password and a valid new one, the system shall update the password and invalidate every other session within 60 seconds.
AC-2 If the current password is wrong, then the system shall return WRONG_PASSWORD and shall not reveal whether the new one was valid.
AC-3 Valid means 12 to 128 characters and not in the list of the ten thousand most common passwords.
| Input | Result |
|---|---|
abc |
INVALID_LENGTH |
password12345 |
TOO_COMMON |
| 129 characters | INVALID_LENGTH |
correct horse 2026! |
accepted |
AC-4 The system shall never send the new password by email.
AC-5 This does not apply to accounts that sign in through single sign-on: hide the form, and return 403 to a direct call.
Every line can be tested, and the most valuable lines are the ones the original was silent about.
Will the spec not get too long?
It is a fair worry, and there is a real trap in it. If every feature is specified like the example you are back to writing everything up front, with the cost of writing and maintaining it, and a long spec drifts: the code changes, the spec does not, and a long stale spec is debt. But the cost of specification does not disappear when you decline to write it. It becomes the cost of wrong guesses, paid at review and in rework. The question is not long or short. It is where paying is cheaper.
Three ways to stay short without reopening the gap.
Move the conventions up a level. Look at the example again: most of its lines are general policy. How errors are shaped, that messages never leak information, how validation is reported. That belongs in the project’s conventions, written once and read by the agent on every task. The feature’s own criteria then hold only the decisions that are the feature’s own: sixty seconds, the single sign-on exclusion. Twelve lines become four. Shorter by moving the repeated information up, not by dropping it.
Depth proportional to risk. An internal admin screen gets one sentence plus the conventions; a mistake there is cheap to fix. Authentication, payments, a data migration, a public API get the full treatment, because a mistake there is expensive and hard to reverse. The length of a spec tracks blast radius times irreversibility. These are the same risk tiers you will use for review, so use one scale for both ends.
Do not write everything first; answer questions when asked. Give the agent the draft criteria and ask it for ten readings or situations the criteria do not settle. Answer the ones with consequences. Mark the rest “per convention” or “your call, it does not matter”. Saying explicitly that you do not care is also closing the gap: the silence becomes a deliberate delegation instead of a guess. And because intent only shows its holes against a counterexample, this moves the cost of fixing intent from after the code to before it, where it is nearly free.
A pleasant paradox: a five-row table is usually shorter than the paragraph that tries to describe it, and more precise. Good specifications in this era are not longer than the old ones. They are denser: fewer words of description, more decisions per line.
In Rindo the conventions live on the project’s Guidance page, which any connected assistant can read before it acts, one call away over MCP; the criteria live in the task’s description, which keeps its revision history; and when an assistant proposes to rewrite them, the rewrite can wait as a draft with a diff. The table turns into test cases whose steps are versioned, so a result recorded later still points at the steps it exercised.