brd_ id or by the agt_ id of the agent that owns it. That is not a convenience: a board with no cards has never told anyone its id, so the agent id is the only reference a caller can have on a team that has not written a card yet.
The four statuses are fixed — todo, doing, blocked, done — and are not configurable columns. The reason a card is stuck rides in blocked_by, as data rather than as a lane.
A BoardCard is { id, object, board_id, title, body, note, status, assignee, blocked_by, woken_session_id, reported_status, wake_count, created_at, updated_at }. assignee is a roster member’s name, or null. woken_session_id, reported_status and wake_count are the board’s own bookkeeping, readable and never written by a client: woken_session_id is the ses_ id of the session the board started for the assignee (null until it has), reported_status is the terminal state — done or blocked — the owner was last told about (null while that report is still owed), and wake_count is how many sessions the board has started for the card (at most three).
create
POST /v1/boards. Ensures the agent’s board. There is one board per agent, so a second call is not a conflict — it answers the same board, and the status code is the only difference: 201 when this call minted it, 200 when it found one.
get
GET /v1/boards/{id}. One board — its brd_ id, the agt_ id of its owner, and cards, which is how many stand in each of the four columns. boardRef is either spelling.
A Board is { id, object, agent_id, cards: { todo, doing, blocked, done }, created_at }. The counts are a real aggregate, not the length of a page: a page of cards is truncated and cannot state one.
cards.list
GET /v1/boards/{id}/cards. boardRef is a brd_ id or an agt_ id.
cards.ofAgent
GET /v1/agents/{id}/board. The same cards, reached from the agent, in column order. Creates the board on first read, so it answers an empty page rather than a 404 for a team that has not started.
cards.create
POST /v1/boards/{id}/cards. CardCreate is { title, body?, status?, assignee?, blocked_by?, key? }; everything but the title has a default — todo, unassigned, unblocked.
key (up to 128 characters) makes the create idempotent on its board: the same key always answers the same card, 201 when this call minted it and 200 when it found one, so a seed that runs twice leaves one card, not two. Without a key every call is a new card. Creating with an assignee is a hand-over — see cards.assign for what that starts.
cards.get
GET /v1/boards/{id}/cards/{cid}.
cards.update
PATCH /v1/boards/{id}/cards/{cid}. Every field is optional, so a move is { status: "doing" } and nothing else. assignee: null unclaims the card; omitting the key leaves the current assignee alone.
This is an operator’s override and is deliberately more powerful than the board_write tool an agent calls: it outranks the claim guards that stop two members stepping on each other, and it is the only way to retitle a card.
One rule it does not outrank: { status: "done" } needs a note — a note on the call or one already on the card — and is refused 409 state_conflict (missing_note) otherwise. A done with nothing written is a card the next reader cannot use.
cards.assign
PATCH /v1/boards/{id}/cards/{cid} with { assignee } and nothing else. assignee is a roster member’s name; null takes the card back.
Assigning is how work starts. A todo card with an assignee and no open blocker starts a session of that teammate in the same call, and the returned card records it as woken_session_id. If the teammate is busy with another card on that board, the card waits its turn and starts when that one is done. If that session ends without the card reaching done, the board parks the card as blocked — a hand-over is never silently lost. Unassigning wakes nobody.
cards.comments
GET /v1/boards/{id}/cards/{cid}/comments. A card’s comments, oldest first — they are a conversation, not a feed, so they are not reversed. A cardId that is not on the named board answers 404, the same guard cards.get sits behind.
cards.comment
POST /v1/boards/{id}/cards/{cid}/comments. There is no author field: the server signs the comment with the principal behind your key, so author comes back as your usr_ or key_ id. A member commenting through board_write is signed with the ses_ id of its own thread, which is how a reader tells an agent’s comment from a person’s.