stack · for colleagues · 2026

Cacophony

Project-first swarm management for coding agents, wherever they run.

Harry Askham · a.skh.am/stack/deck

DRAWINGCACO / GA-01
SHEET1 OF 26
DRAWNH. ASKHAM + AGENTS
the problem

Many agents, one repo, one person

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:

Composablesmall tools, each testable alone
Unix philosophystdio, JSON, pipeable, fast
Remote-agnosticsame workflow on any machine

Lineage: djent (parallel agents + beads) → io-mcp (voice, ring, self-management) → caco

architecture

Five layers

operator surfacesCLI · TUI · web · Android · Wear · iOS · macOS · MCP plugins
local node servicescaco-daemon · caco-bd-daemon · caco-web · sidecars · TTS/STT
cluster coordinationmTLS daemon mesh · PKI · feed replication · peer health
project statecanonical checkouts · agent clones · beads branch · summaries
execution backendstmux / 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 cli

caco does a lot

agentsagent · pi · claude · codex · pico · pi-daemon · heartbeat
workbd · decision · choices · suggest · ops · bisect · blame · changelog
deliverypr · cara · github · release · test · build · checkout
talkmsg · chat · notify · loop · audio · tts · stt
flowsflow compile · simulate · graph · run · input · watch
fleetnode · fleet · cert · token · secret · relay · sidecar · service · cron · mode
memoryscratch · file · link · feed · event · log
surfacestui · 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.

rule one

Local clients talk to the local daemon

caco CLI · TUI · web · phoneloopback · bearer tokenlocal daemonmutual TLSpeer daemons
beads

A task list agents can share

an agent's life

Claim → clone → land

bd createclaimown clonebuild · testreintegratedirect · PRmain
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.

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.

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.

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 them
  • caco flow input answers a node that suspended for a human
  • 61 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
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
dormancy

Asleep until needed

elicitation

Agents ask, anything answers

Culture on Apple Watch composing a replyTool approval dialog in Culture on AndroidPetri phone controls
caravan · cara

Chains, not a pile

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"] } } }
healthywaiting_ciheldretry_tickoperator_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.

runtimes

Agents don't need to be subprocesses

MAR · subprocessa CLI process per agent in a terminal; messages arrive as keystrokes
JUL · daemonpi-daemon: many sessions in one process, asleep between wakes
SEP · protocolAHP: an agent is a session any client attaches to
paratenic + culture

One live session, every client

Culture web: an open conversation with an agent waiting for inputCulture web: a second agent pinned beside itCulture web: the command palette across both hosts
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
surfaces

The same agents, everywhere

Cacophony TUICulture web: two agents side by sideCulture on macOS with tools expandedCulture on iPhone, inboxCulture on Apple Watch

TUI, web, Android, Wear, iPhone, iPad, Apple Watch and native macOS. Fresh Cacophony macOS captures to come.

the tui · graphics engine

The terminal is the canvas

  • 1,913 kitty image uploads in 20 s with ultra-v2 animation on
  • 6.6 MB of images to the terminal; median frame 2.8 ms
  • 238 fps of render headroom at a paced 30
  • layout → 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.

the tui · materials

One renderer, many materials

ultra-v2glass-v2chrome-v2neon-v2nord-v2crt-v2eink-v2ultra

ultra-v2 · glass · chrome · neon · nord · crt · e-ink · the original ultra: the same screen, one renderer, presets in config.

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
why it scaled

The ground was already Nix

live from the atlas

The whole stack

One voice note, top to bottom: ring → caco → beads → an agent → earbuds.

numbers · 2026

What it produced

21k+commits to Cacophony since 16 March
3,511pull requests into Cacophony
17,659beads on its board
5,187PRs across my own repos
864PRs on work repos, same setup
one tower per day · drag to turn
lessons

What I'd tell you to copy

  1. Give every agent its own clone and branch.
  2. Keep one authority per piece of state; everything else proxies.
  3. Make every CLI readable by agents: JSON help, MCP from the same tree.
  4. Declare agents; don't start them. Let the daemon converge.
  5. Spend on the substrate early. Nix made scaling cheap.
try it

Start small

Everything else: atlas · nine-month film · how I use computers

questions

Questions?

QR code for a.skh.am/stack/atlas

a.skh.am/stack/atlas

0:00

next: