Exit codes and the refusal shape
You will be able to branch correctly on what commission tells you — from a shell script, an agent loop, or an HTTP client — and know which failures are instructions to act on and which are genuine faults.
This page is the single definition of the codes and the refusal envelope. The CLI, the HTTP API, and the MCP server all render the same four outcomes; a surface-specific page that restated them would eventually restate one of them wrong.
The four codes
| code | name | what it means | what the caller should do |
|---|---|---|---|
0 | ok | the action succeeded | continue |
1 | error | something unexpected broke | report it; do not retry blindly |
2 | refusal | the action was understood and declined | read the message and fix it, then retry |
3 | not found | the named project, task, or record does not exist | check the id |
The distinction between 1 and 2 is the one that matters. commission done
refusing because a criterion is unticked is the system working exactly as
designed. An agent that treats 2 as a crash will retry the identical command
forever; an agent that treats it as an instruction fixes the thing it was told
about and moves on.
Codes come from one place — EXIT in src/cli/runtime.ts — and every command
goes through the same wrapper, so a new command cannot invent a fifth code.
What a refusal contains
Three fields, and every one of them exists so the reader does not have to guess:
| field | always present | meaning |
|---|---|---|
message | yes | what was refused, naming the offending thing by id |
missing | when applicable | concrete items, not prose — one per line |
hint | when a fix exists | a runnable command with real ids already substituted |
A hint never contains a placeholder you have to resolve from somewhere else, and
never names a command or flag that does not exist — tests/cli-contract.test.ts
checks hint text against the live command tree.
In text mode a refusal goes to stderr:
cannot complete myapp-1: 1 unchecked criteria; gate 'shipped' missing evidence [pr, test]
missing: criterion 0: signature verified
missing: evidence.pr
missing: evidence.test
hint: tick each with 'commission check myapp-1 0' — only once it is actually true; attach with 'commission evidence myapp-1 pr=<ref> test=<ref>'; or --force to override (logged)With --json, the same refusal is a single JSON object on stderr, and stdout
stays empty:
{
"error": "refusal",
"message": "cannot complete myapp-1: 1 unchecked criteria; gate 'shipped' missing evidence [pr, test]",
"missing": [
"criterion 0: signature verified",
"evidence.pr",
"evidence.test"
],
"hint": "tick each with 'commission check myapp-1 0' — only once it is actually true; attach with 'commission evidence myapp-1 pr=<ref> test=<ref>'; or --force to override (logged)"
}error is the code name (refusal, not_found, error), not the numeric exit
code. Branch on it if you are parsing; branch on the exit status if you are
scripting. They cannot disagree — both are derived from the same CommissionError.
The same contract on every surface
| CLI | HTTP | MCP | |
|---|---|---|---|
| ok | exit 0 | 200 / 201 | tool result |
| error | exit 1 | 500 | isError |
| refusal | exit 2 | 422 | isError, refusal shape in the text |
| not found | exit 3 | 404 | isError |
| stale version | exit 2 | 409 | isError |
HTTP returns 409 Conflict rather than 422 when a write lost an optimistic
concurrency check — the caller did nothing wrong except read a moment too
early:
{
"error": "refusal",
"message": "myapp-1 has changed since you read it: you have version 99, it is now 1",
"missing": ["version 99"],
"hint": "re-read it and decide again: commission show myapp-1 --json"
}Over MCP the body of a refusal is the same three fields, rendered into the tool
result and flagged isError, so a model reads the fix instead of a stack trace.
Branching on it
In a shell:
commission done myapp-1 || echo "exit $?"An exit of 2 means the message on stderr told you what to attach. An exit of
3 means you have the wrong id. Anything else is a fault worth reporting.
Two things that deliberately do not refuse
- Guidance drift. When the files a task was claimed under have changed,
commission warns and exits
0. Refusing would strand work that is otherwise fine. See context and drift. - A non-preferred actor. Preference biases which task an actor is offered; it never reserves one. Being passed over produces an explanation, never a refusal. See execution policies.
Commission refuses about facts it owns. It reports about facts the repository owns.
Related
- Refusals, audited — the twenty an agent actually hits, judged one by one.
- The CLI contract — naming, flags, and output promises.
- JSON output — the shapes on the success path.
- HTTP API — status codes and auth.
positioning.md§6 — the definition of refusal.
Part of Reference.
This page is docs/reference/exit-codes.md in the Commission
repository, rendered in place — the site keeps no copy of it. The repository is private, so there is no edit link to follow.