For AI agents
The published JSON contract — every command, every field, every error code — and what one answer costs.
The contract
Every command speaks --json. Every response carries schema: "kadence/v1".
stdout is JSON and nothing else; warnings go to stderr, so a pipe stays clean.
kadence board --json
kadence task show KAD-1 --json
KADENCE_SOURCE=agent kadence task move KAD-1 in_progressKADENCE_SOURCE=agent records who acted, so the history distinguishes a person
from an AI agent afterwards. Without it an event counts as human — kadence does
not guess.
Start a session with what changed
Since 0.4, two commands carry the part of the context that moves. kadence prime is the session preamble — the work you have claimed, how many tasks are
ready, decisions in force by title, the last few notes, the active sprint only
when there is one, and the four commands to go deeper — held to 40 lines and 3 KB by a
test, because it goes into an agent's context every session. kadence ready --json is what can be started now: open, unblocked, unclaimed, seven fields per
task. kadence task claim takes the top of that list in one step; there is no
lock, and a second claim from another branch is kept and shown as contested
rather than rejected.
kadence prime
kadence ready --json
kadence task claim # the top of ready
kadence init # also adds the prime and prompt hooks; --no-hooks to skipThe static part still lives in AGENTS.md and does not grow. Since 0.5 it
teaches writes as well as reads — task claim, note "…" --task and
decision add "…" --why sit beside prime and ready — and it is still 23
lines and 1.2 KB, under a test that fails at 25 lines or 2 KB. An older section
is replaced in place by its version marker, not appended to.
The hook is written for a teammate's machine, not yours. It runs kadence prime when the binary is on PATH and otherwise prints one line asking the human
to install it, exiting 0, so a session never opens on an error. It does not
fall back to npx: a hook that init commits into a shared repository must not
download code on someone else's machine, and the section in AGENTS.md tells an
agent the same — ask the human, the network is their call. init on a
repository with the older hook upgrades it in place rather than adding a second.
The journal comes to the prompt
Telling an agent to search did not make it search. On ten questions about this
repository's own history, 0 of 10 agents searched first
after CLAUDE.md said to, and every one opened with grep. So since 0.8.0 init
adds a second hook: on each prompt, kadence hook prompt searches the journal
and puts up to three records, 1 KB at most, next to the question. A record is
shown once per session, a passage is quoted only when its record holds the whole
question, and nothing is printed when the journal has no words for it. The hook
always exits 0, so a failure never costs anyone their prompt.
| without the hook | with it | |
|---|---|---|
| Used the journal | 1 of 10 | 9 of 10 |
| Right answer | 9 of 10 | 10 of 10 |
| Context added, median | ~10.9k tokens | ~7.9k tokens |
One repository, ten questions, one model. The hook's output was placed in the
prompt by hand, because subagents do not fire hooks, so this measures what the
records do once they arrive. A signal, not a benchmark. The hook is Claude Code
only; Cursor and Codex read the same section in AGENTS.md, which we have not
measured.
Ask the tool what it does
The contract is not prose you have to trust:
kadence schema --jsonIt returns every command the binary ships, the fields you can rely on, every error code, the exit codes and the environment variables — about 17 KB, and it works outside a repository, because an agent asks what a tool does before it has a project to ask about.
Three tests keep it honest: the published error codes must equal the ones the
code can emit, every field it promises must appear in real command output, and
every command it names must actually run. A fourth, since 0.5, reads the
binary's own commands and actions and fails on any the contract leaves out —
0.4 shipped 21 of them unlisted, sprint start and task cancel among them.
Renaming a field fails the build instead of breaking consumers quietly.
What the promise covers
The contract is deliberately narrower than the output. Fields kadence emits
but does not list may still change; what is listed is a promise. Within
kadence/v1, fields and codes may be added — never renamed, never removed.
What one call returns
{"schema":"kadence/v1","ok":true,"task":{
"label":"KAD-1","status":"in_review","estimate":3,
"loggedHours":4.5,"blockedBy":["01M1PN2NVGQFD…"],
"comments":[{"author":"ana","ts":"2026-09-02T09:14:00Z",
"text":"The redirect drops the cookie, not the session."}],
"history":[
{"type":"task.created","actor":"ana","ts":"2026-09-01T10:02:00Z"},
{"type":"task.moved","actor":"ana","ts":"2026-09-01T14:40:00Z",
"data":{"from":"backlog","to":"in_progress"}},
{"type":"task.blocked_by_added","actor":"agent","ts":"2026-09-02T09:20:00Z"}
]}}That is the whole state of a piece of work in one call — no server to ask, no context to rebuild.
And it stays one call. That answer is 982 bytes whether the project holds ten tasks or a thousand, while the journal behind it grows from 5 KB to 528 KB. The cost of asking does not grow with the history that makes the answer worth having. Measured in Probe C.
The reasoning, not just the state
task show --json carries decisions and docs; both are always arrays, so
an agent never has to branch on their absence.
kadence decision list --json # what still holds
kadence decision list --all --jsondecision list returns decisions in force. A reversed one is only in the
--all history, because an agent quoting a superseded reason as current is worse
than one with no memory at all. See Recording why.
Each decision also says who wrote it: source is "human" or "agent", from
KADENCE_SOURCE when the event was recorded. An agent reading the reasoning can
tell whether a person made the call or a previous session of an agent did.
Ask for the fields you read
A board of a thousand tasks is 803 KB in full, because every task is emitted in full in every column. Narrow it:
kadence board --json --fields label,status,assignee
kadence task list --json --fields label,status,estimateOn a 200-task board that is 130,799 bytes down to 11,015. An unknown name fails
with unknown_field and the list of what exists — the selectable set is also in
kadence schema --json.
When a call fails
A failed --json call is machine-readable too:
{"schema":"kadence/v1","ok":false,"error":{
"code":"unknown_status",
"message":"Unknown status \"nonsense\".",
"received":"nonsense",
"allowed":["backlog","todo","in_progress","blocked","in_review","done","cancelled"]}}code comes from a closed list — this one, generated from the binary rather
than copied out of it. The hand-typed version of this paragraph named ten of
them for a whole minor release, and nothing failed when the eleventh shipped:
All 18 of them, from kadence 0.11.0:
| code | means |
|---|---|
not_a_repository | There is no git repository here. kadence stores its journal inside one. |
not_initialised | The repository has no .kadence/ folder yet. Run kadence init. |
no_git_identity | Git has no user.email, so an event would have no author. |
task_not_found | No task carries that ULID or KAD-N label. Nothing was changed. |
sprint_not_found | No sprint carries that name or id. |
decision_not_found | No decision carries that ULID or DEC-N label. |
unknown_status | The status is not one of this project’s columns. They are configurable, so read allowed. |
unknown_type | The task type is not one of the four kadence defines. |
unknown_priority | The priority is not one of the four kadence defines. |
unknown_field | A name given to --fields is not a task field. Read allowed. |
invalid_argument | An argument was missing or malformed; the command did nothing. |
template_not_found | No template carries that name. |
conflicting_state | The arguments were understood, but the current state does not allow it — a closed sprint, or one already started. |
nothing_ready | Nothing can be started right now: the board is empty, or every open task is blocked or claimed by someone else. The message says which. |
milestone_not_found | No milestone carries that name, MS-N label or ULID. |
doc_not_found | No document carries that ULID or DOC-N label. |
note_not_found | No note carries that ULID. A note has no label, so KAD-N and DEC-N never name one. |
journal_damaged | An event file could not be read, so the state is missing whatever it held. events.damaged names the files; git has every version of them. |
Every task in a --json response carries these, always, whether or not they
hold anything:
id, label, title, status, type, priority, estimate, assignee, blockedBy, loggedHours, claimedBy, contestedBy, labelsand --fields selects from these:
id, label, title, description, type, priority, status, labels, assignee, reporter, sprint, milestone, loggedHours, parent, blockedBy, due, claimedBy, contestedBy, criteria, openCriteria, comments, estimate, historyA hint naming a next command is added where one helps. allowed is there whenever the valid set is knowable, and it matters most for
statuses: they are configured per project, so no documentation can tell an
agent what yours are. The agent fixes its own input in one step.
There is deliberately no retryable field. With no network and no lock, the
same input always fails the same way — the field would be a constant dressed up
as information. A test asserts its absence so it does not get added absently.
Exit codes: 0 success, 1 the command understood you and could not comply,
2 bad arguments and nothing was attempted.
No bridge required
init writes the kadence section into both AGENTS.md and CLAUDE.md.
AGENTS.md is the cross-tool convention that 30+ agents read; Claude Code loads
CLAUDE.md and does not read AGENTS.md, so a repository carrying only the
first is invisible to the largest agent audience. Whatever a human wrote in
either file is left untouched, and a repeat init does not duplicate the
section. The reasoning is in ADR-009.
There is no server to run, no token to issue, and no network call to make.
An MCP wrapper stays on the roadmap as an optional package. We measured it: about 700 tokens a session over the instruction-file path — not the saving the industry benchmarks suggest, because kadence is sixteen short commands and not a server exposing dozens of tools. The reasons to keep it optional are that it works for agents with no MCP client at all, and that the core keeps zero runtime dependencies.
Why the CLI is the only bridge
An agent knows a task as KAD-3, because that is what humans write in commits
and pull requests. Searching the journal for it returns nothing:
$ grep -rl "KAD-3" .kadence/events/
$ (no matches)Identity is the ULID; KAD-N is a label derived while folding and never stored
in an event. So an agent holding a human's reference has two options: fold the
whole journal itself, or make one call. That is not an optimisation — it falls
out of a decision made for correctness.
Bulk is all or nothing
kadence task move KAD-1,KAD-2 doneEither both move or nothing changes. A typo does not leave half a board.