Protocol Specification
The core coordination protocol: state machine, lease model, dependency resolution, and event system. Agents don't need a framework — they read these rules from skill files and apply them via the CLI.
State machine
Tasks move through five states. Every transition is recorded in the history table for audit.
| Transition | Command | Effect |
|---|---|---|
| TODO → IN_PROGRESS | claim or claim-next | Agent acquires lease, task is locked for 15 minutes |
| IN_PROGRESS → DONE | complete (default) | Direct complete without review gate |
| IN_PROGRESS → IN_REVIEW | complete --review | Submit for review. Lease released. |
| IN_REVIEW → DONE | approve | Reviewer confirms work. Final state. |
| IN_REVIEW → TODO | reject | Reviewer sends back with reason. No lease assigned. |
| IN_PROGRESS → BLOCKED | block | Agent drops the lease, marks with reason. |
Self-review is disallowed. claimed_by is set when a task is first claimed. If the same agent tries to approve their own task, the protocol returns an error. Another agent must review.
Invalid transitions (enforced)
- IN_REVIEW → IN_PROGRESS — Must reject first (→ TODO), then claim again.
- DONE → anything — Terminal state. No transitions from DONE.
- BLOCKED → IN_PROGRESS or IN_REVIEW — Another agent must claim it from TODO (lazy reclaim on
claim-next). - IN_REVIEW → DONE without approve — Only
approvecan move IN_REVIEW → DONE.
Lease model
Every claimed task gets a lease with a default of 15 minutes. The lease is the crash recovery mechanism:
# Agent Alice claims TASK-1
$ kanban task claim-next --agent alice --role worker
→ TASK-1: assigned_agent=alice, lease_until=now+15m
# 10 minutes later — progress renews the lease
$ kanban task log-progress TASK-1 --agent alice --note "Working"
→ lease_until reset to now+15m from this call
# Alice crashes. 15 minutes pass. Lease expires.
# Bob calls claim-next — gets TASK-1 (expired lease reclaim)
$ kanban task claim-next --agent bob --role worker
→ TASK-1: assigned_agent=bob, lease_until=now+15m Lease rules
log-progressandextend-leaseboth renew the lease.extend-leaseaccepts--minutesto override the default.claim-nextconsiders both TODO tasks and IN_PROGRESS tasks with expired leases. It filters bylease_until < CURRENT_TIMESTAMP.blockclears the lease.completereleases it (task goes to IN_REVIEW with no owner).- A lease check runs inside the same Serializable transaction as the claim — no race between "check expiry" and "claim."
Transfer protocol
Tasks can be transferred from one agent to another:
kanban task claim TASK-1 --agent bob --transfer --to charlie Requirements:
- The task must be IN_PROGRESS.
- Calling agent (
--agent) must be the currentassigned_agent. --tospecifies the new owner with a fresh lease.- Cannot transfer to yourself.
Dependency model
Tasks can declare dependencies using --depends-on with comma-separated task IDs:
kanban task dispatch --title "Write tests" --role worker --depends-on TASK-1,TASK-2 Dependency resolution rules:
claim-nextskips tasks whose dependencies are not all DONE (unless--respect-deps=false).claim(by ID) returns an error if dependencies are unmet.- Cyclic dependencies are detected by
plan lintat plan time, not at runtime. - Dependencies are stored as a
depends_onTEXT column with comma-separated IDs. No join table — simple, explicit.
Event system
Every state transition writes an event to the events table. Events have a TTL (default 3 days, set via ttl_seconds, NULL = never). The event log is append-only.
| Event type | When | Payload includes |
|---|---|---|
task.created | Task dispatched | id, title, role, priority, project |
task.claimed | Agent claims | id, agent, title, project, priority |
task.progress | Progress logged | id, agent, note, note_type |
task.completed | Submitted for review | id, agent, title |
task.submitted_for_review | IN_REVIEW state set | id, agent, title |
task.transferred | Claim transferred | id, from_agent, to_agent, title |
task.blocked | Blocked | id, agent, reason |
review.approved | Approved | id, reviewer, note |
review.rejected | Rejected | id, reviewer, reason |
task.priority_updated | Batch priority | ids, priority |
task.project_updated | Batch project | ids, project |
Concurrency guarantees
All mutations use Serializable isolation via sql.TxOptions{Isolation: sql.LevelSerializable}. This means concurrent transactions are serialized — two claim-next calls at the same moment behave as if one ran after the other.
Retry loop: If a transaction hits SQLITE_BUSY (write contention), the service retries up to 3 times with exponential backoff + jitter (100ms/200ms/400ms base).
Read replicas: Non-mutating queries (search, view) use a separate read connection with PRAGMA query_only = 1. This prevents accidental writes and allows 3 concurrent readers without blocking writers.
Error handling
Every error follows a consistent format:
# State machine error — includes current state for debugging
{"error":"cannot approve: task is DONE (current state: DONE, expected: IN_REVIEW)"}
# exit code 2
# Not found
{"error":"task not found"}
# exit code 2
# Dependency blocked
{"error":"task TASK-5 has unmet dependencies"}
# exit code 2