AI coding

Writing Acceptance Checks an Agent Can Run Itself

Antonio Evans8 min read

A task contract document with its verification half highlighted, beside an agent terminal showing a command, an exit code and a PASS result, and a separate branch marked for human review.
Key points

Key points

  1. 1An agent can verify its own work only when each check names an exact command, an expected exit code, and an assertion on the output that a script can compare.
  2. 2Checks that need human judgment belong in a separate manual step, written so the agent has no way to mark it passed.
  3. 3A verification contract lives in files the agent cannot edit, so weakening a test cannot become a shortcut to "done".
  4. 4The most common false pass is a test run from before the agent's last edit, which proves nothing about the code that ships.

What "done" has to mean when the agent decides

The term comes from Scrum: the 2020 Scrum Guide describes the definition of done as a formal description of the state of the increment when it meets the product's quality measures, and work that does not meet it cannot be released (specstory's summary of the definition of done). On a human team, a developer checks their own work against that list, and several items rest on judgment. When an agent does the work, its report on its own work is generated text, not a record of what ran. So every item needs a check that runs apart from the agent and leaves a record.

That gap shows up in practice. One developer audited 516 "done" claims from his own coding agent sessions and found no passing test run behind 65 to 69 percent of them, depending on how strictly they were counted (the audit, as reported by specstory). This is why spec-driven development puts acceptance criteria up front: the spec is the connective tissue linking intent to architecture, implementation, tests and validation, and the lifecycle ends in a validate step rather than a "looks good" (Microsoft on spec-driven development).

The decision rule for this article is simple. If you cannot write a command whose exit code and output decide the question, the item is not an automated acceptance check. Either rewrite the item until it is, or move it to the manual step described further down.

Anatomy of a check an agent can run alone

A runnable check has four fields, and all four are mandatory:

  1. Command: the exact string, copy-pasteable from the repository root, with no variables that only you know.
  2. Expected exit code: almost always 0, but some checks expect a specific non-zero code. A linter exit of 1 means findings, which may be the signal you want.
  3. Assertions on output: patterns that must appear in stdout or stderr, and patterns that must not. An exact count is better than "some".
  4. Evidence to keep: where the output is written so a person can read it after the agent stops.

Exit codes are the reliable half. The execution layer of any agent harness returns structured output, meaning stdout, stderr, exit codes and truncation notices, and that separation between reasoning and execution is what keeps the loop honest (Blaxel on how agents execute code). Sebastian Raschka's breakdown of coding agents makes the same point from the other direction: the harness is a control loop around the model that decides what to inspect, which tools to call, and when to stop (Components of a Coding Agent). Acceptance checks are what give that stop decision a factual basis.

A workable per-task template, stored as a table at the top of the task file:

idcommandexitmust containmust not contain
V1pytest tests/test_export.py -q0"5 passed""failed"
V2ruff check src/export0(empty output)"Found"
V3python -m mypkg.cli export --dry-run0"rows=1200""Traceback"

Three rules make this template survive contact with a real agent. First, quote exact substrings, including counts, because "tests passed" is asserted by almost any harness text while "5 passed" is not. Second, for checks where success means silence (linters, type checkers), assert on empty output rather than letting any output count. Third, never assert on timing, ordering of unrelated lines, or temporary paths. Those change per machine and produce flaky fails that teach the agent to retry instead of fix.

A worked example with numbers

Take a task: "Let customers export their order history as CSV." A weak criterion reads "CSV export works." Here is the same task written so an agent can settle it alone.

The contract, embedded in the task file:

V1  pytest tests/test_export.py -q
    expect exit 0, output contains "6 passed", does not contain "failed"
V2  python scripts/seed_fixture.py --orders 1200
    expect exit 0, output contains "seeded=1200"
V3  python -m shop.cli export --user 42 --out /tmp/o.csv
    expect exit 0, output contains "rows=1200", stderr does not contain "Traceback"
V4  python scripts/check_csv.py /tmp/o.csv
    expect exit 0, output contains "columns=7", "dupes=0"

