Auto-Kanban — capture-first board for agent workflows
A single durable board for bursty, multi-thread work between a human and an AI agent. It exists because chat is a terrible database: the agent's context compresses and compacts as a session runs, and anything that lives only in the transcript eventually decays into a summary of itself.
Why This Is a Product Skill
Losing a thread isn't an engineering annoyance — it's how product intent leaks out of a build. The constraint a stakeholder stated in passing, the alternative that was considered and rejected, the half-formed idea that would have become the next feature: these are product decisions, and chat memory silently drops them. A capture-first board makes the backlog a property of the system, not of anyone's memory.
Why this exists
Chat is linear and lossy. The agent compresses as it goes; context compaction preserves decisions but strips texture — exact phrasings, half-formed asides, and above all the origin pointer (which message an item came from). When a plan is later written from working memory, anything that scrolled out of context is reconstructed from the compressed summary, not the source. The result is conceptual entropy: constraints stated in passing vanish, rejected alternatives resurface as fresh proposals, verbally-proposed config values never get named settings, and the agent's own mid-turn discoveries (a bug found inside a tool call) are never promoted because they didn't come from the user.
Two failure modes this system kills:
- Capture failure — a user request or flag is acknowledged in chat but never acted on, and falls out of context hours later.
- Reconciliation failure — stale context is carried forward (the agent nags the user about something they already did, or re-plans work that already shipped).
When to load this skill — invocation points
This skill is meant to be loaded at session start automatically and re-entered at specific
events, not left to memory. Two mechanisms guarantee the session-start load: a SessionStart
hook that injects a load reminder + disposition/stale check, and a project-instructions line
that names it. Load it at every point below:
| Invocation point | Why |
|---|---|
| Session start (auto — hook + project instructions) | Read the board, apply queued dispositions, run the renderer's --check, reconcile before reporting status. |
| Every user push / new request | Capture the card(s) the same turn, before acting. |
| Any mid-turn discovery (a bug/knob/constraint inside a tool result) | Card it before continuing — mid-turn finds are the most fragile. |
| Before writing a planning doc | Harvest the relevant cards into the plan (the anti-entropy handoff). |
| Before dispatching a build | Board maintenance — move the target card to In-progress, reconcile siblings. |
| Deploy / commit / release write-back | Reconcile In-progress → Review → Shipped (release-coupled gate). |
| Turn end (state changed) | Move cards, re-render, redeploy the hosted view. |
| Grooming / lull / overnight | Sweep, dedupe, link, AND score (rank: inputs) — ranking is part of grooming. |
| Picking "what's next" | The ranked board is the source of the next task — drive from it, don't ask. |
If the skill is already loaded this session, re-entry is cheap — follow it; do not re-ask whether to load it.
The one rule: capture-first
Every user push AND every pre-planning idea the agent generates lands in the data file as a card in the same turn, before acting — Inbox first, unprocessed. Nothing is allowed to live only in the transcript. This includes the agent's own mid-turn finds (bugs, config knobs, risks surfaced inside a tool result) — those are the most fragile, because nothing else will ever re-raise them.
Capture-at-handoff — the sibling failure to capture-at-turn. Session-close handoff notes and decision-journal / vibememo entries routinely record forward decisions ("the real fix is a RAM upgrade", "next we should migrate X") that never become cards, because the handoff-writing path does not route through card creation. The result is a decision that is documented but invisible to every status-first read of the board — the exact loss capture-first exists to prevent, just displaced to the end of the session. Rule: writing a handoff or a decision-trail entry is a capture trigger. Every forward-looking decision in it must be minted as a card in the same commit. If it is worth writing down for the next session, it is worth a card the next session can actually see.
The second rule: adjacency sweep before a new card is accepted
No new card is accepted as its own item before sweeping ALL existing work for adjacency or duplication. This does not weaken capture-first — capture into Inbox immediately, then run the sweep before the card is promoted out of Inbox. Board hygiene fails in the mirror-image direction from capture failure: a board that never merges becomes a list nobody reads, and duplicate cards split the evidence for one problem across two places so neither is decisive.
The board is NOT the whole prior art. Sweep all four surfaces, in order — skipping the last two is the observed failure mode:
- The board data file — every card's title +
why, not just the thread you assume it belongs to. Adjacency routinely crosses threads. - The project's backlog file(s) — newer diagnoses often land there, not on the board.
- Parked / deferred deliverables inside planning docs ("plan completion tracking" sections). A parked deliverable with an evidence trigger is a card living in a planning doc. This is the surface that gets missed, because it looks like shipped work.
- Catalogs / audits / probe results — the extend-vs-new adjudicators.
Test the relationship in order:
- Duplicate — same problem, same fix surface → merge into the existing card; do not create.
- Adjacent — different problem, but same code surface, same decision, or same discriminator → fold in as a sub-finding, or create with an explicit link (see "Card linking"). Adjacency is the case most often mis-filed as new.
- New — no shared surface or decision → create it, and record what it was swept against.
Is it a new card or an INSTANCE of an existing one? A newly-measured occurrence of a defect that
is already carded is an instance: append it to the existing card as a dated INSTANCE_<date> block.
"It was found in a different cycle" is not a reason for a new card.
Record the ruling on the survivor (DEDUPE_<date>: what was folded in and why) — a silent merge
is indistinguishable from a dropped card three weeks later. Merging is lossy if careless: the
survivor inherits the union of Origin / Constraints / evidence, never the intersection. A
parked item whose trigger has since fired is not done — it is READY; promote it, don't re-plan it.
And check parked-plan triggers at cycle close, not only at card creation: a shipped cycle often
fires a parked plan's revival trigger and nobody notices until the human asks.
The third rule: finish the job — don't leave siblings carded
Decomposing one body of work into pieces (deliverables D1..Dn, tiers, a cluster of cards in one arc) is a commitment to finish the whole set, not a menu to pick one or two from. Shipping part of an arc and leaving the sibling pieces as open cards is the failure mode — the human looks at the feature and doesn't see the work, because the visible half wasn't built.
- Capture-first still holds — the failure is carding-instead-of-finishing, not carding.
- Drive the whole decomposed set to done.
- The one legitimate stop is a real divergence — the remaining pieces are genuinely a different objective / code area / risk owner. Then ask to stop and re-sequence; never leave them silently. A different tier of the SAME problem is not a divergence.
Finish-the-arc mechanics — five rules
Field finding from an arc review: work is rarely lost outright — it piles up (in one month, 69 cards opened vs 31 closed; 40 parked on the human; a card in-progress for two months; built work in review for a month). Goal: finish the key parts of an arc and don't spawn unfinished tasks.
- Arc-close at release (make it HARD). The release script's card gate also checks the shipped
cards' arc siblings (same
batch, same or pointinglinks.parent, orsibling_of). An open sibling blocks the release unless it ships in the same release, is parked with a trigger, or is cut with an explicit reason that gets written onto the card. Setbatch:/links.parentwhen you decompose an arc, or the gate can't see it. - Recommend, don't menu. A
waiting_usercard'snextstates the default the agent will take and why. For low-tier, reversible, non-outward-facing cards, if the human hasn't vetoed by the next session, take the default and record it. Everything else still waits for an explicit answer. - WIP cap on built work. A
reviewcard older than ~7 days gets the first slot at the next attended release, ahead of new builds. An unattended pass doesn't start a new build while ≥3 cards sit inreview; it drains them instead. - Residuals fold into the parent. A leftover from an investigation or a ship is a dated block on its parent card unless it has its own problem statement, fix and acceptance criteria.
- Capture covers everything that changes the system, not only the main repo's code: config changes made through tools/UIs, sibling-repo work, and cross-session hand-offs. These are the changes that most often never get a card, because they never touch the code path.
Architecture — structured data as the source of truth
The board is data, not prose:
- Source of truth:
kanban.data.yaml— structured cards, committed to the repo. Edit HERE. Must stay valid YAML before every commit. - Generated views: a render script (
render_board.pyor equivalent) reads the YAML and emitsKANBAN.md(human-readable markdown) andboard.html(self-contained static page). Views are a pure function of the data — same input, byte-identical output. Never hand-edit a view; edit the YAML and re-render. Regenerate on every data commit, so a committed YAML change can never leave a view describing the previous board. If the board is hosted somewhere (a static file behind a tiny web server), a cron pull-and-render job on the host is the robust default over git hooks: hooks fire per-clone, and once multiple clones or worktrees edit the repo, some editor will always be missing the hook. A dumb loop that pulls and re-renders every few minutes converges no matter who committed from where. - Staleness banner (the freshness contract): the data file carries
meta.last_reconciled. The renderer compares that date against the newest signal of shipped work and, if any is newer, renders a loud STALE banner into every view (markdown, HTML, hosted page), warns on build, and exits non-zero in--checkmode. Reconciling the board (moving the shipped cards, then bumpingmeta.last_reconciled) is what clears it. A board that must be interrogated for staleness is not a mechanism; a banner that appears by itself is.- Wire the trigger to what THIS repo actually moves — not to a signal it never emits.
The original design keyed only on the newest git tag + newest release-notes file. That is
silent in any repo that ships without tagging or writing release notes (observed in
production: 26 work commits, board 2 weeks stale, banner never fired because the repo has
zero tags). The load-bearing trigger is the newest WORK commit — the newest commit
touching any file other than the board's own files (exclude
kanban/*, the rendered views, and the render script itself, so a pure reconcile commit does not re-trip it). Keep the tag/release-note checks as additive, but the commit check is the one that fires. A banner wired to the wrong signal fails exactly as open as no banner at all.
- Wire the trigger to what THIS repo actually moves — not to a signal it never emits.
The original design keyed only on the newest git tag + newest release-notes file. That is
silent in any repo that ships without tagging or writing release notes (observed in
production: 26 work commits, board 2 weeks stale, banner never fired because the repo has
zero tags). The load-bearing trigger is the newest WORK commit — the newest commit
touching any file other than the board's own files (exclude
- History: done cards age out of the active data file into
kanban.history.yamlrather than bloating the board. Git history is the durable record.
Board render design — autonomy visibility + operator agency
The render exists to make autonomous work visible and steerable, not just to list cards. Three elements, all pure functions of the data (so a view can never drift from the board):
- Lane order elevates the human decision queue. Render the "waiting on the human" lane(s)
immediately after
review(ahead of shipped/organic), so cards needing a call are near the top — and groom that lane FIRST each session (a card whose input already arrived is a grooming miss). - "Waiting on human" cards carry a free-form instruction box (not just fixed buttons). It posts
{card_id, action:"instruct", text, at}to the same disposition endpoint the buttons use; the agent applies it at session start (writes the instruction onto the card, front of the grooming queue, treats it as authority). This is the human's free-text steering channel — no drop back to chat. - Top-of-board autonomy counter + recent-work feed (a
meta.autonomy_feedlist):- Counter (large type, above all lanes):
N / Dwhere N = feed entries concluded in the last 24h (ack-agnostic — a conclusion is a historical fact) and D = open-card count (status != done). D shrinks as work completes, and the ratio is recomputed every render, so it re-bases after each push automatically. - Feed: an expandable list of entries that are NOT acknowledged AND younger than 7 days, newest
first. Each has an Acknowledge button (posts
{card_id:<feed-id>, action:"ack"}); ack retires the entry from the live feed, and any entry older than 7 days auto-archives (hidden) regardless — so the feed never clutters. - Append discipline (load-bearing): every time a card is concluded autonomously (built / parked-at-gate / moved to waiting / shipped / done), append one feed entry the same turn. A conclusion with no feed entry is invisible to the human's 24h view — the same capture-failure the board exists to kill, applied to the progress counter. Prune acknowledged / >7-day entries during grooming so the data file stays lean.
- Counter (large type, above all lanes):
Board-stack BLUEPRINT — the contract every board implements itself
When a project runs MULTIPLE hosted boards, do NOT hand-patch each host's server per change — that coupling is what this blueprint removes. The skill IS the contract: any agent maintaining any board reads it and brings ITS OWN render + server into conformance. Maintain one reference implementation.
- Disposition API. A tiny static-file server + micro-API:
POST /api/disposition{card_id, action, at[, text]}appends one JSONL line —action∈done|deferred|declined|approve|investigate|instruct|ack|move:<status>;textis OPTIONAL free-form (operator decision/instruction), capped, and MUST be preserved, not stripped (forack, card_id is the feed entry id; forinstruct, the card id).GET /api/dispositionsreturns the raw queue;POST /api/dispositions/cleartruncates after a successful import. The puller carriestextthrough to the pending file and includes it in the dedup key. - Serving model → why a shim exists. Boards are usually served as a CACHED STATIC file,
re-rendered on a cron — so a click's state must survive a reload before the next render via TWO
layers: (a) the render reads the pending queue and paints the acked/decided state server-side
(durable); (b) a small
localStorageecho + CSS class toggle re-applies it instantly on reload. That is the only client state — no framework. Per-request rendering makes (b) optional. - Render contract. Counter
N/D(shrinking denominator); feed entries show an Acknowledge button, and "waiting on human" entries ALSO show an inline decision box (postsinstructon the card); acked entries render IN PLACE as "acked — pending apply" (never optimistically removed); queued decisions show a "decision queued: …" chip; the human-decision lanes render elevated. - Groom-time reconciliation.
ack→ mark the feed entry acknowledged AND, if its card is in a terminal-ready lane and the work is complete, MOVE the card to done (an ack is acceptance, not a dismissal).instruct→ write the decision onto the card and move it to the lane the decision implies. Then prune acked/decided entries and re-render. Never leave a half-applied disposition.
Self-conform rule: change this blueprint first, then each board's agent brings its render + server into conformance from the spec — the skill is the single source; per-host files are conformant implementations.
The ship gate — board currency as an output of releasing
Board reconciliation is typically the only step in a release ritual with no forcing function: the deploy script refuses to run without tests and without release notes, but nothing refuses without a board update — so it runs on willpower, and rots. Fix it by making the board update an output of shipping, never a task beside it:
This is rung 1 of the board-currency ladder — the one hard forcing function; the staleness banner and session-start checks below it are soft backups, never substitutes:
- The release script takes
--cards ID[,ID...]naming the board cards being shipped and refuses to deploy without it, printing the current in-progress/review cards as candidates. - Card IDs are validated before anything is pushed.
- After a successful push, the script writes
status: shipped+ the released version onto each card, updatesmeta.last_reconciled, and (if you keep a decision journal) emits the "why we shipped this" entry — so the status board and the decision trail cannot lag the release. - An explicit
--no-cardsescape exists for pure-docs releases; it is logged, never silent. - Safety rule: a failed board write must never abort a deploy that has already pushed. Write after the push succeeds; warn loudly on failure; never exit non-zero post-push. A stale board is strictly better than a half-released version.
Rung 1 only bites if shipping actually runs through it. In a repo that deploys by many
paths that were never built to call the release script — pct push, vercel --prod, ad-hoc
host scripts, a session-close handoff commit — the ship gate is simply bypassed and the board
rots anyway. Do not assume one release ritual. Instead hook the reconcile onto the real
deploy paths: add a post-deploy step (a git post-commit/post-push hook, or a line at the
end of each deploy script) that runs render_board.py --check and surfaces its non-zero exit.
That converts every real shipping path into a reconcile prompt, so the forcing function follows
the work instead of waiting at a door nobody uses.
Overnight / unattended work needs a scheduler, not an intention
"Build it tonight while I'm sleeping" has no forcing function: the session ends and nothing wakes anything up. Work tagged for unattended execution must be bound to a real recurring scheduled job whose first action is a board reconcile. An overnight commitment with no scheduler is a promise — and "documented but never wired" is itself a finding (observed: an investigations lane didn't drain for days because the nightly pass was specified in the skill but never scheduled).
Pick the vehicle by what the work must reach. If the measurements need private-network data (a LAN database, SSH to a home server, local MCP servers), a cloud-hosted scheduled agent cannot run them — it has none of that reach. Run it as a local scheduled job (launchd / cron / systemd timer) on an always-on machine that already has the access, invoking the agent CLI headless. A cloud routine is fine only for code-side work: reconcile, static/prior-art investigation, low-tier builds.
The pass contract, in order, every run:
- Reconcile first. Load this skill, apply queued dispositions (including
instruct/ack), run--check, move disposed cards to their lanes, prune acked / >7-day feed entries. - Verify-before-work, then drive the ranked eligible queue through the full gate. Groom the
waiting_userlane first, theninvestigating: run each investigation's one-shot read-only measurement, record it on the card, then escalate or build. - Bounded autonomy — MORE conservative than an attended session:
- Investigations + read-only probes: fully autonomous.
- Low-tier builds that pass the gate: build + review + validate to
reviewonly — never deploy unattended. A deploy at 3 a.m. into a live system nobody is watching is hostile timing. Leave shippable work inreviewwith tests and release notes done. - Delicate / ambiguous / destructive / outward-facing: don't act — move to
waiting_userwith a user-verbnext, and send a real notification.
- Append a feed entry for everything concluded (measured / built-to-review / parked / escalated), timestamped from the real clock, so the morning counter reflects the run. The board + feed IS the report. Respect a token budget; stop when the eligible queue is dry; never spiral.
An unattended pass shares the checkout with any attended session. If a human-driven session starts while the pass is running, it should detect the pass (process list / lock file) and hold its own repo writes — or the pass should run in its own worktree — so two writers don't edit the board file at once.
Operator dispositions — a one-way human-to-agent queue
If the board is hosted as a web page, the human will read it when the agent isn't running — and
will want to act on it. Give every card disposition buttons (done / deferred /
declined) that write a disposition queue file (append-only JSONL is plenty:
{card_id, action, at} per line) rather than editing the YAML directly. The web page never
writes the source of truth; it queues intent.
At session start, before reporting status, the agent applies the queue to the data file with operator authority — apply, don't relitigate:
done→ mark shipped/done per the card's context.deferred→ move toparked, adding a revisit note.declined→ close the card, recording the decline the way a rejected alternative is recorded.approve(early-stage cards: inbox / pre-planning) → mark the card explicitly approved with board-button attribution and promote it out of raw intake. Out-of-band approval carries the SAME authority as an in-chat "go" — the agent must not re-ask.investigate(early-stage cards) → flag the cardneeds_investigation: true; it jumps to the front of the next lull-investigation sweep (below).instruct(waiting-on-human cards) → carries atextfield with the human's free-form instruction. Append it onto the card, treat it as authority (act on it, don't re-ask), and put the card at the FRONT of the grooming queue. It usually says how to resolve the card — follow it, moving the card to the right lane; if it asks for something that fails a gate, notify back + record why.ack(recent-work feed entries) → thecard_idis a feed entry's id; mark thatmeta.autonomy_feedentryacknowledged: trueso it retires from the live feed. Not a card-status change.
Then delete the queue file, re-render, and commit. Ask only when a disposition is ambiguous
against the card's state (e.g. done on a card that never shipped). Dispositions are strictly
one-way, human → agent: the queue is how the human drives the board asynchronously, never a
channel the agent writes to.
Force the apply, don't trust it. The queue-apply step will silently rot if it relies on the
agent remembering it (observed: a declined sat unapplied for a day because the pull-cron ran
but nothing forced the apply). Mechanism: the renderer's --check mode returns a distinct
non-zero exit code when the queue file is non-empty, enumerating the unapplied entries — so
the session-start check cannot pass while a button-tap is pending. Scope the failure to
--check only, so the hosted-page render still deploys (with pending chips visible) even when
the queue is waiting. A chip is a banner; the exit code is the mechanism.
Two field notes from running this mechanism in production: (1) isolate the renderer's tests
from the live queue — a --check exit-code test that doesn't pass an explicit (empty) queue
path inherits the repo's real pending file and fails whenever a genuine disposition is waiting;
point tests at a tmp queue path and add a dedicated exit-code test that seeds a fake pending
entry. (2) verify the whole button→queue→apply loop end-to-end after adding an action — the
hosted page's API may validate actions against a whitelist regex; a new button that renders fine
can still be silently 400'd server-side. Probe-POST each new action and clean the probe entries
from the queue before the next session-start apply.
Inbox hygiene — bounded lull investigation
An uninvestigated intake card is phantom load: it sits on the backlog looking like work while nobody knows if it's a duplicate, already-shipped, config-only, or real. Worked case: one context-wide audit killed a proposed guard card entirely — the existing codebase already had the guard, the proposed discriminator would have caught nothing, and the real fix was a three-entity config swap. Without the audit, a full planning + build cycle would have been spent on the wrong mechanism.
The rule: during lulls (no human turn pending, no build to verify) and on scheduled
autonomous passes, sweep intake cards (inbox + pre-planning) through a context-wide
investigation — the whole system, not just the subsystem the card names — before they age.
Cards flagged needs_investigation (the 🔍 button) jump the queue.
Token bound (hard): ONE search-grade agent per card, single dispatch, no fan-out — set an explicit per-card budget and a per-lull cap (e.g. ≤100k agent tokens per card, at most two cards per lull window). If a card can't resolve inside the budget, that fact IS the finding: record "needs a real planning cycle" and stop digging. Never spawn a review chain or multi-agent workflow for intake triage.
Verdict vocabulary (recorded on the card, same turn): DUPLICATE/ADJACENT (merge) ·
ALREADY-SHIPPED (close with evidence) · CONFIG-ONLY (stage the fix, close the card) ·
DEAD (threat model gone; close with the evidence chain) · REAL (promote with the
investigation as context) · UNRESOLVED-IN-BUDGET (needs a planning cycle; say what couldn't
be determined). An investigated card never returns to plain intake — it moves or it closes.
Watch results write back to cards
When a shipped item's live/organic validation lands — a monitoring pass, a metric read, a user report confirming or violating the ship hypothesis — the result is written back onto the card the same session. This is codified, not best-effort:
- confirmed → append an evidence line (date + one-line observation) to the card; when all of a card's open hypotheses are confirmed, move it to done.
- violated → the card returns to
reviewwith the violation recorded on it — the regression belongs to the shipping card, not to a fresh card that severs the history. - pending → no edit required, but if the proof needs a human action, the card's
nextmust say so explicitly.
A validation result that lives only in a monitoring report or a decision journal is a capture failure: the board is where "is it actually done?" gets answered.
Card schema — the fields ARE the decay vectors
Fill Origin, Why, and Next even when terse. Each field maps to a specific context-loss failure mode it kills:
| Field | Kills this leak |
|---|---|
| Status / Thread | which workstream — prevents cross-thread confusion |
| Created / Updated (date + time) | staleness and churn — a card untouched for weeks, or thrashing daily, is visible at a glance |
| Refinement status (initial vs refined) | whether the plan is as-first-written or has been reworked — a reader knows if it's a raw idea or a hardened one |
| Problem / Solution (plain language — mandatory) | the core — what is wrong and what fixes it, in words anyone can read. See "Problem/Solution framing" below |
| Origin (date + one-line gist of the originating push) | the origin pointer — full intent stays reconstructable after the message scrolls out of context |
| Why | rationale — a settled decision doesn't get re-litigated in a later session |
| Constraints | musts stated in passing ("must never auto-send", "keep the URL stable") that otherwise evaporate |
| Parked-alts (+ why rejected) | rejected options resurfacing as fresh proposals |
| Knobs | verbally-proposed configurables actually becoming named settings instead of magic numbers |
| Next | the single next action — a card is never inert; a title with no Next is a transcript line, not a card |
| Refs | planning doc / review record / commit — the card links to the durable artifacts |
Card template (as it appears in the YAML):
yaml- id: CARD-1 title: One-line plain-language problem -> solution statement thread: area # workstream status: inbox # CLOSED SET: inbox / investigating / pre_planning / planned / # in_progress / review / shipped / waiting_user / waiting_agent / # parked (+ done in history). Never coin a synonym. created: '2026-01-01 14:30' # date + time the card was born. Set once, never changed. updated: '2026-01-02 09:15' # date + time of the last material edit. Bump on EVERY edit. # created == updated on a new card. refinement_status: initial # 'initial' (as first written) or 'refined' (reworked since). problem_solution: # MANDATORY, plain language (see "Problem/Solution framing" below). - 'Problem: <what is wrong, in plain words a non-expert reads once and gets>. Solution: <what we will do about it, same plain words>.' approval: unreviewed # unreviewed / implied / explicit / blocked origin: date: '2026-01-01' gist: one-line gist of the originating push why: rationale, so the decision is not re-litigated constraints: [] parked_alts: [] # rejected options (+ why) refinement: [] # append-only challenge -> sharpened-form beats; length = how much reworked knobs: [] # named configurables this card introduces next: single next action refs: [] # docs / commits / reviews rank: # all optional — the board ranks on defaults until these are set value: 5 # 1-10, user / $ / safety / correctness impact (default 5) time_criticality: 3 # 1-10, how fast the cost of delay grows (default 3) effort: 5 # fibonacci job-size; omit to derive from a tier tag foundational: 0 # 0-10, platform-piece leverage that link fan-out misses (default 0) links: # cards are a graph, not a list — every link carries a one-line context blocks: [] # cards that cannot proceed until this lands blocked_by: [] # what this waits on — mirrors the other card's `blocks` related: [] # same family, no dependency parent: # the card whose fix subsumes this one sibling_of: # two faces of one problem superseded_by: # this card's premise was replaced (link, don't delete) context: 'ONE SENTENCE PER LINK saying WHY it is linked — a bare id is noise.'
Problem/Solution framing — plain language, mandatory
Every card carries a problem_solution written so a person who was not in the room — the
user weeks later, a teammate, a future agent with no context — reads it once and understands both
what is wrong and what you intend to do. Two rules:
- Problem then Solution, explicitly. State the problem as its own sentence (what is broken or missing, and why it matters), then the solution as its own sentence (what you will do). Not one blurred clause.
- Plain, non-jargon language. Write it for a smart reader who does NOT carry the codebase in
their head. Acronyms, file:line refs, internal symbol names, and shorthand are a memory
burden — they force the reader to reconstruct context the card should have supplied. Put the
dense detail where it belongs (a
refs:pointer, a forensic key, the reasoning log) and keep the problem/solution readable on its own.
This does not mean dumbing down the engineering — it means the entry cannot be legible only to the person who wrote it that night. If a sentence needs a symbol name to be true, name it once and say in words what it does.
Too much jargon (memory burden): "P8 exterior count STRANDED + naive:
count_deduped()live w/ ZERO consumers;_calc_sumsums one bit/sensor (no dedup) -> retire per-sensor sum for the deduped path."Plain (anyone reads it once): "Problem: two systems count the same thing and disagree — one counts a single item once per sensor that sees it (so one thing can read as three), and the accurate one isn't shown anywhere. Solution: show the accurate count; keep the rough one as a never-misses backup. (Details in
refs.)"
The plain version is longer in words but far cheaper to read — no decoding. Optimize for the
reader's effort, not the writer's. Retro-fix as you touch: don't mass-rewrite the board; when
you next edit any card whose problem/solution is jargon-dense, rewrite that one plainly in the
same edit (and bump updated). The board converges through the update hooks, not a big-bang pass.
Timestamps and refinement status — hooks, not memory
createdis set once and never changed.updatedis bumped to the current date+time on every material edit — it rides the same update hooks that govern the rest of the board.refinement_statusstarts atinitial; flip it torefinedthe first time the card's plan or scope is reworked (not a mere status-column move). Therefinementtrail's length then shows how much. Together they answer, at a glance: raw first idea, or been through the wringer?- The renderer surfaces
created/updated/refinement_statuson each card in both the Markdown board and the hosted page, so staleness and maturity are visible without opening the YAML. - Every timestamp comes from the real clock — read it, never estimate it. Run
date(or the equivalent) when stampingcreated/updated/ feedat/ any "measured at HH:MM" note. An agent has no reliable sense of wall-clock time, especially a long-running unattended one, and will happily write times that haven't happened yet (observed: an overnight pass stamped feed entries 02:20–03:05 while the clock read 02:10, and a "measured ~07:46" note five hours early). Invented times corrupt the 24-hour progress counter, the staleness ordering, and the anti-starvation tiebreak — and they read as evidence. Use one timezone consistently and say which.
Cards stay true — understanding changes, the card changes the SAME turn
A card is not a task ticket; it is the durable record of what we currently believe. A superseded belief left on a card is a trap set for the next session. Edit the card in the same turn whenever:
- A root cause is refuted or replaced. Don't just add the new story underneath — mark the old one WRONG and say what killed it. A card carrying two mechanisms and no verdict is worse than one carrying a wrong mechanism, because the reader can't tell which survived.
- A claim the agent made turns out false — including one made to the human in chat. Retract it ON THE CARD ("I claimed X; that is wrong; here is what refuted it"). Chat scrolls away.
- Scope changes (something folded in, dropped, handed to another card) — record the decision and its reason, so a reader sees a choice rather than an omission.
- An estimate becomes a measurement. Replace the guess with the number and how it was obtained.
Test: would a fresh session, reading only this card, act on something we now know is untrue? If yes, edit it now. "The fix is happening in another card" or "the fix will make it moot" are not reasons to defer — a fix in flight can be abandoned, and then only the card remains.
Status must track reality, not just the body. Whenever you write a material update to a card,
ask what lane it is now in and move it in the same edit (and bump updated). A card whose body is
actively changing while its status still says inbox, or whose implied next step clearly changed
(audit finished, build landed, blocker cleared) but nobody moved it, makes the lane view lie — and
the lane view is how a returning human reads the board. Common prompt transitions: capture gains a
measurement plan → investigating; investigation finds a mechanism → pre_planning/planned;
investigation refutes → done; a branch with real work exists → in_progress; needs a human call →
waiting_user; the agent owes a check → waiting_agent (never the human's lane).
Card linking — cards are a graph, not a list
A board of unlinked cards loses its most valuable information: which findings are the same finding, which blocks another, which parent will actually fix a symptom. Several of the highest-value findings on a mature board are only visible as relationships — two cards that are one bug carded twice, a cluster of consumers all sitting on one partly-dead producer.
Use the links: block from the card template. Rules:
contextis mandatory and is the point. A bare id says two cards touch, not what the relationship buys the reader.- Link at creation, not at cleanup — a link added later has already failed the reader who needed it.
blocks/blocked_byare mirrored on both cards; a one-sided dependency is invisible from the side that needs it most.- Gating is a link, not a note. When several cards wait on one input, put
blocked_byon each so ranking can't pick a card whose input isn't ready. - When you refute or supersede, LINK — don't delete. The refuted card is the record of why the current answer is the current answer.
- Contradictions get linked too — say so in
context, before a builder discovers it mid-cycle.
In prose, reference cards as [[CARD-ID]]; a reference to a card that doesn't exist yet marks work
worth carding, not an error.
Columns
Inbox (raw capture) → Investigating (we do not yet know what is true; measuring) →
Pre-planning (idea being decomposed) → Planned (has acceptance criteria) → In progress →
Review → Shipped — plus three side lanes:
Waiting on user— decisions or actions only the human can take.Waiting on agent— things the agent owes (a promised verification, a re-measurement). Track obligations on both sides; filing the agent's own debt under the user's lane hides it.Parked— deliberately deferred, always with a written revisit-trigger. Parked is not done: when a trigger fires, the card is READY, not forgotten.
The next of a waiting_user card must open with the USER's action verb, not the agent's.
The card is in this lane because the ball is in the human's court, so its next must name the
concrete thing they do — APPROVE (scoped, needs a go/no-go) · PICK (a choice between
named options A/B) · ANSWER / VERIFY (a fact only they know) · DO (a physical or
external act) · REVIEW (read a delivered doc). Write it as VERB: <the ask> -> <what I do on your answer>, so the human sees their decision first and the downstream work second. A next
that opens with the agent's verb ("Add the OFF path…", "Run the reviews…") is the drift this
kills: it reads as work parked on the human when it is really the agent's work waiting on one
input, and it hides what is actually being asked for. The most common real action is APPROVE or
PICK — if a card can't reduce to one of the five verbs, it may be mislaned work the agent can
drive under implied approval.
status: is a closed set, and it is the ONLY lane field
Status values are a closed set — inbox, investigating, pre_planning, planned,
in_progress, review, shipped, waiting_user, waiting_agent, parked, done. Inventing a
near-synonym (intake, todo, blocked) is a silent hygiene failure: the renderer cannot lane it,
so the card vanishes from every status-first read while still looking filed (observed: a session
coined intake for all seven cards it created that day; every one dropped out of the lane view). If a
card fits no lane, discuss the lane — don't coin a status.
One field owns the lane. Do not let a parallel field (column:, state:, lane:) carry the same
concept: the renderer buckets on status only, and anything else falls into a bucket nobody reads
(observed: a quarter of a board carried its lane in a legacy column: field — perfectly valid values,
completely invisible). Two failure shapes look different and read identically on the board: a card in
a lane that no longer matches its work (stale value — a judgement error), and a card whose lane the
renderer can't see at all (wrong field or invented value — silent, and worse).
Investigating — the step before pre-planning
Pre-planning means we know the problem and are shaping the solution. Investigation is the step
before that: we do not yet know what is true, and we are measuring to find out. Different activity,
different exit. Without this lane, a card carrying hours of measurement, refutations and corrections
sits in Inbox looking identical to a one-line shower thought.
Entry: the card's next action is a MEASUREMENT, not a build or a decomposition.
Exit is mandatory and binary — an investigation that can't say which is not finished:
- CONFIRMED / mechanism found → promote to
pre_planningorplanned, carrying the evidence. - REFUTED → close the card (
done) with the refutation recorded. A refuted investigation is a completed one; closing it is what stops the next session re-deriving the dead mechanism.
Cards must not rot here: an investigation with no measurable next step belongs in parked (with a
revisit trigger) or waiting_agent (if the measurement is a debt the agent owes).
When an investigation surfaces real work — retire-and-relink, keyed on the problem statement. Ask one question: does the surfaced build have the SAME problem statement as the card's title?
- Confirmed — the build IS the card's own premise → promote the same card in place. Don't spawn a new one; the history and evidence stay attached to the work they justify.
- Refuted-but-fertile — the premise was wrong, but a DIFFERENT real problem surfaced → close the
investigation
done(refutation recorded) and open a NEW card for the different work, linkedsuperseded_by/relatedwith a one-line context. If a card for that work already exists, point at it instead of minting a duplicate. - Refuted-and-barren → close
donewith the refutation. A complete result, not a failure.
The failure this kills: an investigation that quietly becomes a build under a title describing the disproven hypothesis, so a later reader can't tell what the card is for.
Cadence — bind updates to events, not to a clock
Clock-based cadences ("update the board every 30 minutes") rely on remembering a separate chore, so they drift under load. Bind every board update to a checkpoint that already has to happen:
- Session start: read the board; apply any queued operator dispositions (see above);
run the renderer's
--check; if stale, reconcile against repo/live state before reporting status. Groomwaiting_userfirst — re-verify whether each human input is still needed (the blocker may have cleared) and move cards whose input already arrived. - On every push / mid-turn idea: add or update a card the same turn, before acting.
- Pre-planning harvest: before writing any planning doc, harvest the relevant cards — Origin / Why / Constraints / Parked-alts / Knobs flow straight into the plan. This is the anti-entropy handoff from chat to plan.
- Before dispatching a build — board maintenance for freshness. BEFORE an agent starts a build / implementation task, move the target card to In-progress then, not at turn-end — a card must never sit in Planned / Pre-planning while its build is actually running; the board should reflect what is happening the moment it starts. While you're there, quick-reconcile the sibling cards the build touches (dependencies, parked-alts, cards it will close). This moves work into In-progress sooner and keeps the board honest during long builds instead of lurching only at turn boundaries.
- Turn end: reconcile — move cards between columns, write one-line dispositions, re-run the renderer so committed views match the data.
- Release: the ship gate (above) marks cards shipped automatically.
Anti-patterns
- A card with a title but no Origin/Next — that's a transcript line, not a card.
- Letting a mid-turn discovery stay in the tool output "because I'll remember it."
- Hand-editing a rendered view instead of editing the YAML and re-rendering — the data file is the source of truth; every view is a reflection, never the record.
- Creating a card without first sweeping existing cards, the backlog, and parked planning-doc deliverables for duplicates or adjacent work — a board that never merges becomes a list nobody reads, and duplicates split the evidence for one problem across two places.
- Silent moves: closing or moving a card without a one-line disposition. A card never just disappears.
- Treating a parked item as done. Parked means "revisit-trigger set" — check whether the trigger fired.
- Duplicating the decision journal's reasoning into the board, or the board's status into the journal. The board is WHAT / WHERE / NEXT; the journal is WHY. They compose.
Reference files
references/schema/kanban.data.example.yaml— annotated example data file: the meta block, the full card field vocabulary across lifecycle states, and a disposition-queue entry.references/dashboard/board.example.html— a self-contained dummy HTML board (light/dark, STALE banner, stubbed disposition buttons) to derive your own renderer's output from.
Product Lens
Before a card leaves Pre-planning, give it a parsimony verdict: is the problem sharp and real (one falsifiable sentence), and is the fancy version worth its marginal risk over the simplest version that captures most of the benefit? Record BUILD / SIMPLIFY / PARK / DROP on the card. Reaching "good idea, not worth it now — park with a trigger" is a success of the gate, not a failure.
Ranking & sequencing — computed, not eyeballed
Cards are not a flat list, and ranking is computed, not eyeballed — a deterministic score so the drive-loop picks the same "next" every time and the renderer can show it. The model is WSJF (Weighted Shortest Job First): cost-of-delay over job-size.
WSJF = (value + time_criticality + unblock) / effort # rounded to 1 decimal
| Factor | Range | Source |
|---|---|---|
| value | 1–10 | rank.value (default 5) — user / $ / safety / correctness impact |
| time_criticality | 1–10 | rank.time_criticality (default 3); a card tagged live-broken floors at 8 — how fast the cost of delay grows |
| unblock | 1–10 | computed (below) — never hand-stored, so it cannot drift |
| effort | fib | rank.effort override, else from a tier tag: T1=2, T2=5, T2-DB=8, T3=13, untiered=5 |
unblock is leverage, and leverage is more than links. A platform piece that everything
quietly stands on is a true unblock even when no card drew the edge, so:
unblock = min(10, max( 2 + 2×(#cards this blocks), foundational ))
- link component
2 + 2×fanout— from this card'slinks.blocksplus the reverse of others'blocked_by. foundational—rank.foundational(0–10) set on genuine platform pieces, or a floor of 6 when tagged a platform-enabler / shared-primitive, else 0. The larger wins. When you build a new primitive many cards will consume (a shared resolver, a core producer, the test harness), set itsfoundationalthe same turn — its leverage is otherwise invisible.
The renderer computes WSJF — it is a pure function of the card data. So it works on every
card via the defaults and sharpens as rank: inputs are set; you never renumber by hand.
Ordering — per-lane, at all times
Within EVERY lane, cards render first→last by WSJF descending (ranks are per-lane, so each
lane shows its own #1, #2, …). Ties break by: (1) batch-affinity with an in-progress card
(ship the batch together), (2) unblock desc, (3) oldest updated (anti-starvation — a
card never rots at the bottom forever). The one exception is the shipped/done lane, which renders
newest-first (WSJF is meaningless once shipped). Each card header shows
#<lane-rank> · WSJF X.X · (v… tc… u… / e…), plus a ⚠ default-scored marker when a card is
still running on tier + link defaults — so scoring effort lands where it matters.
Selection — display ranks per lane, the loop picks globally
Display is per-lane, but the drive-loop picks the globally highest-WSJF card that passes the
eligibility filter — deps (blocked_by) all done, approval not blocked, and (for the highest
tier) approval explicit. That card is always some lane's #1, so the per-lane #1s are the candidate
pool and the agent takes the best of them. Never start a card whose dependency is unmet, however
high its raw score.
Re-evaluate on every move — and score AS you groom
Two things keep ranks honest:
- Ranks recompute on every render. WSJF is a pure function of the data, so re-running the renderer after any change re-orders every lane automatically — a card moved between lanes lands at its correct per-lane rank with no manual renumbering. This is why the turn-end render hook is load-bearing: a lane move without a re-render leaves the shown order stale. Render after every status move.
- But the score INPUTS are not automatic — revisit them when the card materially changes. A
lane move often changes what the card is worth or costs: promotion sharpens
effort; a measurement that lands changesvalue; a dependency clearing raises other cards'unblock. When you move or materially edit a card, re-check itsrank:inputs in the same edit, exactly as you re-check itsstatusandlinks.
Ranking is a core part of grooming, not a separate chore. Every groom pass — the inbox sweep,
an overnight reconcile, a disposition — sets or refreshes the card's rank: inputs (at minimum
value and effort; foundational for platform pieces) alongside its verdict and links. A
groomed board is not just correctly-laned and de-duped — it is scored, so the drive-loop can
pick from it. An un-scored card after grooming is an unfinished groom; the ⚠ default-scored
marker is the tripwire for cards the sweep skipped.
Drive from the board — motion, not permission-seeking
Each card carries an approval state (unreviewed / implied / explicit / blocked) so
the agent can plough independent work without waiting, while never ploughing into things that
need a human call. The default posture is motion, not permission-seeking: a groomed, ranked
board is a work queue — work it. The change classification (tier) is the throttle.
The autonomy ladder (by tier):
- Lower-tier work (hotfix + standard feature cycles) → DRIVE autonomously, no pause for a go. Run the card's parsimony + cost/benefit gate FIRST (below). If the verdict is BUILD or SIMPLIFY, take it all the way — build → the tier's reviews → verify → ship — without stopping to ask. Approval is implied by the tier plus a passing gate; do not re-request it.
- Delicate / invariant-critical / cost-AND-safety work → PAUSE for approval before build and again before deploy. These are the changes where one missed path loses money or safety.
- Investigations (measure-first cards) → DRIVE autonomously too. Run the measurement (a cheap,
read-only probe over data that already exists). Then branch on the OUTCOME: if it does NOT open a
build lane (refuted, or the fix needs a human call / external input) → record the finding and drop
the card into
waiting_userwith a crisp user-verbnext(or close it if the mechanism was refuted). If it DOES open a build lane → plan and build ONLY if it passes the FULL gate below — not just the tier. Tier is the first gate; value, prior-art, parsimony and cost/benefit all still apply.
The gate that earns the autonomy — FOUR steps IN ORDER, run BEFORE building, every card. Do not skip a step because the card "looks obvious"; a later step's verdict is only trustworthy if the earlier steps passed. Record each step's finding on the card.
- Validity — GROUND TRUTH first (still needed AND not already done). Read the authoritative source (the code at the named location, the test suite, the live system, the history) and confirm the work is still real and not already shipped — the board is a claim, not proof, and can be stale in either direction (see "Verify-before-work" below). A card that is already-done / moot retires here before any parsimony debate. 1b. Config-first — can a SETTING fix it instead of code? Before any plan or build, check whether a configuration change solves it: a live runtime knob, a settings/options field, a per-entity config value, a third-party integration setting, or a documented operator action. Read the knobs' live values — don't recall them. If a setting fixes it, do or propose that and record on the card why code is unnecessary. (Field evidence: in one session three planned code changes turned out to be a knob turn, a knob turn, and a non-problem refuted by data.)
- Prior-art / reuse (REUSE-or-BUILD, cite the location). Before proposing to BUILD any new mechanism, scan for existing prior art and record REUSE (with the existing symbol's location) or BUILD (why nothing equivalent exists). The producer you are about to "build" often already exists.
- Parsimony — is the problem sharp and real (one falsifiable sentence)? Does the simplest version capture most of the benefit? Verdict BUILD / SIMPLIFY / PARK / DROP on the card.
- Cost/benefit — does the marginal benefit pay for the ingredient risk + review cost? If a low-tier change drags in a categorically risky ingredient (shared-primitive writes, cross-component state, a rare-fire code path), that is a signal to SIMPLIFY or to treat it as delicate — not to barrel ahead because "it's only a small change."
Verify-before-work — NO card is worked until it is proven not-already-done. This sits ABOVE the rest of the gate: read the card → verify against ground truth → THEN gate → then build. A card is a claim about the world, written at some past moment; the world changes without touching the board (a sibling cycle fixes the bug, a config change moots it, a device comes back, a later ship subsumes it). Treat every card's state assertion as unverified until checked this session, with an independent read of the authoritative source — not the card's own summary.
| Card kind | The check that settles it |
|---|---|
| Code defect / missing behavior | read the current source at the named location — is the fix already on the main branch? |
| Shipped-feature follow-up | the history / release notes / validation record for that surface |
| Live-system symptom | read the live state, DB row, or metrics — is it still happening? |
| Test / suite defect | RUN the suite (or file) now; never trust a recorded pass/fail count |
| Config-only item | read the live config — it may already be set |
| Doc / board item | open the file — the section may already exist |
Both directions are failures. A card that says "open" but is done wastes a rebuild. A card that says "done / clean / fixed" but is not is worse — it silently drops out of every future sweep (observed: a card recorded "collection now clean"; one collect-only run the next morning returned two errors). Verification means confirm the card is true, in whichever direction.
Verdicts (recorded on the card with the command/observation that produced them): ALREADY-DONE ·
PARTIALLY-DONE (narrow to the residual, re-score effort) · MOOT (premise gone) · STILL-REAL
(proceed, carrying the fresh evidence) · CARD-WAS-WRONG (mark the false claim WRONG with what
refuted it — often co-occurs with STILL-REAL).
Retire before moving on. A verdict that resolves a card is cashed the SAME turn: write it, set the
terminal status (done for ALREADY-DONE / MOOT / refuted), bump updated, re-render, and only then
pick the next card. A resolved-but-still-open card is worse than never checking — the next session
re-verifies settled work.
Board-wide, not just the card you're about to pick. Every open lane gets verified, and parked cards get their revival triggers checked the same way. Work highest-ranked first so the drive loop is never blocked on a complete sweep, and date each verdict so a later session can tell fresh evidence from stale assertions. Never trust an agent's or teammate's "still open / already fixed" summary — confirm the commit/code/state exists yourself. An approval does not bypass this: an approved card (board button or chat "go") still passes the validity check first; approval authorises the work, it does not exempt it from proving the work is still needed.
When the gate is AMBIGUOUS → escalate, don't guess. A clean BUILD/SIMPLIFY drives
autonomously; a clean PARK/DROP parks with its revival trigger. But when the parsimony verdict is
genuinely unclear, or the cost/benefit is a toss-up, do NOT silently pick and do NOT stall the
whole queue: notify the operator (a real notification that reaches them) AND park the card
as waiting_user with a crisp user-verb next (APPROVE / PICK A-or-B). Then move on to the
next eligible card — one ambiguous card must not block the flow. The notification and the parked
card are redundant on purpose: the notification prompts, the board holds the decision durably.
Still always pause — the gate does not override these: a review returns do-not-ship; the work grows beyond the card's scope (re-scope, don't silently widen); anything destructive / outward-facing / published; the operator flagged it delicate; hostile timing. These are few and specific, not a general licence to stop.
Keep going — the loop. When a card reaches done (or a clean park), do not stop and report for instructions — pick the next one. Rank per the section above, skip blocked / unmet-dependency / approval-pending cards, and drive the next eligible card through the same gate. Report at natural checkpoints — a ship, a batch cleared, an approval gate, a genuine question — not after every card. The board, not a chat prompt, is the source of "what's next."
Concurrency & builder isolation
Ranking answers what next; concurrency answers how many at once. A board worked strictly serially never empties, so treat parallelism as a first-class sequencing dimension. What safely parallelizes: disjoint surfaces (cards touching different files/subsystems), framing-disjoint reviews of the same diff, and read/scope work beside build work. What must NOT: anything in a dependency chain, two agents writing the same file, or work that depends on a diff still under review.
The hard requirement: every concurrent repo-writing dispatch runs in its own git worktree, and
the orchestrator commits its own work before dispatching. The collision that bites is not only
merge conflicts — a dispatched builder doing routine git hygiene (git reset --hard, a stash, a
checkout -- .) in the shared checkout silently destroys the orchestrator's uncommitted edits
and any other concurrent writer's work, with no recoverable trace for uncommitted tracked files.
Two non-negotiable rules follow:
- Commit (or stash) your own work BEFORE dispatching any repo-writing agent. A committed
change survives a
reset --hard; an uncommitted one does not. The orchestrator's tree must be clean at dispatch time. - Every repo-writing builder runs in its own worktree — even a single one, even a "quick" one. "Only one builder, so no collision" is the exact rationale that fails: the collision is builder-vs-orchestrator, not just builder-vs-builder.
The real bottleneck is orchestrator attention, not agent count: every concurrent agent returns a report that must be independently verified — never accept a builder's or reviewer's summary as fact. Fan out to the width you can actually verify, then stop.
Producer AND consumer — check both, always
If a rule set codifies only "who reads this value", that asymmetry becomes a blind spot: every reviewer looks downstream and nobody reads how the value is made. Observed failure: a household occupancy count doubled its own residents and flipped a whole-house mode, because a default-ON additive derivation silently overwrote a subtractive one while both of its dedup defenses had gone inert — their shared dependency (a recognition subsystem) had failed in a different, separately-investigated incident. Every audit asked who consumed the count. None asked how it was computed.
Both checks belong in any plan or investigation that touches a value:
- Producer: How many derivations exist, and which one wins? What does each depend on — and is that dependency healthy right now? Is this a plumbing fix or an arithmetic fix ("the right inputs feed it" ≠ "it computes the right number")? Compare against external ground truth, never against another internal number that shares its assumptions.
- Consumer + call site: Who reads it, on which path, trust-decision or display, and where is it wired in.
Corollary — acceptance criteria must discriminate. State what the observation looks like under the fix and under a plausible different failure. If they are identical, the criterion proves nothing: "the count now exceeds the identified total" was accepted as proof that a plumbing fix worked, when it was equally the signature of the double-count bug that had replaced it.


