Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Proof and verifiers

A task is finished when ank done says so, and ank done says so only after it has run the task’s verifiers itself and recorded what ran. This page is where verifiers come from, how a task gets its list, what done writes, and what a proof is worth depending on who produced it.

The examples run in the repository the quickstart builds: a src/auth/session.ts, a tests/auth.sh that exits 0, and a default branch named main.

A verifier is declared once, in config.yml

A task names verifiers; it never carries a shell command. The definitions live in .ank/config.yml, under the repository’s own review, and they go in through the same verb as every other key: writing a run for a name the file does not carry is how a verifier is declared.

$ ank config verifiers.auth-tests.run "sh tests/auth.sh"
verifiers.auth-tests.run (unset) -> sh tests/auth.sh
$ ank config verifiers.no-jwt.run "! grep -rq jwt.verify src/auth/"
verifiers.no-jwt.run (unset) -> ! grep -rq jwt.verify src/auth/

which is what the file then carries:

verifiers:
  auth-tests:
    run: sh tests/auth.sh
  no-jwt:
    run: "! grep -rq jwt.verify src/auth/"

ank config --unset verifiers.no-jwt takes one back out. Reading a key says where the value comes from, this repository or a default the tool resolved:

$ ank config verifiers.auth-tests.run
sh tests/auth.sh
$ ank config verifiers.auth-tests.timeout
10m (default)

The distinction is the reason writing is line surgery rather than a round-trip through a YAML serializer. Your comments, blank lines, key order and quoting survive a write, and a key you never set is never written out: an unset key follows the tool, a written one is pinned here, and a serializer would quietly convert every one of the first kind into the second. Every key a verifier takes is in config.yml keys.

A verifier runs under sh -c from the root of the working tree, on all three operating systems, and passes when it exits 0.

A task names its list when it is written

Declare the verifiers first. A task that names one config.yml does not know is refused at creation, so a name you misremember fails when you write the task rather than at the close:

$ ank new task --title "Migrate auth" --scope "src/auth/**" --verify auth-tests
error[7]: no verifier 'auth-tests' in .ank/config.yml
  -> ank config verifiers.auth-tests.run "<command>"

With the definitions in place, --verify names them one at a time, and a composite criterion is mechanised by several verifiers rather than by one that covers half of it:

$ ank new task --title "Migrate auth to opaque sessions" \
    --scope "src/auth/**" \
    --criteria "The auth tests pass and no reference to jwt.verify remains in src/auth/" \
    --verify auth-tests --verify no-jwt
created TASK-820d259af6a7 Migrate auth to opaque sessions

The default list

--verify one at a time is the right amount of ceremony for a verifier that suits one perimeter and the wrong amount for the suite every task in the repository has to pass. A verifier marked default joins every task written afterwards:

$ ank config verifiers.no-jwt.default true
verifiers.no-jwt.default false (default) -> true

which lands beside the run it belongs to, and nowhere else in the file:

verifiers:
  auth-tests:
    run: sh tests/auth.sh
  no-jwt:
    run: "! grep -rq jwt.verify src/auth/"
    default: true

The read form is the same as any other key, and an unmarked verifier answers false (default) – the tool’s default for the default key, which is the one place the word does double duty:

$ ank config verifiers.no-jwt.default
true
$ ank config verifiers.auth-tests.default
false (default)

A task created now with no --verify of its own carries verify: [no-jwt] in its frontmatter, without anybody naming it. --verify replaces that list rather than adding to it, so a task that names its own verifiers gets exactly those.

Declining it

--no-verify is the third possibility, and it is a judgement, not a shortcut. It writes a task with no verify: at all:

$ ank new task --title "Say in the README what a session is now" \
    --scope "README.md" \
    --criteria "The README describes opaque sessions and names no JWT" \
    --no-verify
created TASK-51c2a0f6d418 Say in the README what a session is now

Use it where no declared verifier can settle the criterion – prose a person has to read, a behaviour only a published release answers – and where that is genuinely the case, say so when you write the task, so the empty list is visible in its diff. A task that reaches done with an empty verify: nobody decided on closes on a proof nothing ran.

What done records

$ ank claim 820d
claimed TASK-820d259af6a7 migrate-auth-to-opaque-sessions -> HEAD
$ ank done
running: auth-tests ... ok (0.0s)
running: no-jwt ... ok (0.0s)
proof recorded: auth-tests@94a1f671c577 -> local/e3b0c44298fc@9c45c50  (scope/18d14da584ab)
proof recorded: no-jwt@791cc818d0ad -> local/e3b0c44298fc@9c45c50  (scope/18d14da584ab)
TASK-820d259af6a7 -> done

One proof entry per verifier, carrying the hash of the verifier definition that executed, a hash of what it printed, the HEAD commit, and a hash of the scope files’ content at that moment. Four of those hashes are content and not identity, so they are the same on your machine as on this page. 94a1f671c577 and 791cc818d0ad are the two verifier definitions; e3b0c44298fc is what each of them printed, which is nothing; and 18d14da584ab is src/auth/session.ts as the quickstart wrote it. The commit is the one your tree is on, so that one is yours.

If a verifier fails or times out, the transition is refused and the task stays where it is.

A task with no verifier needs a proof you hand it. There is nothing for done to run, so --proof becomes mandatory:

$ ank claim 51c2
claimed TASK-51c2a0f6d418 say-in-the-readme-what-a-session-is-now -> HEAD
$ ank done
error[5]: proof required to move TASK-51c2a0f6d418 to done
  -> ank done --proof commit:<sha>

The reverse is refused too: a task that declares verifiers takes no --proof, because what closes it is what ran and not what somebody typed. Give a proof you already hold, commit:<sha>, and never a run id you would have to wait for.

What a proof is worth

The proof types are commit, test, human-review and assertion, and what separates them is not local versus hosted but who controls the environment. A CI reference is out of the agent’s reach and guarantees the most; commit:<sha> is checked with git; a local test proves what ran in a tree the agent could have altered; assertion:"..." guarantees nothing and is marked weak, which is what keeps it from quietly becoming the default path.

The type is half the answer, and the entry records the other half. Every proof carries via: verifier when Ank ran the verifier itself, attested when it arrived on refs/ank/proof/<id>, submitted when a caller typed it, because a run reference is the strongest thing in that list when a pipeline wrote it and the weakest when somebody typed it (ADR-b6b69053a47b). Typing --proof test:<run-id> is still accepted and still recorded; what it does not do is clear the done with no test proof signal, which stays until a pipeline anchors the run. Entries written before the field carry no via and are read exactly as they were.

done records what ran on the machine that ran it, which is a local claim. Anchoring a run is how a pipeline adds the external half after the merge, without a commit.

done proves the task in the working tree it ran in. It does not prove the change merges, or that the combined system works: that gap, and the task that closes it, are Multi-agent work.