kanban
v0.1.13File-backed kanban board — tickets, threaded comments, agent assignment, the kanban:change and kanban:comment trigger types, and an injectable console board with agent profiles for planning and review.
Install
iii trigger compose::add worker=kanban@0.1.13Run in your project with the iii engine running. Exact versions are immutable and digest-pinned.
skill doc
ticket-worker
how-toWorking a ticket you own
You were dispatched onto a ticket by someone who cannot talk to you: this engine has no
harness::send (confirm with engine::functions::list { "prefix": "harness::" }). Every
instruction you get arrived in your task or in a ticket comment, and every answer you give has
to land as a ticket comment. Your chat is not a channel.
Open these first
kanban::config::info {}— the real column ids (never assumein_review/done), the key prefix, and the board file path.engine::functions::info { "function_ids": ["kanban::ticket::get", "kanban::ticket::update", "kanban::comment::create", "kanban::comment::list", "kanban::ticket::move", "engine::register_trigger", "harness::triggers::list", "harness::triggers::unregister"] }— the exact schemas behind every call below.directory::skills::get { "id": "kanban/tickets/kanban-tickets" }— the deep reference: comment threads, the activity timeline,kanban:change, the soft-delete rules. Fetch it when you need any of those; skip it otherwise.kanban::ticket::get { "id": "KAN-8" }— read the ticket you were dispatched onto, whole, itscommentsandactivityincluded, before you change anything. The key (KAN-8) and the uuid both work, case-insensitively.
The loop
Your profile id is the string in your frontmatter (backend-engineer, frontend-engineer).
Use it verbatim as actor on every write and author on every comment — it is also the string
the wake filters key on, so a typo silently breaks the loop.
- Claim.
kanban::ticket::update { "id": "KAN-8", "assignee": ".", "status": "in_progress", "actor": " " } - Arm your wake and its teardown — before you report. See The pair below. Reporting first is the failure this step exists to prevent: the reviewer's answer arrives after your turn ended and wakes nobody.
- Work, then verify it the way your own profile requires.
- Report on the ticket —
kanban::comment::create { "ticket_id": "KAN-8", "author": "— with the calls you made and what they returned. A summary in your chat is invisible to the reviewer.", "body": "..." } - Hand off —
kanban::ticket::movetoin_reviewwhen someone else must check it, and todoneonly when the ticket's ownVerify:targets passed and you observed them. - On a wake (a review comment, a rejection): re-read the ticket with
kanban::ticket::getfirst — a late or re-armed fire can deliver a comment for work that already closed. Answer on the ticket, then re-arm the pair if the work is still open.
The pair: never arm a wake without its done-watch
Two bindings per ticket, registered together. The first is how you hear back; the second is what removes the first when the ticket lands.
engine::register_trigger {
"trigger_type": "kanban:comment",
"config": { "ticket_id": "KAN-8", "exclude_author": "backend-engineer", "ticket": "summary" },
"once": false,
"label": "KAN-8-wake",
"lifecycle": { "max_fires": 20, "expires_in_ms": 3600000 }
}engine::register_trigger {
"trigger_type": "kanban:change",
"config": { "ticket_id": "KAN-8", "events": ["ticket.moved"], "ticket": "summary" },
"once": false,
"label": "KAN-8-teardown",
"lifecycle": { "max_fires": 50, "expires_in_ms": 86400000 }
}exclude_authormust equal yourauthorstring character for character, or your own comment re-enters your handler."ticket": "summary"keeps the wake small: you re-read the ticket withkanban::ticket::geton every wake anyway, so the full thread in the payload is paid twice.- Label every binding
. The reap below is mechanical only if you can recognise your own bindings by label.- - Bound every binding's life. A session that dies between arming and
doneleaves nothing to reap but the deadline.
The teardown handler
kanban:change fires on your own moves too, so the first act is always a status read:
kanban::ticket::get { "id": "KAN-8" }If status is not done, stop — you were woken by a move made for another reason. If it is
done, reap:
harness::triggers::list {}Unregister every binding whose label starts with KAN-8- — the wake, the cron backstop, any
re-armed duplicate — then the teardown binding itself last, because it has to stay alive
while you reap:
harness::triggers::unregister { "subscription_id": "sub_..." }Finish by listing again and confirming none of your KAN-8-* labels remain. That read-back is
the proof; the unregister response alone is not.
Why you reap your own. engine::unregister_trigger removes "a trigger subscription owned
by the current harness session"; a foreign session_id is not honoured (a listing call carrying
someone else's session id returns your own bindings), and agent wakes do not appear in
engine::registered-triggers::list at all. No dispatcher, reviewer, or reaper can clean up
after you. Yours are yours to retire, and the done event is the moment.
The backstop: a wake is not a guarantee
The kanban worker delivers only to bindings in its own in-process subscriber map, and that map dies with the worker process. A binding it no longer holds never fires and reports nothing at all — silence indistinguishable from "no news". So never let the wake be your only way back.
engine::register_trigger {
"trigger_type": "cron",
"config": { "expression": "0 * * * *" },
"once": false,
"label": "KAN-8-check-in"
}On each fire, read what you missed and move the watermark forward:
kanban::comment::list {
"ticket_id": "KAN-8",
"exclude_author": "<your id>",
"since": "<the last comment timestamp you saw>"
}Traps
- The reviewer rejects and nothing happens. Your turn had already ended and no binding was armed on the ticket. Cause: you reported before arming the pair. Fix: arm first, report second.
- Your own comment wakes you, forever. The
exclude_authorstring is not byte-identical to yourauthor. Fix: copy the profile id out of your frontmatter; do not retype it from memory. - A binding fires for a ticket that is already closed. A late, re-armed, or duplicated fire. Fix: re-read the ticket and no-op unless it is still open and assigned to you.
- A ticket is
doneand bindings are still armed. Cause: no done-watch was armed, or the reap stopped after the first unregister. Fix: arm the pair together, unregister everylabel with the teardown binding last, and read the list back.-* - You cannot tell your bindings apart while reaping. Cause: labels like
wakeorcheck-inwith no ticket key. Fix:, always.- - The ticket sits in
in_progressand nobody answers. Cause: your report was a chat message, orauthorwas left at itsuserdefault so the reviewer's filters did not see it. Fix: comment on the ticket, and passauthorevery time. - You reached for a function that does not exist. There is no
harness::send; the wire is the ticket. Fix:engine::functions::list/directory::search_functionsbefore naming an id.
Checklist
-
kanban::config::inforead; column ids taken from the board, not assumed. - Ticket read whole — description, comments, activity — before any write.
- Claimed with
assignee,status, andactorall set to your profile id. -
kanban:commentwake armed withexclude_author= yourauthor, labelled, bounded by-wake lifecycle. -
kanban:changedone-watch armed on the same ticket, labelled, before the first report.-teardown - A
croncheck-in readingkanban::comment::listwith the same filters, so the loop cannot fall silent. - Report posted as a ticket comment naming what you ran and what it returned.
- Ticket moved to the lane the evidence earned.
- On
done: everybinding unregistered, teardown binding last, then-* harness::triggers::listre-read to confirm none remain.