kanban
v0.1.23File-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.
- macOS: arm64 · x64
- Linux: arm64 · armv7 · x64
- Windows: arm64 · x64
exact versions are immutable; binary and bundle artifacts are digest-pinned.
skill doc
feature-planning
how-toPlanning a feature into tickets
Open these first
- The
kanban-ticketsskill owns the write mechanics — actor, keys, columns, priorities, the comment loop. If it is not in your context:directory::skills::get { "id": "kanban/tickets/kanban-tickets" }. kanban::config::info {}— the live column ids, priorities and key prefix. Never assume a column id; read it.kanban::board::get {}— what is already planned, plus the board's projects and releases. A ticket you cannot see is a ticket you will duplicate.
The board is the authoritative record. The chat is a scratchpad that is thrown away when the session ends.
The first move
kanban::board::get {} before you say anything about the feature. Then, for every ticket named, kanban::ticket::get { "id": "KAN-7" } — its description and comments are the context you are about to edit, and rewriting them from memory is how a decision somebody already made gets silently dropped.
Interview before writing
Ask until you can answer all five in one sentence each. Those sentences become the ticket.
- Who is this for, and what do they do today instead?
- What changes for them when this ships — the observable difference?
- What is explicitly NOT in this slice?
- How will we know it works, in a way a person can check without reading the diff?
- What must exist before this can start — other tickets, an API, a decision?
If the user says "just write the tickets", answer 1–4 yourself, mark each answer Assumed: in the description, and say the assumption out loud. Never ask a question you can answer by reading the board or the code.
The ticket description template
Every description is this, headings verbatim, in this order:
## Problem
<one paragraph: who is hurt today, and how>
## Outcome
<what is true after this ships, from the user's side>
## Acceptance criteria
1. <subject> <action> → <observable expected result>. Verify: <URL, command, or screen>.
2. ...
## Out of scope
- <the thing a reasonable reader would assume is included, and is not>
## Notes
- Depends on: KAN-4
- Assumed: <anything you decided without confirmation>The rules that make criteria usable:
- One criterion per verifiable statement. If it contains "and", it is probably two.
- Name the actor and the observation. "An operator sees the row in In review" beats "status updates correctly".
- Every criterion carries a
Verify:— a URL, a command, a screen. A criterion nobody can check is a criterion that will pass on vibes. - Criteria describe behaviour, not implementation. File paths, table names and module choices belong in
Notesas constraints, never as criteria. - 3–7 criteria. More than that is two tickets.
Split by outcome, not by layer
A ticket is a slice that can ship and be checked on its own.
- One ticket per observable outcome. "Add the search API and build the filter UI" is two outcomes, therefore two tickets.
- If two tickets must land together before either is checkable, they are one ticket.
- Ordering is a
Depends on:line in Notes, not a tenth criterion. - Infrastructure with no observable outcome is a
Notesline inside the ticket that needs it — not a ticket.
Group outcomes under a parent; file them under a project and release
When a feature is more than one outcome, keep one feature ticket (the Problem, the Outcome and the criteria that only hold once everything landed) and file each outcome as a subtask by creating it with parent:
kanban::ticket::create {
"title": "Filters survive a reload",
"description": "<the template above>",
"parent": "KAN-7",
"project": "iii cloud",
"release": "Phase 2",
"actor": "product-manager"
}- One level. The parent must be live and have no parent; a ticket that already has subtasks cannot become a subtask. Move an existing ticket under a feature with
kanban::ticket::update { "id": "KAN-9", "parent": "KAN-7" }("parent": nulldetaches). - A subtask is a full ticket. It carries its own criteria,
Verify:lines andOut of scope; the feature ticket's criteria cover only what no single subtask can show. Ordering between subtasks is still aDepends on:line. - Progress counts only the done column. The parent's
progressis{ done, total }over its live subtasks indone_status; a subtask inin_reviewis not done. The parent does not move when a subtask does, and a feature ticket moved to done with open subtasks is flagged in the console. - Nothing is inherited. A subtask does not copy its parent's project or release; pass
projectandreleaseon each one. Both take an id or a name. - Set them only if the user named them. Read
kanban::project::list {}andkanban::release::list {}and use what they return. A project or release that does not exist yet is the user's to create in the console (creating, shipping and converting them are not agent actions); ask, or leave the field empty and say so in the ticket'sNotes. - A single-outcome ticket needs no parent. Do not wrap one ticket in a feature ticket to look organised.
The edit loop — the part that must not slip
The plan changes every few turns. Each change lands in the ticket in the same turn it is agreed:
kanban::ticket::update {
"id": "KAN-7",
"description": "<the FULL new body>",
"actor": "product-manager"
}description replaces the body — read the ticket, edit it, write the whole thing back. Never a fragment.
Then report the change in prose — "criterion 3 now reads X, and I moved Y to out of scope" — and stop for confirmation. A plan the user has not seen written down is not agreed.
Creating a new one:
kanban::ticket::create {
"title": "Search filters persist across sessions",
"description": "<the template above>",
"status": "backlog",
"priority": "medium",
"labels": ["feature"],
"actor": "product-manager"
}status takes a configured column id (backlog, todo, in_progress, in_review, done by default — read them from kanban::config::info); priority is one of low, medium, high, urgent.
Assigning is a separate decision. Leave assignee null unless the user names who does the work; then use the profile id kanban::agent::list {} returned — senior-software-engineer, never the display name "Senior Software Engineer".
Traps
- The plan lives in chat. Symptom: the user comes back next session and the ticket still says what it said yesterday. Cause: you batched the update "for later". Fix: write it in the turn it is agreed, mid-conversation if need be.
- You overwrote a description an engineer had started annotating. Cause:
updatereplaced a body that had grown comments. Fix:kanban::ticket::getfirst; if work is underway (a comment, a move toin_progress), post the change as a comment and get an answer before rewriting the body. - A ticket nobody can start. Symptom: the engineer replies with three questions. Cause: criteria written as intentions, with no
Verify:line and no dependency list. - Two tickets that are really one. Symptom: neither can be moved to
in_reviewon its own. - Duplicate plan. Symptom: two tickets with overlapping criteria. Cause: planning from memory instead of
kanban::board::get. - You invented scope. Symptom: the user reads the description and finds a feature they never asked for. Cause: filling a gap with a plausible product decision instead of asking. Fix: ask, or mark it
Assumed:.
Checklist
-
kanban::board::get {}read this session before any planning. - Every created or edited ticket read back with
kanban::ticket::getand matches what you told the user. - Every ticket has ≥3 acceptance criteria, each with an observable result and a
Verify:line. -
Out of scopeis non-empty on every ticket. - Every
Depends on:key exists on the board. - A multi-outcome feature has one feature ticket, and each outcome is a subtask created with
parent(one level only). -
project/releaseare set only to ids or nameskanban::project::list/kanban::release::listreturned, and passed on each subtask explicitly. -
actor: "product-manager"on every write;assigneeonly set to an idkanban::agent::listactually returned.