Beads
Beads is Cacophony's built-in task management system. One node in the cluster serves as the authoritative beads host; all other nodes proxy requests through their local daemon.
Architecture
- caco-beads — native Rust library for storage and mutation
- caco daemon — serves beads in-process only for the legacy scalar-primary form; in service-object mode it always proxies and never opens a project beads database
- caco-bd-daemon — exclusive SQLite owner and standalone beads authority when
beads.primaryuses the service-object form - caco bd — first-party CLI client that talks to the local daemon
caco bd always talks to the local daemon, which serves the request in-process (legacy scalar primary), proxies it over loopback to a same-node caco-bd-daemon, or forwards it over cluster mTLS to the authoritative host.
- One owner: in service-object mode only the standalone process opens
beads.db; even the same-node main daemon fails closed rather than opening it. - Auth: same-node proxying re-reads
$CACOPHONY_DIR/tokens/node.tokenbefore each hop, so it survives token repair and restarts. Remote peers always use the primary's advertisedcluster_portover mTLS, never another node's loopback. - Which primary?
/api/v1/beadsis the source of truth, andcaco statusandcaco bd statusagree with it after a switchover.caco bd statusalso shows mode, ports, proxy target and auth scheme. - Split views: if nodes disagree about the primary after a config change,
caco bd trigger-vote --dry-runshows each node's view and the planned action, andcaco bd trigger-voteconverges them through a normal election, without a restart.
Storage
$CACOPHONY_DIR/beads/<project>/
.beads/issues.jsonl # Git-tracked (beads branch)
beads.db # Authoritative SQLite store (WAL alongside)
beads.dbplus its WAL is the runtime authority for reads, writes, ownership generations, dispatch state, mutation history, and export-dirty metadata..beads/issues.jsonlis a deterministic Git-owned snapshot and rebuild source, not the live query path. It contains exactly one JSON object per bead, ordered by ID for stable diffs.- Mutations commit transactionally to SQLite and mark the export dirty before the API returns. A bounded sync/export pass later rewrites JSONL and commits only when its deterministic content changed.
- External JSONL import is exceptional: startup/recovery or an observed external branch change may reconcile it into SQLite, while ordinary reads and writes never rematerialize from JSONL.
- The beads host creates the configured beads branch when missing, and quiet sync ticks avoid no-op commits and pushes.
Runtime access and load control
Bounded admission, query deadlines, read connections, and indexes keep expensive reads from occupying every worker. All-project status is single-flight and cached for five minutes; this staleness applies to aggregate diagnostics, not CRUD or project lists.
Dispatch wakes the target immediately; the cached batch endpoint and five-minute jittered poll are anti-entropy. Legacy per-project polling is rejected before SQLite work. UI snapshots fetch only interactive states through a short-lived, coalesced mesh cache.
Single-primary standalone deployments omit the multi-primary peer-probe mesh. Cluster mTLS still serves remote API proxying.
Operational invariants
caco status --jsonshould report the standalone beads host reachable and the main daemon not API-backpressured.- Only the
caco-bd-daemonPID should hold projectbeads.dbfiles in service-object mode. - A persistently growing WAL, repeated
read-pool-wait slow, ownership-fence refusal, or route timeout is incident evidence; an idle or checkpointed WAL may be zero bytes. - Aggregate status freshness may lag by its cache window, but authoritative CRUD and filtered list reads must remain live and low latency.
Forensic snapshots
Before destructive reconciler rewrites, Cacophony writes a defensive snapshot under .beads/snapshots/<UTC-stamp>/ containing the pre-write issues.jsonl and, when present, beads.db. Automatic snapshot creation is rate-limited to at most one snapshot per project every 15 minutes so a busy reconcile loop cannot consume unbounded disk. Operators can inspect and prune the retained backlog with first-party commands such as caco bd snapshot list --project <project> and caco bd snapshot rotate --project <project> --retention-days N --dry-run; destructive rotation preserves pinned snapshots and should be previewed before cleanup.
Bead Data Model
| Field | Type | Description |
|---|---|---|
id | string | Unique bead identifier (e.g., bd-abc) |
title | string | Short description |
description | string | Detailed description |
status | enum | Persisted state: open, in_progress, merging, closed, deleted, draft, permanent; list/detail surfaces may render an effective blocked state when dependencies are unresolved |
priority | 0-4 | 0 = highest (critical), 4 = lowest |
type | enum | task, bug, feature, epic |
creator | caller ID | Who created the bead |
assignee | stable owner ID | Who is currently working on it (empty if unclaimed or closed). Agent-owned work uses the stable project:agent-id form rather than a host-prefixed caller ID; close provenance is preserved separately in closed_by. |
labels | string[] | Unique label tags |
spoken_name | string? | Short spoken name for TTS surfaces (auto-generated if omitted) |
dependencies | bead ID[] | Beads that must close first |
created_at | timestamp | Creation time |
updated_at | timestamp | Last update time |
closed_at | timestamp? | Close time (null unless closed) |
CLI Usage
caco bd show --bead-id <id> and its info alias select the project from --project, then CACO_PROJECT, then CACOPHONY_PROJECT before contacting the daemon.
Managed workers therefore read their assigned board without needing cross-project listing authority.
A missing or forbidden scoped bead never triggers a global fallback.
Only when neither a flag nor an environment project is selected do these commands discover the exact bead ID globally, subject to the caller's existing daemon permissions.
Create a Bead
caco bd create --project lighthouse --type task --priority 1 \
--title "Expose the latest build result over the API" --description-file ./bead-body.md
created: bd-a00001 — Expose the latest build result over the API
# or pipe the description in, or create and claim it in one step
printf '%s\n' "$BODY" | caco bd create --title "Fix flaky queue" --description-stdin
caco bd create --title "Fix regression" --type bug --claim true
- One description source:
--description,--description-file PATHor--description-stdin. Files and stdin keep Markdown with backticks and$intact; create warns when a description looks damaged by shell substitution, and--forcefiles it anyway once you have checked. - No duplicates: an exact copy of a non-closed bead (same title, description, type and priority) is refused with
already_existsnaming the original. Beads labelledbroken-on-mainare also matched on overlapping title keywords, so two agents reporting one regression in different words file it once. - Create and claim:
--claim truesends both intents together, keeping the assignee through forwarding and outbox replay.
| If create reports | Then |
|---|---|
indeterminate_create | the bead may exist despite a transport failure: find it before filing again |
indeterminate_claim, claim_after_create_unverified, claim_after_create_error | the bead may exist but its owner is unconfirmed: check it with caco bd show before claiming |
The TUI's bead detail can show a provenance timeline of signed transition records; recording them on every mutation is groundwork that does not yet change behaviour.
Converge beads-primary views
# Inspect without emitting election events
caco bd trigger-vote --dry-run
# Nudge peers with a bounded vote/switch reaffirmation when the preview is safe
caco bd trigger-vote
The command is safe to retry: it no-ops when the routing view is already converged, refuses when the authoritative service is unreachable outside a known maintenance window, and reports triggered plus would_trigger in --json output for controllers and TUI surfaces.
List Beads
# List all open beads
caco bd list --status open
# List ready beads (open + unblocked)
caco bd list --ready
# Filter by assignee (managed agents can use their short id)
caco bd list --status in_progress --assignee "$CACO_AGENT_ID"
# Explicit stable owner IDs are preserved
ASSIGNEE="cacophony:agent-id"
caco bd list --assignee "$ASSIGNEE"
# Filter by type and priority
caco bd list --type bug --priority 0
# Search across all statuses for an existing broken-on-main failure
caco bd search --query "integration_observability::flaky_case"
# Multi-word searches fall back to significant-token matching
caco bd search --query "cargo clippy vec_init_then_push transcription"
caco bd search returns both data.query.query and the deprecated compatibility data.query.test_name field in JSON output. Exact substring matches still win; for multi-word queries, common command/failure filler words such as cargo, test, and clippy are ignored when enough significant tokens match the bead title, description, or spoken name.
By default caco bd search runs a client-side substring scan. Add --fts to route to the daemon's server-side indexed FTS5 full-text search (BM25-ranked via the GET /beads/search endpoint) instead — useful on large boards. --fts is project-scoped and is ignored with --all-projects .
Stalled, operator-action, graph, and triage views
# In-progress beads whose current claim is older than a threshold
caco bd stalled --threshold 6h
# Beads needing manual operator action (operator-action label / [operator-action] prefix)
caco bd operator-actions
# Relationship graph (parent/child + dependencies) as ascii, dot, or mermaid
caco bd graph --root bd-a00002 --format mermaid
# Drain the draft pool: promote / discard / defer / merge
caco bd triage --next
caco bd stalled lists in_progress beads whose current claim exceeds a duration threshold (default 6h) so operators can find wedged work. caco bd operator-actions surfaces beads flagged for manual operator handling (an operator-action label or [operator-action] title prefix) that generic auto-claim deliberately skips. caco bd graph renders parent/child plus dependency relationships from an optional --root with bounded --depth as DOT, mermaid, or terminal ASCII. caco bd triage processes the draft pool oldest-first with promote/discard/defer/merge actions and an --interactive loop.
Read-only planning helpers
BEAD_ID="bd-a00002"
caco bd oracle "$BEAD_ID"
caco bd related "$BEAD_ID" --limit 10
caco bd oraclegives a deterministic, read-only estimate for one bead: complexity and risk from 1 to 5, a duration bucket, a recommended profile and model class, likely touched surfaces, and decomposition hints for broad work.caco bd showincludes the same summary row.caco bd relatedranks related work by dependency edges, parent and sibling links, shared labels, creation batch and optional landed-file overlap. It never adds links or changes the bead.- When dispatching, the oracle's profile and model suggestion is advisory; explicit
--profile,--modeland--nodealways win.
Closed-bead archive
Old closed beads can be moved out of the hot store into a per-project archive and restored later. Archiving is policy-driven (closed age and count), dry-runnable, and idempotent.
caco bd archive list --project my-project --since 30d
caco bd archive show --bead-id "$BEAD_ID"
caco bd archive restore --bead-id "$BEAD_ID" --confirm # back to an open bead; the archive record is kept
Restore refuses when a live bead with the same ID exists. Retry policy, audit auto-dispatch, oracle-driven decomposition and monthly archive digests exist but are off by default: until enabled they preview decisions without creating beads, spawning agents or calling a model.
caco bisect finds the bead that introduced a regression: start takes the target bead, a test command and a JSON list of candidate commits with their bead IDs; run tests the next midpoint commit in an isolated worktree and records good or bad; status shows remaining candidates and, once narrowed, the guilty commit's beads with caco bd show and caco blame follow-ups. It never files beads itself. See the CLI reference for flags.
caco blame is the read-only source-history companion for beads: it parses git blame --porcelain, extracts bead IDs from commit subject/body text, and renders line-level bead hints without mutating the board. caco blame commits --bead-id searches commit messages for one bead and lists the touched files, which is useful when investigating why a line or area changed before filing follow-up work. caco bd audit-provenance --file records.json validates signed provenance records from a single JSON object, an array, or {records:[...]}; supplying --key-id and --key enables keyed SHA-256 verification, while unsigned records are reported as legacy instead of broken.
File Attachments
Beads can carry multiple versioned file attachments. Attachment rows are bead metadata, not free-floating project files: uploads are stored under .beads/attachments/<bead_id>/ and bead records keep relative paths, filenames, MIME types, sizes, SHA-256 hashes, versions, capture metadata, and optional descriptions. Re-attaching the same filename creates a new immutable version instead of overwriting the old file, absolute paths and .. traversal are refused on readback, and attachment access follows the same project-scoped bead authorization as the bead itself.
Create/update payloads may append attachments through an attachments array, and the dedicated endpoints under /api/v1/projects/<project>/beads/<bead_id>/attachments provide the same append/read workflow for UI surfaces. TUI, caco-web, Android, and CLI bead detail views render attached filenames and version numbers so operators can see evidence files without inspecting the beads checkout directly.
Claim a Bead
caco bd claim # the next ready bead
caco bd claim --bead-id bd-a00001 # this one
A claim assigns the bead to the caller atomically, commits it and emits a bead_claimed feed event. Drafts are never ready and cannot be claimed. A claim without an ID takes:
- an open, unblocked, unassigned bead, highest priority first;
- for a generic worker, skipping coordination parents and checkout-drift repair beads, which must be claimed by ID;
- for a generic worker, preferring general work over Android, companion, web and macOS beads, though it still takes those when nothing else is ready (surface specialists are unaffected);
- skipping any IDs passed in
--exclude-bead-idfor this request only, without hiding them from other workers.
When nothing qualifies, no_ready_beads reports how many beads each rule excluded, with sample IDs. indeterminate_claim means a restart window hid the outcome: check caco bd show or your in-progress list before retrying. Assignee filters accept a bare agent ID, project:agent-id or node:project:agent-id for the same agent.
Unclaim a Bead
caco bd unclaim bd-abc
Releases a claimed bead back to open status, clearing the assignee. Closing a bead also clears the current assignee so closed work no longer appears in assigned/in-progress views; the closer remains auditable through closed_by.
An unclaim, and the rollback of a dispatch that never reached its agent, advances the ownership generation with a binding release. A stale registry row or peer snapshot from the former owner, even one on an unreachable node, therefore cannot put that owner back or take over the next dispatch.
A cross-project claim grant (including wildcard or friend-project beads_claim) also permits releasing the authenticated agent's own claim. The authoritative store checks the current qualified owner and advances its generation under one write lock. Caller fields cannot impersonate another owner; an unqualified legacy owner is not sufficient proof of cross-project ownership. This grants neither foreign-owner release nor access to non-bead project operations. Existing operator/controller recovery authority is unchanged, and forwarded requests retain scoped agent identity rather than falling back to node authority.
Transfer Ownership Under a Controller
A controller can move a bead to a new owner and fence the move, so no background process quietly moves it back.
caco bd transfer-ownership --bead-id bd-abc --to lighthouse:heron \
--expect-owner lighthouse:wren --pr 1942 --head ccae607f6 --reason "queue drain"
- Generation: every ownership-establishing write (claim, spawn-and-claim handoff, assignee update, transfer) advances
ownership_generation. It is authoritative bead state, so it survives index rebuilds, replication, restarts and standby promotion. - Fence: an agent-registry reconcile, peer snapshot, replayed outbox write, dispatch reconcile or goal-derived fallback that would restore an older ownership decision is refused with a receipt naming the source, the writer, both owners, both generations and the controller.
- Stand-down: the refused owner's agent clears its
bead_idand assigned-bead residue so it stops re-proposing the reversion; its files, checkout and adopted head are untouched. - Guards:
--expect-ownerand--expect-generationrefuse rather than clobber when the board has moved on. - Dispatch reservations: a
bd_dispatch:<bead>reservation belongs to its dispatch. Only the agent that dispatch created may adopt it, through the same next-generation handoff; every other background writer is refused. - Competing valid receipts: at the strongest ownership generation, the latest daemon acceptance wins, with greatest request ID as the deterministic timestamp tie-breaker. Payload timestamps and replica arrival order do not decide ownership. Losing receipts remain immutable history, the bead stays readable and writable, and old competing-chain quarantines resolve automatically on startup. See receipt ordering and replication.
- Receipt quarantine: genuinely invalid accepted snapshots remain isolated until a controller runs
caco bd update-receipt --resolve-quarantine. The preview lists the durable candidates. When no internally consistent evidence reaches the strongest generation, it offers oneboard_adoptioncandidate that keeps the latest board snapshot under a fresh controller transfer one generation higher; applying it needs the exact candidate hash and a reason. Ordinary valid ownership races do not require this workflow. - Preserved state: unlike a claim takeover, a transfer keeps the in-flight PR marker, queued dispatch reservation and recorded merge commit. Beads that were never fenced keep the old drift-repair behaviour.
Dispatch a Bead
# Claim a bead and spawn an agent for it in one step
caco bd dispatch --bead-id bd-abc
# Dispatch to a specific remote node
caco bd dispatch --bead-id bd-abc --node worker-1
# Queued dispatch (async pickup when target is unreachable)
caco bd dispatch --bead-id bd-abc --node worker-1 --queued
# Where is it now? queued, picked_up (agent), running, completed or failed
caco bd dispatch status --bead-id bd-abc
Atomically claims the bead and spawns a new managed agent to work on it. Combines caco bd claim and caco agent new into a single operation. If the launch outcome is ambiguous (e.g., the create response is lost), dispatch reconciles against authoritative bead and agent state before reporting failure. When a launch is still settling, it reports a pending/in-flight result instead of a hard failure so controllers do not accidentally double-staff the same bead.
Dispatch is a worker-spawn surface, so it refuses EPIC/umbrella records encoded as type: epic, a [EPIC] title prefix, or an epic label. Treat those records as coordinator containers and dispatch a child implementation bead instead.
Before a synchronous cross-node dispatch, the daemon probes the target live for up to three seconds. Only a target that does not answer is queued instead; a stale cached liveness label never decides this, and a successful probe refreshes that label. Both paths report the same {agent_id, node, state}: a queued request waits --wait-secs (default 60) for the target to pick it up and prints the agent ID, or prints one next step naming caco bd dispatch status. caco bd show shows the same state on its dispatch line.
When dispatching to a remote node, if immediate forwarding fails, the --queued flag records the dispatch intent on the bead for async pickup by the target node. Queued dispatches apply the same EPIC/umbrella guard and expire after a configurable TTL (projects[].beads.dispatch_queue_ttl). Duplicate queued requests for the same target node, project, and bead reuse the existing queue entry, and stale duplicate entries are dropped during drain so capacity-backed retries do not spawn multiple workers for one bead.
caco bd dispatch --dry-run (or --preview) is a no-mutation placement preview. It renders the would-request dispatch metadata, selected/ready/blocked node readiness rows, score summaries, and rejection reasons without claiming the bead, contacting the daemon for spawn, or starting an agent.
Evaluate a Markdown Bead File
caco bd file --markdown ~/beads.md
Evaluates a Markdown file in place. Level-one headings select the project for the level-two sections beneath them. A section headed ## X <prompt> uses the existing expansion endpoint in one-bead mode, creates exactly one proposal, then rewrites the section as ## [bd-id] <title> with priority/labels, ### Description, and a fenced ### Prompt archive. A section headed ## N <title> creates that literal title, respecting an optional P0 | tag1 tag2 line and ### Description. A bracketed bead ID pulls the title, priority, labels, and description from the daemon while preserving its prompt archive; synchronization is never pushed from the file to the bead. Other level-two blocks remain unchanged.
Each successful create, expansion, or pull is checkpointed with a same-directory atomic file replacement before the next section runs. A later daemon or authorization failure therefore leaves earlier sections durably annotated and safe to repeat instead of truncating the file or recreating those beads. Project headings do not bypass caller scope: the daemon still authorizes every project independently. Generated sections include an invisible description-end marker so level-two headings inside daemon-authored descriptions are not mistaken for new bead sections.
Expand and Refine Free Text into Beads
# Turn unstructured notes into structured beads via LLM
caco bd expand --text "We need rate limiting on the API and also fix the auth bug"
# Preview the proposed beads without creating anything
caco bd expand --dry-run --text "Split the mobile QA epic into focused tasks"
# Or omit --text to compose the brief in $VISUAL / $EDITOR
caco bd expand
Passes free-form text through an LLM to generate one or more structured beads with titles, descriptions, types and priorities. Every client (CLI, web, TUI, Android, macOS) calls the same daemon endpoint; none picks a model.
- Models: proposals use
beads.expand.model, falling back tobeads.rewrite.modeland then, with a warning,llm.fast. Image Quickfile describes the image with the vision alias first. Draft rewrite and/beads/refineusebeads.rewrite.model. Aliases must exist underllm. - Preview:
--dry-run(or--preview) returns the proposals without creating anything. - Editor input: without
--text, the CLI opens$VISUAL/$EDITOR(defaultvi); an empty save creates nothing. - Undo and refine: quick-file surfaces keep new beads visible with delete and open actions. Web and TUI can refine them with follow-up instructions via
/api/v1/projects/<project>/beads/refine, which updates the same IDs instead of filing duplicates. - Dispatch: in the TUI, Ctrl+H hands every bead just created to one worker rather than one agent per bead. The TUI's other quick-file keys are listed on the TUI page.
- macOS: b outside text fields, or “Quick-file bead” in ⌘K, with Copy IDs, Copy show commands and Open Beads afterwards.
Suggest Beads (Ctrl+S in the TUI) asks for five varied suggestions from project context and only ever proposes:
- It refuses context that is empty, too thin, too broad or has no project, and drops vague, duplicate or very short candidates.
- Proposals stay pending until the operator accepts, edits and accepts, or rejects each one.
- Creating a backlog bead from an accepted proposal needs an explicit per-proposal confirmation and is idempotent.
Lifecycle Metrics
caco bd stats
Reports per-project lifecycle metrics including average time-to-close by priority and bead type, recent closure counts (24h and 7d windows), throughput in beads/hour, and the longest-running currently in-progress beads. Supports --json for machine-readable output.
Close a Bead
caco bd close bd-abc
Records an authorized completion decision, persists the closing metadata, and emits a bead_closed feed event. It does not fetch main, scan source commits, or require the bead ID in a commit message. Use --note or --reason for the rationale. Ownership, authorization, generation and permanent-bead confirmation checks still apply.
--validate-on-main, --main-ref and --admin-override remain accepted compatibility no-ops; none grants additional authority. Work tracked in external issues, device acceptance and follow-up tasks uses the same ordinary close path.
Update a Bead
# Update priority and add labels
caco bd update --bead-id bd-abc --priority 0 --add-label urgent --add-label security
# Move a bead from one configured project to another
caco bd update --project cacophony --bead-id bd-abc --target-project docs
--project identifies the source project that currently owns the bead. --target-project moves it to another configured project after local project-name validation, preserving the bead ID while resetting it to an open/unclaimed state in the destination.
Query Parameters
The list endpoint supports filtering:
| Parameter | Description |
|---|---|
status | Filter by status (single value or comma-separated, e.g. open,in_progress) |
ready | Open + unblocked only |
assignee | Filter by stable assignee owner ID. When the CLI value matches the current CACO_AGENT_ID / CACOPHONY_AGENT short id, it resolves to project:agent-id; explicit colon-containing values are preserved. |
creator | Filter by creator caller ID |
priority | Filter by priority level |
type | Filter by type |
label | Filter by label |
since | Filter by created_at ≥ value (RFC3339 or relative duration like 1h, 3d) |
before | Filter by created_at < value (RFC3339 or relative duration) |
sort | Sort by priority, created_at, or updated_at |
reverse | Reverse the selected sort order |
count_only | Return only the matching bead count (omits full rows) |
limit | Max results |
cursor | Pagination cursor |
Commit Messages
Beads mutations produce structured commit messages on the beads branch:
bd: close bd-123 by localhost:cacophony:sunny-owl
bd: create bd-124 by localhost:cacophony:operator
bd: claim bd-124 by localhost:cacophony:bold-fox
Consistency Model
- One writable beads host per cluster
- Writes serialized per project
- Reads observe the latest committed local state
- Remote push is async after local commit (default behavior)
- Use
caco bd syncfor immediate JSONL-DB reconcile, pull, and push - Overlapping manual or automatic sync attempts are debounced as lock-contention no-ops/retryable successes; they should not be treated as daemon crash-log failures
- Transient beads git fetch/push failures back off from the time the blocking git operation finishes, not from when it started, so timeout-length failures do not immediately re-enter sync and keep claim/close gates closed almost continuously
API Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/v1/projects/<project>/beads | Create bead |
| GET | /api/v1/projects/<project>/beads | List beads |
| GET | /api/v1/projects/<project>/beads/<id> | Bead detail |
| PATCH | /api/v1/projects/<project>/beads/<id> | Update bead |
| DELETE | /api/v1/projects/<project>/beads/<id> | Delete bead |
| POST | /api/v1/projects/<project>/beads/refine | Apply post-creation refinement instructions to selected existing beads |
| POST | /api/v1/projects/<project>/beads/claim | Claim next ready bead |
| POST | /api/v1/projects/<project>/beads/<id>/close | Close bead |
| GET | /api/v1/projects/<project>/beads/<id>/attachments | List bead attachments |
| POST | /api/v1/projects/<project>/beads/<id>/attachments | Append one or more bead attachments |
| GET | /api/v1/projects/<project>/beads/<id>/attachments/<attachment_id> | Read one attachment after bead authorization |
| GET | /api/v1/projects/<project>/beads/stats | Lifecycle metrics (avg time-to-close, throughput, longest in-progress) |
| GET | /api/v1/beads/all | Global bead list (all projects, includes owning project per row) |
| GET | /api/v1/beads/spoken-names | Lightweight cross-project TTS projection with only project, bead ID, and spoken-name rows |
| GET | /api/v1/beads | Beads routing and status summary |