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

Every bead command goes to the caller's local daemon. On a node that is not the beads primary, the daemon forwards the request over mutual TLS to the primary's advertised cluster port. On the primary, the daemon proxies over loopback to the standalone caco-bd-daemon, re-reading the node token for each hop. The bead daemon is the only process that opens the SQLite database and commits each change to SQLite and its WAL before anyone is notified, then exports a deterministic one-row-per-bead JSONL snapshot to the beads Git branch only when content changed. A standby or recovered former primary imports from that branch and stays fenced until it proves it has caught up.
RoutingOnly the primary's bead daemon opens the database; everyone else reaches it through their own daemon, and the Git branch is an export, not the request path.
Routing

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.token before each hop, so it survives token repair and restarts. Remote peers always use the primary's advertised cluster_port over mTLS, never another node's loopback.
  • Which primary? /api/v1/beads is the source of truth, and caco status and caco bd status agree with it after a switchover. caco bd status also 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-run shows each node's view and the planned action, and caco bd trigger-vote converges 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)

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

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

FieldTypeDescription
idstringUnique bead identifier (e.g., bd-abc)
titlestringShort description
descriptionstringDetailed description
statusenumPersisted state: open, in_progress, merging, closed, deleted, draft, permanent; list/detail surfaces may render an effective blocked state when dependencies are unresolved
priority0-40 = highest (critical), 4 = lowest
typeenumtask, bug, feature, epic
creatorcaller IDWho created the bead
assigneestable owner IDWho 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.
labelsstring[]Unique label tags
spoken_namestring?Short spoken name for TTS surfaces (auto-generated if omitted)
dependenciesbead ID[]Beads that must close first
created_attimestampCreation time
updated_attimestampLast update time
closed_attimestamp?Close time (null unless closed)
A draft bead is never ready or claimable until it is updated to open. An open bead with an unresolved dependency is shown as blocked, which is computed rather than stored. Claiming or dispatching an open, unassigned, unblocked bead moves it to in progress with an assignee; unclaim returns it to open. When the work is published as a pull request awaiting merge the bead moves to merging and keeps its assignee. Beads close when an authorized actor records that the work is done; no matching source commit is required. Permanent beads are umbrella records whose close needs explicit confirmation by their assignee; closed beads can be archived and restored as open.
Status lifecycleBlocked is computed from open dependencies, not stored; merging keeps the assignee while the PR's auto-merge lands the work.

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
If create reportsThen
indeterminate_createthe bead may exist despite a transport failure: find it before filing again
indeterminate_claim, claim_after_create_unverified, claim_after_create_errorthe 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

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:

  1. an open, unblocked, unassigned bead, highest priority first;
  2. for a generic worker, skipping coordination parents and checkout-drift repair beads, which must be claimed by ID;
  3. 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);
  4. skipping any IDs passed in --exclude-bead-id for 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"
A controller transfers bead bd-abc from agent wren to agent heron with a compare-and-set guard on the current owner. The beads primary checks the guard, advances the ownership generation from 4 to 5 and records a receipt naming the previous owner, the new owner, the controller and the expected pull request and head. Later a background writer, such as an agent-registry reconcile, a peer snapshot or a replayed outbox write, tries to put wren back. The primary refuses it with a receipt carrying both owners and both generations, and stands wren down: wren's agent forgets the bead but keeps its files, checkout and head.
Ownership transfer and its fenceThe generation only moves forward, so a stale writer is refused and the previous owner stands down.

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.

caco bd dispatch asks the local daemon to start a worker for one bead. The daemon refuses epics, picks one target node, and reserves the bead for that node on the beads primary with a reservation generation, so the bead stops being ready for anyone else. It forwards an agent create pinned to that node and generation; the target daemon starts the agent in its own worktree and adopts the reservation as the claim instead of claiming again. Dispatch then verifies the agent and its claim before reporting success. Progress streams back as phases, so a dropped connection still shows the last step that completed, and a lost response is answered from the durable handoff receipt rather than a second spawn.
caco bd dispatchOne reservation per bead: the target adopts it as the claim, and a lost response is answered from the handoff receipt instead of spawning again.

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.

Suggest Beads (Ctrl+S in the TUI) asks for five varied suggestions from project context and only ever proposes:

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:

ParameterDescription
statusFilter by status (single value or comma-separated, e.g. open,in_progress)
readyOpen + unblocked only
assigneeFilter 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.
creatorFilter by creator caller ID
priorityFilter by priority level
typeFilter by type
labelFilter by label
sinceFilter by created_at ≥ value (RFC3339 or relative duration like 1h, 3d)
beforeFilter by created_at < value (RFC3339 or relative duration)
sortSort by priority, created_at, or updated_at
reverseReverse the selected sort order
count_onlyReturn only the matching bead count (omits full rows)
limitMax results
cursorPagination 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

API Endpoints

MethodEndpointPurpose
POST/api/v1/projects/<project>/beadsCreate bead
GET/api/v1/projects/<project>/beadsList 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/refineApply post-creation refinement instructions to selected existing beads
POST/api/v1/projects/<project>/beads/claimClaim next ready bead
POST/api/v1/projects/<project>/beads/<id>/closeClose bead
GET/api/v1/projects/<project>/beads/<id>/attachmentsList bead attachments
POST/api/v1/projects/<project>/beads/<id>/attachmentsAppend 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/statsLifecycle metrics (avg time-to-close, throughput, longest in-progress)
GET/api/v1/beads/allGlobal bead list (all projects, includes owning project per row)
GET/api/v1/beads/spoken-namesLightweight cross-project TTS projection with only project, bead ID, and spoken-name rows
GET/api/v1/beadsBeads routing and status summary