Now the two possible outcomes. In a pass, the agent runs V1 through V4 in order after its last edit, sees 6 passed, seeded=1200, rows=1200, columns=7 and dupes=0, writes all four transcripts into the pull request, and reports done. In a fail, V3 exits 0 but the output says rows=0. The export ran, the query filtered by the wrong key, and the assertion on the count catches what an exit code alone never would. That distinction is the whole point. An exit code of 0 means the command finished; the assertion means it did the right thing.

A second, subtler fail: V1 passes with 6 passed but the agent had edited tests/test_export.py to drop a parameterized case, so the count should have been 9. This is test tampering, and it is why the definition of done must include a check on the diff itself, for example "the diff deletes no test and marks no test as skipped" (specstory's definition of done guide). A passing run is not evidence if the tests were rewritten to pass.

Teams that run agents through a single API for text and embedding work still keep the acceptance checks as plain shell commands, because the check should be runnable by anyone on the team without the model in the loop.

The gate runs after the report, not inside it

The ordering matters more than the wording. The six-step process that works puts the check outside the agent's message. The team writes the list into files the agent cannot change. The agent edits the code and runs the listed checks. The agent reports that the task is done. A hook or CI job runs the listed checks again on the final code. If a check fails, its output goes back to the agent to fix. When the checks pass, a reviewer reads the recorded evidence (specstory). The hook is the trust boundary. The transcript from the agent is a convenience; the transcript from the hook is the record.

Practically, that means a post-edit hook or a CI job, not a "please run the tests" line in the prompt. Agent instructions get ignored, paraphrased or truncated when context is tight. A CI job does not.

Where the check runsWho can edit itWhat it proves
In the agent's promptThe agentNothing; it is advice
In the repo, run by the agentThe agent, unless excludedSomething, if the diff check confirms tests and config are untouched
Post-edit hook or CI on final codeThe teamThat the shipped code passed
Reviewer reading the hook logThe teamThat the evidence matches the contract

Two further numbers argue for the gate. In a 2026 study of 33,596 pull requests opened by coding agents, failing CI or tests was the most common code-level reason for rejection, at 17.6 percent of rejected pull requests analyzed (specstory citing the study). Also, the most underrated item in a done list is "a passing run on the final code": a test run from before the agent's last edit shows nothing about the code that ships. Both facts point to the same design. Check last, check automatically, keep the record.

Long tasks make this harder, not easier. Agent success rates begin declining after roughly 35 minutes of human-time equivalence, and accumulated context causes long-context degradation and reasoning failures (a survey of long-horizon agent research and the degradation pattern, and the AdaCoM paper on context management). A check that runs after every sub-task, rather than only at the end, is the cheapest defense against an error compounding over an hour.

When the check needs a person

Some acceptance criteria cannot be decided by a machine. "The wording is clear to a first-time buyer", "legal approved the data retention notice", "the chart reads correctly to a colorblind reviewer", "the migration plan matches the on-call runbook". Pretending these are automatable is how agents learn to claim passes they did not earn.

The method has three parts. First, write the manual step as its own numbered item with a named human role and a specific artifact to produce: a screenshot path, a review comment, a signed line in the checklist. Second, make it structurally impossible for the agent to close the item. In practice that means the checklist item is a checkbox in a file the agent's tooling cannot write to, or a required approval in the pull request, or a label applied by a human. Third, let the agent prepare the material for the person. It generates the screenshots, starts the local server, builds the before-and-after comparison, and attaches it to the pull request. The agent's job ends at "ready for review", not "done".

Matt Pocock's walkthrough of an AI coding workflow makes an adjacent point about where humans belong: the workshop covers the full lifecycle from ambiguous requirements to agent-ready plans and running agents that ship production features, with the human spending effort on planning and validating rather than typing code (the workshop video). The same split applies at the level of one task. The agent runs the machine checks; the person runs the judgment checks.

One decision rule for sorting criteria: if two competent reviewers could reasonably disagree, it is manual. If they could not, write the command.

Recording a manual step so it cannot become a pass

The trick is to make the manual item visible in the same list as the automated ones, so the record is complete, while giving the agent no path to write the pass. A workable structure:

# acceptance.yaml, owned by the team
automated:
  - id: V1
    command: pytest tests/test_export.py -q
    exit: 0
    contains: ["6 passed"]
manual:
  - id: M1
    owner_role: product
    artifact: screenshots/export-menu.png
    how_to_verify: "Menu reads 'Export order history', not 'Download CSV'"
    closed_by: human_approval

The agent reads this file, runs the automated section, and attaches the artifact for M1. The gate fails the task until a human approval appears on the pull request, and the merge is blocked on it. The evidence trail then contains both halves: command transcripts with exit codes, and an approval name with a timestamp.

Build a small audit trail even for agent-only items. The definition of done example from specstory asks the pull request to include the hook's log of each command and its exit code, so a reviewer can read back what ran. That single line closes the loop between what the agent said and what actually happened.

Splitting tasks until the checks fit

An acceptance check that needs paragraphs to describe is usually a sign the task is too big. The classic remedy from agile practice is to split work into vertical slices that each deliver a working, demonstrable change across the layers involved, rather than horizontal slices that deliver only one layer (Visual Paradigm on vertical vs horizontal slicing). The same principle applies to verification: a vertical slice ends in an observable behavior that one command can assert. "Build the reporting module" has no check. "The CSV export returns 1,200 rows with 7 columns for user 42" does.

The Humanizing Work guide to story splitting stresses that good stories are small, independent and valuable, and that splitting is a skill teams pick up with practice (the story splitting guide). Applied to agents, the practical version is: split until the verification table has at most five or six rows, each of which a stranger could run without asking you a question.

Two consequences follow from keeping the checks small. First, the agent's context stays short, which matters because context accumulation is a documented cause of failure on long tasks (the AdaCoM paper). Second, a fail arrives early and cheap, which is markedly better than a fail at hour three. When a task legitimately cannot be split, for example a migration that must land atomically, that is exactly where the gate after every sub-task earns its keep.

If your agents are being driven through a skill library or a coding agent's plugin system, put the verification table in the skill itself so the agent looks it up per task instead of you restating it in every prompt. Quanta Skills (https://quantaintelligence.ai/skills) is one place to keep such a library, with the same skill installable into a coding agent and reusable behind an expert agent.

FAQ

FAQ

Can an agent really decide that a task is done without a human?

Yes, for any criterion you can reduce to a command, an exit code and an assertion on the output. The agent runs the check, reads the transcript, and reports on that basis. What it cannot do is judge criteria where two competent reviewers could disagree, which is why those live in a separate manual step.

What if the command passes but the output looks wrong?

That is the failure an exit code alone cannot catch, and it is why assertions on output are mandatory. The worked example above shows the case: the export command exits 0 but prints rows=0, and the assertion on the count catches it. If you assert only on exit codes, you are testing that the program ran, not that it worked.

How do I stop the agent from editing the tests to make them pass?

Put the done list and the verification table in files the agent cannot change, and add a diff check that fails when an existing test is deleted or marked skipped. A passing run against weakened tests is test tampering, and the hook that runs on the final code is where you catch it.

Where should the check run so its output can be trusted?

After the agent reports done, in a post-edit hook or a CI job, and against the final code rather than an earlier state. A run from before the last edit says nothing about the shipped code. The hook's log, with each command and its exit code, goes into the pull request so a human can read it back later.

How do I handle a check that genuinely needs a person?

Give it its own numbered manual item, with a named role, a specific artifact and a human approval as the only way to close it. The agent's job is to prepare the material, such as screenshots or a local server, and stop at "ready for review". The task cannot be marked complete until the approval exists.

Sources

Sources

  1. Definition of done for AI coding agents
  2. A Spec-First Approach to AI-Native Engineering - Microsoft Developer
  3. How Do AI Agents Execute Code?
  4. Components of A Coding Agent - by Sebastian Raschka, PhD
  5. Long-Horizon Planning and Goal Decomposition in AI Agents
  6. Learning Agent-Compatible Context Management for Long-Horizon Tasks
  7. Full Walkthrough: Workflow for AI Coding, Matt Pocock - YouTube
  8. User Story Splitting - Vertical Slice vs Horizontal Slice - Visual Paradigm
  9. The Humanizing Work Guide to Splitting User Stories
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles