Skip to content

Contracts

GoalLearn the checks that let you trust a run you did not watch.

Told to the agent, enforced by code

A contract is a rule the agent is told about and code checks anyway. The point is not that the agent is careless. The point is that you were not watching, so the only things you can trust afterwards are the ones a program verified.

ContractThe question it answersWhen it fails
scopeDid the unit touch only what it was allowed to?hard stop, task parked
gateDoes the project still pass its checks, as far as this change is concerned?hard stop, task parked
task fileDid each role write only the sections it owns?hard stop, task parked
traceDid the unit print the verdict its stage requires?hard stop, task parked
closingIs each closing criterion actually true, according to code?the task does not close
bounce capHave implementer and reviewers disagreed too often?task parked for a human
one loop per checkoutIs another loop already running here?the loop refuses to start
standing rulesDoes every agent file carry the current shared rules?the runner exits 2
model splitIs the judge on a different model from the implementer and the tester?the runner exits 2
branchBefore a push: is the task branch there, ahead of base, every commit T-NNN:?task parked, nothing pushed
planIs every [closes] command a real command in this project?plan parked, no code yet
probeIs the environment up?exit 4, task untouched

A hard stop ends the unit with exit code 1 and keeps the evidence. It fires on a failed scope, task-file, trace or gate check, when the agent exits with an error, or when a unit runs longer than limits.unitTimeoutMinutes (60 by default). The runner moves the unit's changes into a named git stash, harness quarantine: T-NNN unit N, restores the working tree, and parks the task with the reason. The next unit starts on a clean tree, and you can look at exactly what the agent did. A usage limit is not a hard stop: the unit ends with exit code 4 and the tree is put back as it was.

Scope

The planner writes the scope into the task: a list of paths the implementer may change. For T-002 in the example project, "Do not add the same URL twice", a planner might write this (an illustration, not the output of a real run):

scope:
  - src/lib/bookmarks.ts
  - src/lib/bookmarks.test.ts

After an implement unit, the runner checks that:

  • the branch is still the task branch, and its upstream has not moved;
  • the unit made no more commits than allowed;
  • every committed file and every newly dirty file is inside scope.

The qa, judge and prototype units are read-only. They may write only under .harness/artifacts/T-NNN/. A file anywhere else fails the unit.

A second fence stands behind this one: the agent's tool allowlist (agent.allowedTools) has no git commit and no push. Only the runner commits.

A task file you changed during the unit, by an approve or an answer, is not the unit's doing: the harness keeps a ledger of human writes, and the scope contract excuses a file whose content matches it. An agent's edit of another task file still parks the unit.

Gate, and why narrowing matters

The gate is the command that must pass: the task's gate: plus the project default, usually types, lint and tests.

gate: pnpm typecheck && pnpm test

A plain gate has a trap. Your project already has some lint warnings and one flaky test. The unit changes the bookmark logic and the gate fails on a file it never touched. Now the loop parks good work, again and again, for a defect it did not cause.

So the gate is narrowed to the change:

  • lint owns only the lines the diff changed;
  • a test that fails is re-run at the unit's starting commit, in a throwaway worktree. If it fails there too, the defect was already there, and this unit is not blamed for it.

Task file

The task file is split into sections, and each role owns some of them. The runner parses the file before and after the unit and compares them. A role that wrote outside its own sections, or left the file unparseable, causes a hard stop. The ### Review blocks in ## Plan and ## Prototype are yours, so a unit that edits one is parked too. The roles page lists who owns which section.

Trace

The runner writes the stream-json output of every unit to trace/unit-NNNN.jsonl. The trace contract reads the verdict from the final result event, which is the coordinator's own last word. A specialist's verdict quoted inside a tool result does not count. config.json lists the verdicts each stage requires: the qa unit needs qa: PASS, qa: FAIL or qa: CANNOT_VERIFY, the judge unit needs judge: CONFIRMED or judge: REJECTED.

No required verdict, no progress. And a task can only close with a CONFIRMED judge in its trace. This is also what catches the coordinator that ended its turn too early.

Executable closing

An agent saying "done" proves nothing. The closing contract turns the most important acceptance criteria into commands that code runs.

Every criterion is tagged:

  • [closes] — a shell command, written after a colon. The runner runs it; exit code 0 means the criterion is met. Only these decide whether the task closes.
  • [found] — something checked on the running app, such as behaviour on a page. With a browser (the project sets app and the app started), qa walks each [found] criterion: it does the steps, reads the console and saves one screenshot per criterion in the artifacts folder. A criterion that does not hold is a FAIL, with its screenshot. Without a browser, and for a step a browser cannot do (time passing, another device, an e-mail, a second account), qa checks what a command, a test or the code can show and copies the rest word for word into a For the reviewer list in .harness/artifacts/T-NNN/qa-report.md. That list is never a reason to fail or park. The pull request marks a criterion qa walked with ✓ and its screenshot and lists the rest unchecked, for a human. The judge judges [found] criteria from the code.
  • [process] — something a human does, such as a design review.

