kadence

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_progress

KADENCE_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 skip

The 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 hookwith it
Used the journal1 of 109 of 10
Right answer9 of 1010 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 --json

It 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 --json

decision 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,estimate

On 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:

codemeans
not_a_repositoryThere is no git repository here. kadence stores its journal inside one.
not_initialisedThe repository has no .kadence/ folder yet. Run kadence init.
no_git_identityGit has no user.email, so an event would have no author.
task_not_foundNo task carries that ULID or KAD-N label. Nothing was changed.
sprint_not_foundNo sprint carries that name or id.
decision_not_foundNo decision carries that ULID or DEC-N label.
unknown_statusThe status is not one of this project’s columns. They are configurable, so read allowed.
unknown_typeThe task type is not one of the four kadence defines.
unknown_priorityThe priority is not one of the four kadence defines.
unknown_fieldA name given to --fields is not a task field. Read allowed.
invalid_argumentAn argument was missing or malformed; the command did nothing.
template_not_foundNo template carries that name.
conflicting_stateThe arguments were understood, but the current state does not allow it — a closed sprint, or one already started.
nothing_readyNothing 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_foundNo milestone carries that name, MS-N label or ULID.
doc_not_foundNo document carries that ULID or DOC-N label.
note_not_foundNo note carries that ULID. A note has no label, so KAD-N and DEC-N never name one.
journal_damagedAn 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, labels

and --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, history

A 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 done

Either both move or nothing changes. A typo does not leave half a board.

On this page