Skip to content
commission

Refusals, audited

Commission’s most-quoted principle is that a refusal is a prompt: exit code 2 is not a crash, it is an instruction, and an agent with no human present should be able to read one and know exactly what to do next.

That claim is only worth what the worst refusal in the codebase is worth. This page is the audit: every refusal(), notFound() and thrown CommissionError in src/ was read at once, and the twenty-one an agent actually hits in the RC1 loop were judged one by one against a single question:

Given only this message, with no human to ask and no other command run first, can an agent take the next action?

Passing needs three things. The refusal has to name what is missing as concrete items rather than prose; it has to give the command that fixes it; and that command has to carry real ids, not a placeholder the caller has to go and look up. A hint that says “see commission actors” is a redirect, not a fix — commission already knew the answer and made the caller pay a round trip for it.

The mechanical half of this is enforced by tests/cli-contract.test.ts (see Mechanical rules below). The judgement half is this page, and it is deliberately willing to call a refusal weak.

Verdicts

Twenty refusals, in roughly the order a working agent meets them. Every example is real output captured from the CLI, not an illustration.

1. done with unchecked criteria — strong

cannot complete shop-1: 2 unchecked criteria; gate 'shipped' missing evidence [test, commit]
  missing: criterion 0: checkout works end to end
  missing: criterion 1: payment errors surface
  missing: evidence.test
  missing: evidence.commit
hint: tick each with 'commission check shop-1 0', 'commission check shop-1 1' — only once it is actually true; attach with 'commission evidence shop-1 test=<ref> commit=<ref>'; or --force to override (logged)

Every unmet condition is a separate missing line, and each one maps to a command with the position already substituted. This is the refusal the whole principle is named after and it now earns it.

Was weak until this audit. The hint used to read tick with 'commission check shop-1 <n>'. The positions were sitting in the missing list one line above, and the agent was still asked to work out <n> for itself. Fixed in src/core/transitions.ts.

test=<ref> stays a placeholder on purpose: the value is the PR number or the build url, which is the caller’s knowledge and nothing commission can supply.

2. done with outstanding approval — strong

cannot complete shop-1: 1 outstanding approval(s)
  missing: approval from a human — autonomy is supervised, so a human must accept the work
hint: a person has to accept this — blaze or dana: COMMISSION_ACTOR=blaze commission approve shop-1 -m "<why it is acceptable>"

The missing line explains why judgement is required (the autonomy setting), not just that it is. The hint names the people who can give it and hands back a line that runs as-is.

Was weak until this audit. It used to end — see who can: commission actors, which is the redirect pattern: Commission had the actor list in hand and sent the agent to fetch it. Fixed in src/core/approvals.ts. When no humans are declared, it says so and gives the .commission.json edit instead — the honest answer, since there genuinely is nobody to name.

3. start on a blocked task — strong

cannot start shop-1: blocked by shop-2 (open)
  missing: shop-2
hint: finish the blockers first, or 'commission start shop-1 --force' to override deliberately

Names the blocker, its status, and both branches (do the work, or override knowingly). An agent can act on this without reading anything else.

4. Claiming a task somebody holds — strong

shop-2 is already claimed by agent:b
hint: pick another with 'commission ready', or 'commission release shop-2' if that claim is stale (lease expires 2026-07-28T02:00:00.000Z)

The lease expiry is the fact that decides which branch to take, and it is in the message rather than one command away. This is the model the rest of this page is measured against.

5. Claiming a task the policy forbids — strong

agent:a cannot claim shop-3: autonomy is human_only and agent:a is an agent
  missing: autonomy: human_only
hint: blaze can: COMMISSION_ACTOR=blaze commission claim shop-3 — or change the policy: commission edit shop-3 --autonomy supervised

Was weak until this audit for the same reason as #2: see who may work it: commission actors. It now resolves the eligible pool and names it, so an agent can hand off by name instead of reporting “refused”. Fixed in src/core/execution.ts.

6. A blocking question — strong

shop-2 is waiting on an answer: "Stripe or Adyen?"
  missing: answer to question #1
hint: blaze decides this: COMMISSION_ACTOR=blaze commission answer shop-2 -m "<the decision, and why>" — pick something else meanwhile: commission next --claim

This is the refusal an unattended agent hits when it must genuinely stop, so it is the one that most needs somewhere to go. The second clause matters as much as the first: without commission next --claim the correct behaviour is “halt”, and a halted agent looks identical to a broken one.

Was weak until this audit — it said “a human decides this” and stopped. Fixed in src/core/questions.ts.

7. An agent trying to approve or answer — strong

agent:a cannot approve shop-1: approval is a human judgement
  missing: a human actor
hint: a human runs this — blaze or dana: COMMISSION_ACTOR=blaze commission approve shop-1 -m "<why>"

The refusal that must never be routed around, so the wording is careful: it names who, and never suggests a flag that would bypass it, because none exists. --force overrides commission’s own checks and never a person’s sign-off.

8. An illegal transition — strong

cannot move shop-2 from done to in_progress
hint: done is terminal; 'commission reopen shop-2' first

Two branches, both correct: from done it names the one legal exit; otherwise it enumerates the legal targets (allowed from open: in_progress, done, cancelled). Nothing to look up.

9. Transitioning to the status it already has — strong

shop-2 is already done
hint: nothing to do — it is already closed; 'commission reopen shop-2' if that was wrong

Was the worst refusal in the codebase. It had no hint at all — a bare refusal() on one line, which the old mechanical test could not even see (it only matched multi-line calls). For an unattended agent this is the dangerous shape: “already done” reads as failure, the agent retries, and the loop cannot terminate. The fix says the quiet part — nothing to do — which is the only thing that stops the retry. Fixed in src/core/transitions.ts.

10. --force with no reason — strong

--force requires -m "<why>"
  missing: -m "<why the override is justified>"
hint: commission done shop-4 --force -m "<why>" — the override and its reason both go in the event log, and the next session reads them

Was weak until this audit. The old hint — “the override is recorded in the event log; say why it’s justified” — explained the policy and left the agent to reconstruct the invocation. It now echoes the exact line, with the right verb for the transition being attempted. Fixed in src/core/transitions.ts.

<why> is the one thing commission cannot fill in, which is the point of the refusal.

11. Cancelling with no reason — strong

cancellation requires a reason
hint: commission cancel shop-4 -m "why" — or --superseded-by <id> if another task replaces it

<id> here is a flag value naming a different task the caller has to choose. That is genuinely unknowable, and distinct from a placeholder standing in for an id commission already has. The mechanical test draws exactly that line.

12. A stale write (--if-version) — strong

shop-1 has changed since you read it: you have version 1, it is now 8
  missing: version 1
hint: re-read it and decide again: commission show shop-1 --json

Both versions in the message, and the hint says decide again rather than “retry” — a retry with the same content is the bug this refusal exists to catch.

13. A criterion index that does not exist — strong

task shop-1 has no criterion #7
hint: it has 1, numbered 0–0 (criteria are 0-indexed) — read them with 'commission show shop-1'

Was weak until this audit — it said only “see criteria with ‘commission show shop-1’”. Criteria are 0-indexed, models overwhelmingly guess 1-indexed, and the old message gave an agent nothing to correct the guess with; the natural repair is to run show and re-derive the convention. Stating the range ends it in one step. Fixed in src/core/tasks.ts.

14. An unknown gate — strong

unknown gate 'nope' for project 'shop'
  missing: gate profile 'nope' in project 'shop'
hint: known gates: shipped (test, commit) — use one of those, or define this one: commission gates --set nope=test,commit

Was adequate, now strong. It listed gate names but proposed commission gates --set nope=<kind>,<kind> — asking the caller to invent an evidence vocabulary the project had already established. It now shows each gate’s kinds and seeds the definition from the ones in use. Fixed in src/core/tasks.ts.

15. Malformed evidence — strong

evidence must be kind=value, got 'pr14'
  missing: a '=' in 'pr14'
hint: gate 'shipped' on shop-1 wants [test, commit] — e.g. commission evidence shop-1 test=<ref> commit=<ref>

Was bare until this auditevidence must be kind=value, got 'pr14' and nothing else. It now reads the task’s own gate profile, so the correction is not just the right shape but the right kinds. Fixed in src/cli/commands/work.ts.

15a. Evidence that is a placeholder — strong

evidence that says 'pending' is not evidence
  missing: commit: 'pending' is a placeholder, not a commit
hint: attach the real reference — commit=9c4473f — or leave it off until you have it; the gate refusal will keep telling you what is missing. If the work genuinely has no such reference, commission evidence shop-1 commit=<why there is none> --force -m "<why>".

Added after this audit, because the audit itself missed it. Every refusal above checks a shape; none checked a value, so commit=pending satisfied a gate that wanted a commit and done proceeded. Fifteen tasks in commission’s own repository were closed that way, including tasks gated on released.

The hint does three things in order of what the caller most likely needs: the real value, permission to attach nothing at all, and the override for the case where there genuinely is no such reference. The middle clause matters most — without it the refusal reads as “you must produce a commit now”, and the honest answer is usually “finish the work first”.

Deliberately not a per-kind format validator. Whether gates should declare shapes is a real design question; guessing at what a SHA or a URL looks like would refuse legitimate values, and a refusal that cries wolf gets --forced until it means nothing. Only bare placeholders are refused — pending review of PR 14 is a real note about state and is accepted.

16. bind with no target — strong

a binding needs the external object shop-1 maps to
  missing: one of --jira <KEY>, --pr <owner/repo#number>
hint: e.g. commission bind shop-1 --jira RTB-543, or commission bind shop-1 --pr Row-Labs/gulfline#17 — 'commission branch shop-1' names a branch whose PRs auto-bind

