stack · for colleagues · 2026
Cacophony
Project-first swarm management for coding agents, wherever they run.
Harry Askham · a.skh.am/stack/deck
DRAWING CACO / GA-01
SHEET 1 OF 26
DRAWN H. ASKHAM + AGENTS
Who I am, and what this is: the system I use to run many coding agents at once, on my own projects and at work.
the problem
Many agents, one repo, one person
Parallel agents in one folder overwrite each other.
A session ends and its context is gone.
Fifty pull requests a day conflict with each other.
I want to steer from anywhere: terminal, browser, phone, watch, voice.
Start with the pain. Each point here maps to one component later: clones, beads, Caravan, and the surfaces.
16 march 2026 · the design note
“Too coupled.”
My first two attempts (djent, then io-mcp) worked, but each did too much in one place. The next pass had three rules:
Composable small tools, each testable alone
Unix philosophy stdio, JSON, pipeable, fast
Remote-agnostic same workflow on any machine
Lineage: djent (parallel agents + beads) → io-mcp (voice, ring, self-management) → caco
My first two tries worked, but each did too much in one place. These three rules drove every later decision.
architecture
Five layers
operator surfaces CLI · TUI · web · Android · Wear · iOS · macOS · MCP plugins
local node services caco-daemon · caco-bd-daemon · caco-web · sidecars · TTS/STT
cluster coordination mTLS daemon mesh · PKI · feed replication · peer health
project state canonical checkouts · agent clones · beads branch · summaries
execution backends tmux / Herdr · dynamic nodes · AKS · Codespaces · microVMs
Surfaces are clients of the daemon API, never independent authorities. Durable state is append-only or git-backed, so you can inspect it without the UI.
The layers, top to bottom. The point to land: surfaces are clients only. State lives in git or append-only logs.
the cli
caco does a lot
agents agent · pi · claude · codex · pico · pi-daemon · heartbeat
work bd · decision · choices · suggest · ops · bisect · blame · changelog
delivery pr · cara · github · release · test · build · checkout
talk msg · chat · notify · loop · audio · tts · stt
flows flow compile · simulate · graph · run · input · watch
fleet node · fleet · cert · token · secret · relay · sidecar · service · cron · mode
memory scratch · file · link · feed · event · log
surfaces tui · web · macos · tendril · image
103 top-level commands. Every one has --help --json, and the same tree is served as MCP tools, so agents drive caco the way I do.
Show the breadth before the depth. Everything here is one binary with one help format, and it's all agent-readable.
rule one
Local clients talk to the local daemon
caco CLI · TUI · web · phone loopback · bearer token local daemon mutual TLS peer daemons
One auth pattern and one place to infer the caller (node:project:agent). The daemon decides: serve locally, proxy to a same-node service, or forward to the authoritative peer. Remote peers never touch another node's loopback port.
The one rule that keeps auth and routing sane. Mention that remote peers never use someone else's loopback port.
beads
A task list agents can share
IDs like bd-1a2b3c, priorities P0–P4, dependencies caco bd claim hands the next ready task to exactly one agentone node owns the SQLite store; everyone else proxies through their daemon commits and PRs cite bead IDs, so history links back to intent
Beads is shared memory for agents. The atomic claim is what lets many agents run without duplicating work.
an agent's life
Claim → clone → land
bd create claim own clone build · test reintegrate direct · PR main
Identity node:project:id; branch agent/<node>/<project>/<id>. Each agent gets a dedicated clone that shares object storage with a daemon-maintained canonical checkout. States: pending → starting → running → waiting / blocked → completed / failed.
Walk through one agent: claim, own clone, build, reintegrate. The branch naming makes the whole history traceable.
declarative agents
Agents are config, not processes
# projects.yaml
cacophony:
agent_defaults:
profile: [caco-surfaces]
agents:
persistent:
config-dev-0: {{ set "nodes" (list "node-a")
values.config-dev }}
# a role is imports + profiles
dev: { imports: [agents/dev.yaml] }
pr-monitor: { imports: [agents/pr-monitor.yaml] }
life-dev: { imports: [agents/pi-paratenic-dev.yaml],
profile: [global-bead-scope],
all_projects: true }
projects.yaml role ↓ agents/dev.yaml ↓ cacophony_dev.yaml ↓ base.yaml ↓ default_model + default_profile
Persistent agents are declared per node; the daemon keeps them running. Workers are spawned from the same recursive defaults.
The big idea: I don't start agents by hand. They're declared, composed from imports, and the daemon converges on the declaration.
shaping a project
A project is a policy
cacophony-config:
remote: "ssh://git@github.com/harryaskham/cacophony-config.git"
always_update: true # live config source stays fresh
integration:
backend: pull_request
pr_base: origin/main
gate:
test_command: nix run .#yaml-check
check_command: caco --config config.yaml config validate
friend_projects: [cacophony] # agents here can see and reach that repo
agents:
persistent:
config-dev-0: {{ set "nodes" (list "node-a") values.config-dev }}
Remote, landing policy, CI gate, friend projects, actions, releases and its standing agents, all in one reviewable file.
How a project is shaped: what counts as done (the gate), how work lands (PR backend), and who works on it.
profiles
Profiles compose
---
name: worker
description: Implement the next ready bead.
hooks:
on_start:
- type: command
command: caco agent status set "starting"
post_tool_use:
- matcher: "Bash|Edit|Write"
hooks:
- type: command
command: caco msg speak "Continuing work"
reintegration:
mode: direct
---
Implement the assigned bead, validate it, and report completion.
A role stacks profiles: default_profile + mixins such as caco-surfaces, cacophony-bead-scope, global-bead-scope, project-controller. Runtime-agnostic: Pi, Claude or Codex. Landing modes: direct, pr_auto_merge, pr_review, artifact_only.
Profiles are just Markdown, so roles are reviewable and diffable. The same file drives Pi, Claude or Codex.
flows
Typed agent graphs
flow: caco.canary.default-agent
imports: { t: ./types.jtd.yaml }
in: { request: t.Request }
out: { result: { type: t.Result, checkpoint: true } }
nodes:
respond:
agent: { mode: ephemeral }
instructions: >-
Return request.text unchanged …
in: { request: t.Request }
out: { result: t.Result }
edges:
- { from: in.request, to: respond.request }
- { from: respond.result, to: out.result }
typed ports (JTD schemas), checked at compile time caco flow simulate: deterministic, records effects instead of doing themcaco flow input answers a node that suspended for a human61 registered flows: std.feature.delivery · pr.queue.drain · multi.surface.review · android.review · macos.review · fleet.error-triage … cron and feed events can start runs
Flows turn multi-agent procedures into typed, versioned graphs. Simulation is the killer feature: you can run a flow with no side effects.
messaging
Batch or wake
message_delivery:
default_mode: batch
batch_flush_interval_secs: 120
allow_runtime_opt_out: true
heartbeat:
interval_secs: 900
jitter_secs: 900
routine mesh chatter coalesces into one summary wake per recipient, at most every 120 s direct, lifecycle, system and --wake messages stay immediate without batching it's senders × recipients model turns msg send · broadcast · speak · reply · thread · inbox
The costly part of many agents is agents waking each other. Batching made the fleet quiet and cheap.
dormancy
Asleep until needed
pi-on-demand / pico-on-demand : the workspace and session persist, and no process runs between approved wakespi-daemon : logical sessions in one process; a wake is an idempotent turn, not a spawnheartbeats wake chosen agents on a jittered 15-minute cadence dormant isn't failed: the UI shows "wake & send" instead of a dead terminal
Most agents wait most of the time, so they shouldn't hold a process. Dormant agents make a big fleet affordable.
elicitation
Agents ask, anything answers
AHP input-needed: an agent pauses on a question and any client can settle it caco choices and caco decision: an async queue of operator decisionsspoken choices in Pi: pick by keys, ring, voice or a number tool approvals from the phone or the watch
Questions route to whatever is in my hand: the watch, the phone, a gamepad or my voice.
caravan · cara
Chains, not a pile
a caravan is labelled PR chain: each PR targets the one before it only the head has auto-merge; the head's number is the caravan ID deterministic compatibility and CI checks run mechanically judgement calls come back as typed decision points for an agent
Caravan has its own look because it's its own product, with its own site. Explain the chain model first.
caravan · cara sync
Every tick ends in a verdict
$ cara sync --json
{ "scheduler_status": {
"disposition": "waiting_ci",
"root": "#5181", "tail": "#5186",
"missing_required_runs": [],
"head_of_line": {
"pr": "#5183", "position": 3,
"block": "conflict",
"remedies": ["repair", "reshape", "evict"] } } }
healthy waiting_ci held retry_tick operator_action
Idempotent: run it again and it either converges or returns the same one decision. --rerun-failed retries only this generation's infrastructure failures. Illustrative output shape.
The point: automation never asks a vague question. It names the blocking PR, its position, why, and the allowed fixes.
runtimes
Agents don't need to be subprocesses
MAR · subprocess a CLI process per agent in a terminal; messages arrive as keystrokes
JUL · daemon pi-daemon: many sessions in one process, asleep between wakes
SEP · protocol AHP: an agent is a session any client attaches to
The runtime arc. The punchline: agents don't need to be subprocesses. pi-daemon keeps thousands of sessions asleep.
paratenic + culture
One live session, every client
Paratenic: one AHP host per node, on the official Rust crates a Pi extension exposes the already-running session; no second process Culture: one Rust core for TUI, web (WASM), Android, Wear, iOS, macOS Cacophony delivers over AHP, and never sends one message by two routes Petri (Paratenic labs, formerly Agent IO): gamepad, keyboard and voice control of the whole roster
Paratenic is the neutral AHP host; Culture is the client on every surface. One logical message never goes two routes.
voice
Speech in, speech out
# stt.yaml
stt:
model: mai-transcribe-2
vad: { engine: silero, threshold: 0.9,
min_silence_duration_ms: 2000 }
dictation: { chunk_secs: 10, overlap_secs: 1 }
smart_append: { enabled: true }
rewrite: { enabled: true, model: smart }
# tts.yaml
profiles:
harry: [defaults, models/harry, lang_rotation_gb]
harry-express: [defaults, models/harry, express_as_harry]
voice activity detection, chunked dictation, then a rewrite pass TTS in a cloned voice, with express-as styles and per-action voices the daemon plays through the phone's PulseAudio server, into my earbuds Pebble ring → transcribe → Jev picks an action → spoken receipt
The voice stack is config too. STT and TTS are declarative profiles, composed like agents.
surfaces
The same agents, everywhere
TUI, web, Android, Wear, iPhone, iPad, Apple Watch and native macOS. Fresh Cacophony macOS captures to come.
One daemon, one protocol, many clients. Each surface is thin.
the tui · graphics engine
The terminal is the canvas
1,913 kitty image uploads in 20 s with ultra-v2 animation on6.6 MB of images to the terminal; median frame 2.8 ms 238 fps of render headroom at a paced 30layout → raster → encode → kitty upload → place, with caching, dedupe and shared memory when the terminal allows it
The real TUI on a synthetic fixture (16 agents, a 60-bead board), captured in kitty. Numbers are that run's own.
Most TUIs draw with characters. This one also draws with images: glass chips, layered light, moving reflections. It stays fast because every image is cached, deduplicated and only re-sent when it changes.
the tui · materials
One renderer, many materials
ultra-v2 · glass · chrome · neon · nord · crt · e-ink · the original ultra: the same screen, one renderer, presets in config.
Each preset is a small YAML file over a shared material library. ultra-v2 is the only one that moves.
voice · pulse
One phone speaks for every node
# the phone (nix-on-droid)
pulseserver = {
enable = true;
virtualSinks = [ "cacophony" ]; # :4713
};
# every NixOS node
services.pipewire.pulse.tcp.enable = true;
# the Mac, the watch app, Pebble
PULSE_SERVER = "<the phone>:4713";
every agent on every machine speaks through the phone, into my earbuds the phone's mic feeds dictation back to whichever node asked the realtime tools can switch server, sink and source at runtime
One audio endpoint in my pocket instead of a speaker per machine. It's all declared in the same Nix config as everything else.
why it scaled
The ground was already Nix
~30 machines in one flake: NixOS, nix-darwin, nix-on-droid, NixOS-WSL cltv switch rolls any machine forwardAndroid apps are Nix apps; a tag ships to Play every CLI has --help --json and is also an MCP server
Why it scaled so fast: Nix was already on every machine, so anything an agent built shipped everywhere.
live from the atlas
The whole stack
One voice note, top to bottom: ring → caco → beads → an agent → earbuds.
Every piece in one view, layer by layer. The highlighted path is one voice note travelling through the stack.
numbers · 2026
What it produced
21k+ commits to Cacophony since 16 March
3,511 pull requests into Cacophony
17,659 beads on its board
5,187 PRs across my own repos
864 PRs on work repos, same setup
one tower per day · drag to turn
Numbers for 2026. Pull requests are the better measure after June, when agents started landing work as PRs. The calendar: under 80 PRs January to May, then over a thousand a month from June. Drag it to turn it.
lessons
What I'd tell you to copy
Give every agent its own clone and branch.
Keep one authority per piece of state; everything else proxies.
Make every CLI readable by agents: JSON help, MCP from the same tree.
Declare agents; don't start them. Let the daemon converge.
Spend on the substrate early. Nix made scaling cheap.
If you copy one thing, copy the clones and the single authority per piece of state.
try it
Start small
pi install git:github.com/harryaskham/agent-utilsspeech, voice and choices for Pi nix run github:harryaskham/tendril -- listlet an agent see your screen github.com/harryaskham/mcp-clia CLI that's also an MCP tool github.com/harryaskham/caravana merge queue with an agent in it
Everything else: atlas · nine-month film · how I use computers
Everything here is linked from the hub: the atlas, the films and the long read.
questions
Questions?
Scan for the atlas: every piece with what it does, how and why. Happy to go deeper on Cacophony, Caravan or the input bus.