6.3 Define "done" with acceptance criteria

Overview and links for this section of the guide.

Why “define done” prevents bad output

Without acceptance criteria, the model has to guess what success looks like. When a model guesses, you get:

  • missing features (“I thought that was optional”),
  • extra features (“I assumed you wanted auth/logging/config”),
  • fragile behavior (“works for the happy path only”),
  • argument-driven debugging (“it should work”) instead of evidence.

Acceptance criteria replace guessing with an observable target.

The vibe-coding upgrade

Vibe coding becomes engineering when “done” is testable.

What good acceptance criteria look like

Good acceptance criteria are:

  • Observable: you can run a command or test and see pass/fail.
  • Complete enough: includes success + failure behavior.
  • Minimal: only what’s needed for this iteration.
  • Unambiguous: avoids subjective language like “fast” or “nice.”

Examples

  • “Given input X, return Y.”
  • “If input is invalid, return exit code 2 and print an error.”
  • “Existing tests pass; add 3 new tests for edge cases.”
  • “Output must be valid JSON matching this schema.”
Avoid “should” language

“The UI should be intuitive” is not testable. Turn it into something observable: “The primary button is disabled until required fields are filled.”

Types of acceptance criteria (practical)

1) Functional criteria

What the system does (inputs → outputs). Example: “When I pass --json, the tool prints valid JSON.”

2) Error and edge-case criteria

How failures behave. Example: “Invalid input returns a validation error, not a stack trace.”

3) Compatibility criteria

What must not break. Example: “Existing CLI flags remain unchanged.”

4) Non-functional criteria (only when needed)

Performance, cost, latency, security, etc. Keep these concrete:

  • “No network calls.”
  • “Must run within 2 seconds for a 1MB input.”
  • “No secrets in logs.”

How to write acceptance criteria quickly

Use this 60-second method:

  1. Write 2–4 happy-path examples (the main thing working).
  2. Write 2–4 failure/edge cases (invalid input, timeouts, empty input).
  3. Write 1 “must not change” constraint (protect existing behavior).

That’s enough to anchor the model.

Small lists beat long paragraphs

Acceptance criteria are easiest for both humans and models when they’re bullet points.

Turning acceptance criteria into tests

When possible, turn acceptance criteria into automated tests:

  • Unit tests for pure logic.
  • Integration tests for CLI/HTTP paths.
  • Golden tests for structured outputs (exact JSON shape).

If you can’t automate yet, write a repeatable manual checklist (“run these 5 commands and compare outputs”).

Tests are a communication tool

When you paste a failing test into the next prompt, the model gets a precise target. That’s why tests increase vibe speed.

An acceptance-criteria prompt template

Task:
[Describe what to build/change.]

Acceptance criteria (must be true):
- [Happy path example]
- [Happy path example]
- [Edge case / failure behavior]
- [Edge case / failure behavior]
- [Must not change / compatibility rule]

Constraints:
- Language/runtime: [...]
- Dependencies: [...]

Process:
- If tests exist, update/add tests to cover the acceptance criteria
- Output diff-only changes
- Explain how each acceptance criterion is satisfied

Common anti-patterns (and fixes)

Anti-pattern: vague “done”

Fix: convert to commands, outputs, schemas, and exit codes.

Anti-pattern: too many criteria

Fix: pick the minimum set that defines correctness for this iteration. Save the rest for future loops.

Anti-pattern: no error behavior defined

Fix: always include at least one “invalid input” criterion so the model implements defensive behavior.

Where to go next