Write the acceptance criteria first, or the model will write them for you
Ask a coding agent to make the checkout work and it will make the checkout work, for some definition of work that it chose and did not tell you about. Maybe it handles the failed-card case. Maybe it catches the exception and logs a warning. Maybe it retries three times and then silently marks the order paid. All three make the error go away, which is the objective you actually gave it.
This is not a model failure. It is an underspecified request being interpreted, which is what you asked for.
Every prompt contains a hidden spec
When a request does not say what done means, the gap gets filled — by the model, from context, in whatever direction makes the immediate symptom disappear. That is a reasonable default and a terrible engineering process, because the filled-in parts are exactly the parts you never see. You review a diff, the diff does what the prompt said, and the decision that mattered was made three sentences earlier in a place that was never written down.
- “Make the import faster” becomes a cache with no invalidation story.
- “Handle the error” becomes a try/except that swallows the failure your monitoring needed.
- “Support multiple currencies” becomes floats for money, because nobody said otherwise.
- “Add auth” becomes a check on the client, which is where the check is easy.
Two lines, before the work starts
Acceptance criteria have a reputation as ceremony, which comes from teams that write them by the page. You do not need a page. You need the smallest set of sentences that a stranger could check without asking you anything, and that a model cannot satisfy the cheap way.
Compare a normal request with a specified one:
# The request
Fix the CSV import — it fails on some customer files.
# Done means
- A row with an empty optional column imports; a row missing a required
column is rejected with the row number in the message.
- A file with CRLF line endings and a UTF-8 BOM imports unchanged.
- A duplicate external_id updates the existing record; it never creates
a second one.
- 50k rows import inside one transaction, or none of them do.
- Failures are reported per row, in a summary, not by aborting the job.
# Explicitly not in scope
- Column mapping UI. Async/background processing. Anything about Excel.That took ninety seconds to write and it does four things at once: it names the cases that actually break, it forecloses the lazy implementation, it tells the model which direction to resolve ambiguity, and it gives whoever reviews the diff something to check against other than vibes.
The five criteria worth the time
You do not need every case. You need the ones where a plausible implementation could go the wrong way and still look green:
- 01The observable behavior, from outside.
What a user, a caller, or the next system sees. Not “refactor the parser” — “the endpoint returns 422 with the failing field, and nothing is written.”
- 02The failure case you care about most.
Every task has one. Name it explicitly, because the silent-catch version of your feature always passes the happy-path test.
- 03The data rule that must hold afterwards.
Uniqueness, idempotency, no partial writes, money stays in integers. These are the bugs that cost the most and show up the latest.
- 04How you will know it works.
A test name, a command, a query you will run. If the only verification is “open the app and look,” you will be doing that forever.
- 05What is out of scope.
One line. It saves the review where you have to explain that the extra feature, while nice, now needs its own tests and documentation.
Not in the chat
Criteria in a chat message are gone when the session is. Put them where the work lives — the issue, the PR description, a markdown file in the repo. Then every session, including the one you open three weeks from now with no memory of this conversation, starts from the same definition of done.
The practical payoff is that review changes character. Instead of reading a diff and trying to reconstruct what the author intended, you read five lines and check them. Disagreement moves to where it is cheap: before the code exists, about the sentence, rather than after, about four hundred lines.
A specification is not bureaucracy. It is the part of the work you would otherwise do badly, in your head, twice.
The compounding effect is the real reason to bother. Written criteria are reusable: they become the test names, the PR description, the changelog line, and the context the next task starts from. Criteria that only ever existed in a prompt box produce exactly one diff and no institutional memory at all.
A week of senior engineering against one agreed deliverable — with the finish line written down before Monday. Code in your repo by Friday.
See how the free week works →