qa-report.md starts with one line per criterion in a fixed form, because the pull request is built from those lines: - <n> PASS|FAIL|REVIEWER · browser|command|code · <screenshot file, or ->, for example - 7 PASS · browser · qa-7-toast.png.

Continuing the T-002 illustration:

## Acceptance criteria

1. **[closes]** Adding the same URL twice is rejected: `node --test src/lib/bookmarks.test.ts`
2. **[found]** adding a URL that is already in the list keeps the old bookmark and tells the user
3. **[process]** a human reads the message wording

The loop reads only criteria numbered N. [tag]. A tagged line numbered 1a. or written as a bullet is not read; the plan contract names it, "not read as a criterion, number it N.: …", parks the plan, and harness status shows the message.

The command is the first backtick span after a colon, so names in backticks before it stay plain text. A criterion without a colon falls back to its first span, so older task files still parse. Before any code is written, the plan contract checks every command: its first word, after ! and VAR= prefixes, must pass command -v in the project. A name or a path there parks the plan with "criterion N: word is not a command here", so a typo costs one plan round instead of overruling a correct judge.

The tag works with or without bold; **[closes]** is the canonical form. Aim for one [closes] per task. The first lesson explains why more is worse.

Bounce cap

qa FAIL and judge REJECTED share the counter bounces in the task's frontmatter. The rejection that brings it to limits.bounces, 2 by default, parks the task with the reason instead of sending it back to the implementer. With the default that means at most two implement rounds and two commits. qa: CANNOT_VERIFY parks the task too, but it is not a bounce: it means a [closes] command or the gate could not run, such as a missing tool. Reopen sets bounces back to 0.

An implement round that changes nothing goes on to qa without a commit when the task branch already holds this task's commits; the log says "no change this round". The implementer is told to change nothing when the reason for a rejection is not in the code, so this is how a wrong rejection or a broken check recovers. A round with no work on the branch at all still parks with "implement: no change inside scope".

One loop per checkout

The scope contract diffs the whole working tree, so two loops in one checkout would blame each other's changes. The supervisor holds .harness/trace/supervisor.lock and a second loop refuses to start.

Standing rules

Some rules every agent needs, whatever its role. They live in one file, harness/prompts/STANDING-RULES.md, and harness agents --sync copies them between markers into every base agent. A project agent without the block gets it added. Before a unit, the runner compares every copy with the source. A drifted copy makes it exit with code 2, broken configuration, so no agent runs on outdated rules.

Rule 11 says to run one plain command per Bash call. Under --permission-mode dontAsk, Claude Code refuses $?, $(…), shell variables and mkdir, whatever the allowlist says, so an agent writes its evidence with the Write tool. The runner creates the task's artifacts folder before the unit starts. The allowlist matches how a command starts, so the rule also says: run git from the folder you start in (git diff main...HEAD), never git -C <path>, and change files with the Write and Edit tools, never tee, echo > or cat >. A real run found it: qa and the judge both asked for git -C <path> diff and were refused.

Rule 7 says to hold the turn until every specialist reports. To wait, an agent runs sleep 20 as its own Bash call and then reads the report, never a shell loop (until, while); the default allowlist has sleep for that.

Model split

The judge must run on a different model from the implementer and the tester, for the reason the roles page gives. The runner checks the configured models before a unit and exits with code 2 when they match.

Branch

Only when deliver.pr is on. Before it pushes anything, the runner checks that the task branch exists, that it is ahead of the base branch, and that every commit subject on it starts with T-NNN:, the sign that the harness made it. A failed check parks the task with parked: deliver: branch contract: …, and nothing is pushed. The task lifecycle page describes the whole delivery step.

Environment probe

A project can set a probe: command in config.json. If the environment is down, the unit ends with exit code 4 before the agent starts, and the task stays as it was. Without a probe, an agent would spend a unit discovering that the database is not running.

NextDashboard→
Guide10 chapters · ~55 min
  1. 1Concepts
  2. 2Roles
  3. 3Coordination
  4. 4Task lifecycle
  5. 5Running the loop
  6. 6Contractsyou are here
  7. 7Dashboard
  8. 8Quickstart
  9. 9Your own project
  10. 10Lessons
OverviewDemoExampleGitHub