Quickstart
By the end of this page your repository holds one binding constraint, one
finished task, and a proof that Ank wrote after running the verification
itself. It takes about ten minutes, and nothing here asks you to read the
specification. It assumes ank answers ank --version; Install
gets it there.
Every command and every output below was run against a fresh repository, and the suite replays them against the binary on every change. Where the tool refuses, the refusal is shown as it appears. Each step says what it does and links to the page that explains it whole.
Initialise a repository
Three files are in the tree before anything starts, because every scope and every verifier below is pointed at one of them. These are their exact contents, one line each, and they are written out because two of the hashes further down are hashes of them:
src/auth/session.ts export function createSession(id) { return store.put(id) }
tests/auth.sh exit 0
README.md # auth service
What matters is that all three exist. A scope matching no file and a verifier
whose command is not there are the two ways this walk ends in red, and neither
says anything until ank done, a dozen commands later.
From the root of that repository:
$ ank init
created .ank/entities
wrote .ank/config.yml
wrote .gitattributes
wrote .gitignore
pointer added to AGENTS.md
refspec added: +refs/ank/*:refs/ank/*
The last line is the one a repository with no origin does not print:
there is no remote to add a refspec to, so init reports five effects instead
of six and the other five are identical. Re-running changes nothing either way:
init is idempotent and says already initialised, nothing to do. Whether you
want an origin at all is a question about coordination, and Claims and
identity answers it.
One directory is created, not one per kind: entities live flat in
.ank/entities whatever they are (ADR-c9f9d0d6f05d), and a task, an ADR, a
spec and a log entry are told apart by a field rather than by a folder. The
.gitattributes line keeps .ank/ in LF on checkout: on Windows git would
otherwise convert back to CRLF everything the tool has just written, on every
clone. The .gitignore line is .ank/index.db, the derived SQLite index: it
is rebuilt from the files whenever it is missing, so committing it would only
track a binary that every command rewrites. The refspec is what makes claims
travel, since hosts do not fetch non-standard refs on their own. Both git files
are appended to, never replaced, so an existing .gitignore keeps everything
already in it. The file format describes the whole layout.
Name your default branch. It is the one key init leaves unset on purpose:
it runs where the reference branch is not known yet, and writing main there
would be exactly the guess the tool refuses everywhere else. .ank/config.yml
is written through the CLI rather than by hand:
$ ank config default_branch main
default_branch (unset) -> main
Without it, Ank looks for refs/remotes/origin/HEAD, and a repository with no
remote has none. It refuses rather than guessing, and here is what the verb two
sections down would have said (06d2 is the ADR you write there):
$ ank accept 06d2
error[9]: default branch indeterminable (default_branch absent from .ank/config.yml, refs/remotes/origin/HEAD absent)
-> git remote set-head origin -a
-> or ank config default_branch <name>
A clone sets refs/remotes/origin/HEAD for you, so a repository with an
origin resolves the branch without the key; setting it anyway costs one line
and removes the difference between the two.
The two kinds this page uses
Flat in .ank/, markdown with YAML frontmatter. Four kinds exist and this page
needs two of them; the other two are a spec, normative text that describes
rather than binds, and a log entry, written once and never transitioned.
An ADR is a decision that constrains code. Its constraint is the one
field injected into an agent’s context, so it is short and imperative; the body
holds the reasoning and costs nothing at injection time.
A task is a unit of work. Its done_criteria says what would prove it
finished.
Both carry a scope: a list of globs, and the only thing that joins the two
kinds. There is no epic, no parent, no label. “Everything about the auth
migration” is answered by ank context src/auth/. The full field list is in
Entity fields; you do not need it to work through this page.
Write the first constraint
$ ank new adr --title "Opaque sessions rather than stateless JWT" \
--scope "src/auth/**" \
--constraint "Do not introduce self-contained JWTs for user auth. Every session goes through the Redis store."
created ADR-06d29e727d24 Opaque sessions rather than stateless JWT
It is created proposed, which means visible but not binding. Promotion goes
through one command, and that command is the only one in the tool that makes a
git commit:
$ git add -A && git commit -m "adr: opaque sessions"
$ ank accept 06d2
accepted ADR-06d29e727d24 -> 9c45c50
Short prefixes work everywhere an id is accepted; an ambiguous one is an error
listing the candidates, never a guess. The commit it produced carries the hash
of constraint and scope at acceptance:
ratify ADR-06d29e727d24
constraint+scope: c5d4f3478ad5
by: human:marie
That hash is the anchor. Editing the constraint afterwards does not change it,
which is how ank check notices. accept runs on the default branch only, and
there is no flag around it: a constraint ratified on a feature branch would bind
on that branch alone. How a ratification reaches a protected default branch is
Ratifying, and how others can verify it is Signing
keys.
Declare the verifiers, then the task
A task names verifiers; it never carries a shell command. The definitions live
in .ank/config.yml, and 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/
Then the task, naming both:
$ 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
A composite criterion is mechanised by several verifiers, not by one that covers half of it, and all of them must pass. What else a verifier can be told, the list a task gets when it names none, and the task that deliberately has no verifier at all are Proof and verifiers.
Orientation
ank context is the first call, and the only one you have to remember. With no
argument it covers the whole repository; with a path it covers that perimeter.
$ ank context src/auth/
CONSTRAINTS (1 active)
ADR-06d2 Opaque sessions rather than stateless JWT
TASKS (1)
TASK-820d [open] Migrate auth to opaque sessions
> ank claim TASK-820d to start
Before a claim a constraint is one line, and that line is its title. What
is being answered here is which perimeter to enter, and a survey that spent its
whole budget on constraint text would answer it worse. The constraint itself
arrives with the claim, below, where the perimeter is settled and the rule is
what you are about to be held to; ank show 06d2 prints it whole at any time.
Constraints come first in both forms, and once you are working they are never
truncated. The output ends with the next command, as every output here does.
Claim
$ ank claim 820d
claimed TASK-820d259af6a7 migrate-auth-to-opaque-sessions -> HEAD
The task moved to in_progress, a claim ref now says who holds it, and its
done_criteria was frozen by hash where the file’s editor cannot reach it.
claim also sets HEAD, so the following commands need no id. What a claim is,
how long it lasts, and why a second session needs an identity of its own are
Claims and identity.
Run ank context again and the output inverts: no other task, the full
criterion, and the constraints matching this task’s scope.
$ ank context
TASK-820d Migrate auth to opaque sessions
DONE_CRITERIA
The auth tests pass and no reference to jwt.verify remains in src/auth/
CONSTRAINTS (1 active)
ADR-06d2 Do not introduce self-contained JWTs for user auth. Every session goes through the Redis store.
ank show 820d gives you the entity whole, frontmatter, body and log, which
is where the reasoning behind a task lives.
Work, and log what you learn
$ ank log "jwt.verify removed from session.ts"
logged LOG-6b0f39d7a4c1 on TASK-820d259af6a7
The log is a work trace, not proof. Write to it when you discover something,
not when you finish: it renews the claim, so working is what keeps the lock and
there is no heartbeat command to remember. Each entry is an entity of its own,
which is why the line names one, and ank log 820d with no message reads them
back, newest first. How entries are stored is the file
format.
Finish
$ 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
This is the point of the tool. Ank ran the verifiers itself and wrote what
actually ran; nobody reported their own result. Never set status: done by
hand: a status written by the party being measured measures nothing. What each
hash on those lines is, and what done asks for when a task has no verifier,
are Proof and verifiers.
Commit, and read check
Ank never commits, except accept. Everything else writes files and leaves
them in your working tree, so the corpus travels through your normal review
like any other change. Before you commit, check has something to say:
$ ank check
signal: ADR-06d29e727d24: ratified by its own author (human:marie)
signal: TASK-820d259af6a7: finished on another branch, main has not caught up
signal: allowed_signers: no ratification key declared: permissions are advisory, not enforced (§8)
signal: corpus: 4 entity file(s) differ from main: this checkout does not carry the corpus the default branch does (git merge main)
check: ok — 1 tasks, 1 adr, 4 signal(s)
Commit, and the two signals about the uncommitted work go:
$ git add -A && git commit -m "the migration is done"
$ ank check
signal: ADR-06d29e727d24: ratified by its own author (human:marie)
signal: allowed_signers: no ratification key declared: permissions are advisory, not enforced (§8)
signal: corpus: 3 hot entities are cold and belong in .ank/archive/entities/, with every entry about them (ank archive)
└── ank archive --dry-run lists them and moves nothing
pruned refs/ank/claims/TASK-820d259af6a7
check: ok — 1 tasks, 1 adr, 3 signal(s)
That is the green this page set out to reach, with an origin and without:
signals, no fault, exit 0. A signal is something a reader should see and never a
failure; exit 8 is reserved for findings. What each of those lines means, and
why check just pruned a ref, is Reading ank check.
When a command refuses instead, the exit code carries the meaning so a script can route without parsing anything, and the message always ends with the exact command to run next. The codes are the exit-code reference.
Starting where there is already code
Everything above happened in an empty directory, which is the one repository
nobody has. ank init on two years of history leaves you with a corpus and no
content, and the question it raises is not how to write an ADR: it is which
decisions this code has already made, and which of them were worth writing down
all along. That reading is an agent’s work, and these are the three prompts that
ask for it. Both installers offer to print the same three, character for
character, and a test holds the three copies together.
Run this once, at the root of the repository:
ank init
Then paste each prompt into your agent, one at a time, reading what comes back before you send the next. They follow the three moments of an adoption: what the code already decided, what it still owes, and whether the answer holds.
What the code already decided. This is the one you judge the tool on. A constraint you recognise on sight is a constraint that was true and unwritten, and the reason it now has an id is that the next agent reads it without being told.
Read this repository and write, as ank ADRs, the decisions its code
has already made: the ones a newcomer would break without knowing
they existed. One ADR per decision, each with a scope glob covering
the files it binds and a constraint stated as a rule. Leave them
proposed; I ratify them myself.
They land proposed, which binds nobody. ank review lists what is waiting, and
ank accept <id> stays yours: a signed act, on the default branch, one at a
time. An agent proposes and says it is waiting.
What it still owes. Intentions scattered across TODOs, an issue tracker and a
README are not work an agent can take. A scope and a criterion are, which is what
ank claim freezes and ank done measures.
Read the TODOs, the open issues and the README of this repository,
and turn what they promise into ank tasks. Give each one a scope
glob and a done_criteria a test could settle, and use blocked_by
only where a task genuinely waits on another.
Whether the answer holds. The third is the uncomfortable one, and it is meant to be: a constraint the code already breaks is the ordinary result of writing down what was implicit, not a sign the first prompt went wrong. What you want out of it is the list, before anybody starts repairing.
Run ank check and ank review here, then read every ADR back against
the code its scope matches. Tell me which constraints the code
already breaks and which scopes match no file, and change nothing
until I have read your answer.
None of the three is a step the tool waits on. A corpus with nothing but the ADRs
from the first prompt is already worth ank context, and the other two can wait
until you want them.
Why it works this way
You have now done the loop once, which is the right moment for the four claims underneath it. Each one is a property of the tool rather than a convention you are asked to keep.
Scope, not hierarchy. Constraints and work are two planes joined only by globs. A rule written last year binds work created today, and a glob is verifiable against the filesystem where a label is not. There is no epic, no parent and no rollup to keep in step.
Nobody declares themselves done. An agent that reports its own result can
simply be wrong, so ank done runs the verifiers itself and records what
actually ran, hashed.
Freezing is verifiable, not defended. The CLI cannot stop you editing a file
and does not pretend to. Frozen fields are anchored by a hash the editor does not
control, and ank check compares. Editing a criterion to unblock yourself
unblocks nothing; it makes the divergence visible.
Git does the hard parts. Claims are git refs, so the compare-and-swap that arbitrates two agents is the one git already guarantees. Undo, history and recovery are git’s, and there is no server to run.
One call is bounded at 8000 characters by default, roughly 2000 tokens, which is
the constraint every one of those choices is paid for by: what context serves
has to fit in a context window beside the code. The normative text behind all of
it is the specification; ank help lists every verb, and
ank help <verb> answers about one.