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.

01TODO
02IN_PROGRESS
03IN_REVIEW
04DONE
TransitionCommandEffect
TODO → IN_PROGRESSclaim or claim-nextAgent acquires lease, task is locked for 15 minutes
IN_PROGRESS → DONEcomplete (default)Direct complete without review gate
IN_PROGRESS → IN_REVIEWcomplete --reviewSubmit for review. Lease released.
IN_REVIEW → DONEapproveReviewer confirms work. Final state.
IN_REVIEW → TODOrejectReviewer sends back with reason. No lease assigned.
IN_PROGRESS → BLOCKEDblockAgent 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)

Lease model

Every claimed task gets a lease with a default of 15 minutes. The lease is the crash recovery mechanism:

lease-lifecycle
# 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

Transfer protocol

Tasks can be transferred from one agent to another:

transfer
kanban task claim TASK-1 --agent bob --transfer --to charlie

Requirements:

Dependency model

Tasks can declare dependencies using --depends-on with comma-separated task IDs:

deps
kanban task dispatch --title "Write tests" --role worker --depends-on TASK-1,TASK-2

Dependency resolution rules:

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 typeWhenPayload includes
task.createdTask dispatchedid, title, role, priority, project
task.claimedAgent claimsid, agent, title, project, priority
task.progressProgress loggedid, agent, note, note_type
task.completedSubmitted for reviewid, agent, title
task.submitted_for_reviewIN_REVIEW state setid, agent, title
task.transferredClaim transferredid, from_agent, to_agent, title
task.blockedBlockedid, agent, reason
review.approvedApprovedid, reviewer, note
review.rejectedRejectedid, reviewer, reason
task.priority_updatedBatch priorityids, priority
task.project_updatedBatch projectids, 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:

error-output
# 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