Was bare until this audit (pass --jira <KEY> or --pr <owner/repo#N>). The third clause is the valuable one: it points at the path where binding happens by itself, which is usually what the caller should have done.

17. A dependency on itself — strong

shop-1 cannot depend on itself
hint: name the task it actually waits on: commission dep shop-1 --on <other-id> — 'commission list' shows the candidates

Was bare until this audit. <other-id> is honest: the whole content of the mistake is that Commission does not know which task was meant, and the hint names the command that lists them. Fixed in src/core/tasks.ts.

18. An apply document that references an unknown key — strong

apply: reference 'ghost' is neither a key in this document nor an existing task id
  missing: a task with key or id 'ghost'
hint: keys in this document: a — add a task with key 'ghost', use one of those, or use a real id ('commission list --project shop' shows them)

Was bare until this audit. Listing the document’s own keys is what makes it actionable: the error is almost always a typo against a key that is right there. Fixed in src/core/apply.ts.

19. No project resolved — strong

multiple projects exist: shop, billing
hint: name one, e.g. --project shop — or bind this repo to it once: commission config init shop

Was weak until this audit. The message listed the slugs and the hint then said pass --project <slug> — a placeholder for a value printed on the line above. Fixed in src/cli/config.ts and src/adapters/github.ts.

20. A workspace with no free ports — acceptable, with a caveat

no free block of 3 ports for shop-1 in [4000, 4009]
  missing: 3 blocks of 3 in the range, all held or in use
  missing: 4 ports held by other prepared workspaces
hint: release a workspace you are done with (commission workspace list, then commission workspace release <id>), or widen workspace.ports.range in .commission.json

The diagnosis is excellent — it distinguishes “the range is too small” from “other workspaces are holding them”, which are different fixes.

The caveat, stated rather than hidden: commission workspace release <id> is a placeholder, and it is the one case on this page where that is defensible. Which workspace to release is a judgement about work in flight, not a lookup; commission cannot choose, and the hint names the command that shows the options immediately before it. It stays under review — if workspace list ever ranks by idleness, this hint should name the top candidate outright.

What did not survive

Three patterns were treated as defects across the whole codebase, not just in the twenty-one above:

The redirect. — see who can: commission actors, see who may work it: commission actors. Commission had the answer loaded and charged the caller a command to get it. Every instance now resolves the list and names it.

The reconstructed command. A hint that explains the policy (“say why it’s justified”, “a human must run this”) and leaves the agent to assemble the invocation. Every instance now prints the line.

The bare refusal. Fourteen construction sites had no hint and no missing at all. Most were single-line calls, which the old mechanical test structurally could not see — the check matched only calls that closed on their own line, so the shortest and most neglected refusals were exactly the ones exempt from it.

Mechanical rules

tests/cli-contract.test.ts walks every error-construction site in src/ with a bracket-balancing scanner rather than a regex, because refusals nest template literals, ternaries and ${} interpolations that defeat pattern matching. Five rules hold:

rulewhat it catches
every refusal carries a hint or a missing lista refusal with no remediation at all, single-line or not
no hint names a command that is not in the treecommission unclaim after the command was renamed
no hint names a flag that is not in the tree--reason when the flag is -m
a refusal that interpolated an id must not then ask for <id>commission show <id> in a refusal that just printed the id
the scan itself found >80 sites, including single-line onesthe check silently going blind, which is how rule 1 failed before

The fourth rule is deliberately narrow: it fires only on a positional argument to a commission command, so --superseded-by <id> (#11) and test=<ref> (#1) — values only the caller can know — are not flagged.

None of this can check whether a hint is good. That is what this page is for, and it should be re-read whenever the command surface moves.

Explainability: the same rule for automatic actions

A refusal explains why Commission would not act. The mirror obligation is explaining why it did — and for anything commission does on your behalf, that explanation has to survive in the event log, because it cannot be recomputed later. The PR is closed by then, the frontier has moved, the config has changed.

tests/explainability.test.ts holds the line. Four automatic actions were not answerable from the log before this audit:

  • next --claim wrote { via: "next" } and nothing else. The whole point of the command is that commission chooses for you, and the choice was unrecorded. It now carries the selection policy, the field values it compared, every candidate it passed over with the reason, and the routing sentence naming who else was eligible.
  • commission start silently claimed the task. A lease appeared with no event behind it, so “who took this?” had no answer. It now writes a claim event saying it was the start that took it, and names the expired holder it took over from.
  • commission sync logged counts — { changes: 2, divergences: 1 }. Which tasks moved, why, and what the drift warnings said were all absent. The payload is now itemised: each change with the external fact that forced it, each divergence with its sentence and its fix, each planned outbound action with its reason.
  • auto-close on merge recorded PR acme/app#1 merged — the fact, but not the setting. A task closing itself is the most surprising thing commission does, and the answer to “why did this close?” is a config line, so the reason now names integrations.github.autoCloseOnMerge as well as the PR.

The invariant the test enforces is narrow: a transition marked via must carry a reason. via is precisely the marker for “machinery did this, not the caller” — a caller-driven transition needs no reason, because the caller is the reason and the actor column already records it.

Two things are deliberately not logged, and should stay that way. Delegation suggestions and routing explanations are derived on every call and never written: a suggestion cannot go stale if it is never stored, which is the same reason blocked is derived from dependencies. Drift warnings are likewise derived — but their input, the context fingerprint the claim was taken under, is stamped on the claim event, so the warning is reconstructible from the log even though the warning itself is not in it.

This page is docs/reference/refusals.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.