Agents

Cacophony manages the full lifecycle of coding agents: checkout, terminal host (Herdr or tmux), profile loading, hook execution, status tracking, and reintegration.

An agent is a directory under the daemon's agents root plus a live runtime. agent.json holds its state, assignment and provenance; checkout is its own Git worktree on its own branch; prompt.txt and system-prompt.txt are its composed profile; env.sh and init.sh launch it; logs and result keep its output; and sentinel files record that it started, is ready and is alive. The daemon runs init.sh in a Herdr workspace or tmux session, which starts the runtime. Heartbeats from the daemon arrive in that terminal, and the agent reports back through the CLI or MCP tools.
What an agent isA directory the daemon owns, a worktree on the agent's own branch, and a runtime in a Herdr workspace or tmux session.

Agent Identity

An agent is ${NODE}:${PROJECT}:${AGENT_ID}, here atlas:lighthouse:7f3a9c21d04e8b15; its work goes on the branch agent/${NODE}/${PROJECT}/${AGENT_ID}.

Tables and cards show the full ID, so workers with similar prefixes stay distinguishable; short labels appear only in cramped secondary controls.

Agent States

A spawn request is pending until capacity admits it, then starting while its checkout and terminal are set up, and running once its runtime reports ready. Around running are the other live states: waiting for input or a decision, stale when the session is alive but has made no tool calls within the inactivity timeout, dormant for on-demand runtimes with no live process, paused with its terminal or session retained, recovering while it resolves a reintegration conflict itself, retrying after a crash with backoff, and blocked when the runtime cannot make progress. Completed, failed, stopped and discarded are terminal; an intact failed, stopped, completed or discarded agent can reopen its conversation in the same worktree without reclaiming old work.
LifecycleLive states orbit running; terminal states sit apart, and failed or stopped agents can be resumed in the same worktree.
StateMeaning
pendingSpawn accepted and waiting for node or project capacity.
startingCheckout, prompt and terminal being prepared; waits for the alive and ready sentinels.
runningWorking. Counts as healthy capacity.
waitingWaiting for input or a decision; the agent reported it.
staleSession alive but no tool calls within the inactivity timeout; not counted as healthy until it shows activity again.
dormantOn-demand runtime with its checkout and session kept but no live process; a wake resumes it.
pausedPaused by an operator: no autonomous progress, terminal or provider session kept.
recoveringRuntime alive and resolving a reintegration conflict itself, so supervisors leave it alone.
retryingCrashed; a resume is scheduled with backoff and it still counts against concurrency.
blockedRuntime dead or unable to make progress.
completedFinished through caco agent complete or reintegration (terminal).
failedFailed fatally or ran out of retries (terminal; can be resumed).
stoppedStopped by caco agent stop (terminal; can be resumed).
discardedDiscarded: hidden from lists, files kept for forensics (terminal).

Checkout Strategy

Every agent works in its own checkout on its own branch, made by default from a canonical checkout the daemon keeps per project.

The daemon keeps one canonical checkout per project on each node, on the default branch, fetched from the project remote and recloned if the remote, branch or topology changes. By default each agent's checkout is a single-branch shared clone of it, on the agent's own branch agent/<node>/<project>/<id>: it borrows the canonical checkout's Git objects, so a new agent costs a working tree, not a second copy of history; checkout_type full instead clones the project remote with its own objects. A materialized friend project is a clone of that project's own remote under friend-checkouts/, with its own objects; with checkout_write the agent works on its own friend branch there. Work leaves only by reintegration, as a squash-merge or a pull request.
Agent checkoutsDefault clones borrow the canonical checkout's objects, while full and friend clones keep their own; each agent's directory is $CACOPHONY_DIR/agents/<project>/<id>/.

The canonical checkout records a secret-redacted cache identity: if project config changes the remote, branch, sparse spec, extra remotes or reintegration topology, the daemon reclones it before making new agents from it. How an agent's checkout is made is checkout_type; a persistent agent's declaration wins over projects[].agent_defaults, which wins over the profile.

checkout_typeMade byUse it for
clonesingle-branch clone of the canonical checkout, objects sharedthe default
worktreea Git worktree on the canonical checkout's object storethe least extra disk
read_only_symlinka symlink to the canonical checkoutobservers that never write
fulla clone of the project's configured remote with its own object store, using the project's SSH identity; needs no canonical checkoutstandard GitHub workflows, and agents that must not depend on the canonical checkout
pr_brancha full clone tracking an existing pull request's head branchworking on someone's PR; needs GitHub integration

Every clone the daemon makes, canonical, full or friend, takes its shape from config: the project's remote, SSH key, identity, git.config entries and extra remotes. caco git clone --project <name> --into <dir> makes the same clone by hand, with optional --branch, --filter, --depth and --sparse, and --check proves the identity, ls-remote and a dry-run push. The full table, the callers and the no-ad-hoc-clone rule are in the repo-shaping contract.

Friend projects with materialize: true become non-blocking clones of the friend project's own remote under friend-checkouts/<project>/, with their own objects, each with its own pending, ready or failed state. Writable friends require checkout_write, work on an isolated friend/<agent-id>/<project> branch, and land only through reintegrate --friend or complete --friend.

Terminal Hosts

Shell agents (Pi, Pi-Paratenic, Claude, Codex and other commands) run in the terminal host set by agents.runtime.multiplexer. Herdr is the default; tmux is available globally or per project.

agents:
  runtime:
    multiplexer: herdr   # herdr | tmux
    herdr:
      session: caco      # shared Herdr server name

projects:
  - name: example
    agent_runtime:
      multiplexer: tmux  # optional per-project override
HostEach agent getsStopping it
Herdran owned, unfocused workspace on a shared servercloses that workspace, never the server
tmuxa session named caco-${NODE}-${PROJECT}-${AGENT_ID}cleans up the session and pane metadata
noneno live process: dormant pi-on-demand and pico-on-demand agents, logical pi-daemon sessionskeeps the identity; Pico attaches over structured RPC

A missing or incompatible Herdr is reported, never silently replaced by tmux. Existing agents keep the host they were created with, so changing the default never moves a live session, and caco agent attach --raw finds the right host on the owning node. caco agent register can borrow an existing Herdr pane; stopping a borrowed registration leaves the pane, its processes and the checkout alone.

CLI Usage

Spawn an Agent

caco agent new \
  --type codex \
  --profile worker \
  --goal "Implement the next ready bead"
caco agent new goes to the local daemon, which forwards it over mutual TLS when another node was requested. The owning daemon admits the request against node and project concurrency ceilings, reserves the agent ID and directory, creates the agent's own checkout, writes its composed prompt, env.sh and init.sh, and opens a workspace in Herdr or a tmux session that runs init.sh. init.sh writes an alive sentinel straight away and a ready sentinel after bootstrap; only then is the agent running. caco agent attach later resolves the agent's terminal host, locally or over SSH to the owning node.
Spawn and attachThe agent is only running once init.sh has written both sentinels; attach finds its terminal locally or over SSH.

To try a profile draft that is not yet under .cacophony/profiles/, pass --profile-path profiles/draft.md instead of --profile; the daemon loads that file through the normal profile bridge.

Where it runs

Each candidate node is scored, and ineligible nodes carry their reasons. An operator's choice comes first, then the highest-scored eligible node. The agent's status records why, and the daemon snapshot marks each node selected, ready or blocked for dashboards.

placement_reasonWhen
operator_preferred--node or a preferred node named the target
only_eligible_nodeone node passed the eligibility checks
highest_scoreseveral were eligible; the best score won
fleet_fallbackno scheduler decision was available

On what compute

Transient per-agent compute is opt-in per spawn: --compute local is the ordinary path, --compute default resolves agents.transient_compute.default, and any other value names a configured provider, such as the deterministic fake provider used in tests. A transient agent keeps the same ID and lifecycle; the daemon records its launch-spec hash, runtime and retry ceilings, admission and budget checks, and where its artefacts and logs will land.

caco agent new --type codex --profile worker \
  --goal "Implement the next ready bead" --compute fake

Now or in the background

With async_spawn: true or --async, caco agent new returns the agent ID straight away in starting while the checkout, bootstrap and launch continue; follow it with caco agent status or attach. --sync waits until it is ready. TUI spawns and Pi or debug-shell starts are asynchronous by default, so a slow provider or MCP server never times out the command.

List Agents

caco agent list

Attach to Agent Terminal

AGENT_ID="agent-id"
caco agent attach "$AGENT_ID"
caco agent attach "$AGENT_ID" --raw

By default caco agent attach opens the agent's own pane: status on top, its terminal below. --raw attaches straight to the underlying terminal, over SSH when the agent is on another node. A remote attach or status fails fast only when the peer is unreachable or its API probe is degraded; a config mismatch shows as cluster drift but does not block the request, so busy remote workers stay inspectable while config converges.

View Agent Status

AGENT_ID="agent-id"
caco agent status --id "$AGENT_ID"

Status reads reconcile with the agent's persisted agent.json first, so a recent liveness ping or terminal transition on disk clears a stale stalled or failed view; list and summary views do the same.

Pane diagnostics never change anything by themselves. The daemon classifies each agent pane as healthy, frozen, rate-limited, stuck on a prompt, in a merge conflict, or with an exited or zombie child; an idle controller or observer with no recognizable blocker is watch-only, not restarted.

An optional remediation policy (off by default) picks the least invasive next step (observe, notify, nudge, dismiss a prompt, pause/resume, or ask the operator about a restart) based on attempts, cooldowns, role and explicit allow flags. Only non-destructive steps run directly; anything destructive becomes an operator choice. Attempts are kept for 30 days so cooldowns and post-incident review can see the history.

View Agent Logs

AGENT_ID="agent-id"
caco agent logs "$AGENT_ID"

Import transient-agent artifacts

Short-lived dynamic-compute workers can disappear before an operator inspects them. Use MANIFEST="/path/to/transient-result/manifest.json"; caco agent import-artifacts --manifest "$MANIFEST" to materialize an uploaded result-bundle manifest back into the normal local runtime layout so caco agent status, caco agent logs, caco agent replay, and caco agent diff remain the canonical read surfaces. Automatic dirty-work checkpointing before involuntary terminal transitions refuses large deletion-only staged changes (20+ deletions and no additions/modifications), resets the staging area, and leaves the checkout dirty for explicit first-party recovery instead of committing a likely broken whole-tree deletion checkpoint.

For microVM-backed workers, the same manifest may include crash/timeout checkpoint breadcrumbs. The importer preserves logs/microvm-console.log, logs/microvm-supervisor.log, microvm/checkpoint.json, optional checkpoint patches, git-bundle fallback, checkout tarballs, backend name, checkpoint kind, and checkpoint reason under the normal agent directory.

MANIFEST="/path/to/transient-result/manifest.json"
caco agent import-artifacts --manifest "$MANIFEST"
caco agent status --id "$AGENT_ID"   # shows backend/checkpoint breadcrumbs
caco agent logs --id "$AGENT_ID"     # includes microVM console/supervisor logs

Set Agent Status

caco agent status set "running tests"

Stop an Agent

AGENT_ID="agent-id"
caco agent stop "$AGENT_ID"

A stop that outlives the response budget returns HTTP 202 with stopped:false and completed:false; the CLI reports it as accepted, not completed. The original stop keeps the runtime-control lock until it records success or refusal, and a concurrent runtime operation receives retryable agent_stop_in_progress. Check caco agent status before acting on the runtime.

Resume an Intact Conversation

AGENT_ID="agent-id"
caco agent resume --id "$AGENT_ID"

Explicit resume also reopens completed, stopped and discarded-but-intact agents in the same checkout and conversation. It refreshes the managed environment/profile and injects one short [caco-resume] notice directing the agent to await the operator's next instruction. It does not require, reclaim, reopen or republish a bead. If the host session is absent, it reloads retained state or recreates the same logical identity and says so in the notice; a pruned or deleted checkout is refused, not silently replaced. In the TUI, a paused agent still shows Unpause (U), while bulk recovery does not reopen all delivered conversations.

caco agent recreate keeps a stopped agent stopped: when the source is Stopped (or a persistent declaration is operator-stopped), the replacement gets a fresh checkout, environment and record but no runtime launch, and the operator-stop intent is kept. Start or resume launches it later. Recreating a running agent still relaunches it immediately.

A completed AHP agent's local health is detached, not evidence that the host deleted its conversation. Older socket-era session_missing completion records are corrected on the read surface without claiming a fresh host probe. See conversation resume and retained-session health.

Fork a Live Agent Pane

AGENT_ID="agent-id"
caco agent fork --id "$AGENT_ID" --type pi

Fork launches a sibling runtime pane in the same per-agent tmux session without stopping the parent process. The fork inherits the same Cacophony agent ID, checkout, branch, and token scope, rewrites the runtime invocation to use --fork rather than --continue, and can select a supported backend with --type pi|claude|codex.

Resume Nudge

Agents record lineage: which agent an agent was retried, replaced, recreated, handed off, forked or resumed from, with reason, source and time. Fork and resume keep the same identity; the others name a successor. caco agent status --id <agent> shows the lineage chain, and its JSON carries the full records with filter facets. Reading lineage never rewrites it.

Decision points are bounded, redacted snapshots of an agent at a commit or an explicit capture: bead, branch, head and a short diff and session excerpt. caco decision-point rewind --agent-id <id> --decision-point-id <dp> --dry-run previews restarting work from one of them (checking bead ownership and that the ref still exists) without spawning anything. Handoff snapshots carry the same metadata for passing work to a successor agent; caco agent wip-handoff --dry-run describes the plan, and nothing is captured, patched or spawned by these previews.

Calls about a remote agent go straight to its owning node when that node is reachable, and fall back to the fleet collector otherwise; failures come back as structured errors with an HTTP status, a retryable flag, retry-after seconds and guidance. A single-agent status read never turns into a fleet-wide sweep. While an agent is starting or resuming, stop and discard stay safe, and a racing spawn, recreate or resume is refused or queued by policy.

Automatic work recovery and restart retain their existing recovery nudges. Explicit completed/stopped/discarded conversation resume sends the short [caco-resume] notice instead, without an old-goal nudge. For automatic recovery, phrasing depends on the stop reason:

Controller-scoped agents (those monitoring the fleet rather than working on beads) receive controller-appropriate phrasing directing them to rehydrate fleet state. Worker agents are told to continue an assigned bead only when the canonical agent record actually carries a bead assignment; if no assigned bead is recorded, the nudge says so, treats the missing assignment as potentially incomplete, directs the worker to audit the live board for its own in-progress assignment before idling or auto-claiming, then inspect its managed checkout and follow its profile idle/work-selection rules instead of inventing phantom bead work.

Merge Conflict Recovery

If direct reintegration hits a merge conflict or stale base, prefer AGENT_ID="agent-id"; caco agent rebase --id "$AGENT_ID" as the first-party recovery shortcut.

Spawn Request

{
  "node": "localhost",
  "type": "codex",
  "profile": "worker",
  "goal": "Implement ready work",
  "id": null,
  "env": {},
  "hooks": {
    "on_start": [],
    "before_tool": [],
    "after_tool": [],
    "on_complete": []
  },
  "reintegration": {
    "mode": "direct"
  }
}

If node specifies a remote node, the local daemon forwards the spawn request to that node's daemon via the cluster listener.

Agent Wrapper

caco agent run wraps the actual agent binary and handles:

  1. Materialize checkout from canonical clone
  2. Create tmux session
  3. Load profile and install hooks
  4. Launch underlying agent type
  5. Inject resolved provider environment variables
  6. Capture stdout/stderr
  7. Record status changes
  8. Write completion result

Startup and Activity Timeouts

A startup/readiness timeout does not automatically fail an agent while its tmux pane is still alive and attachable. The daemon preserves the pane in starting, surfaces an explicit warning, and lets operators attach or wait for slow bootstrap work such as first-run environment realization to finish. Immediately after creating a managed tmux session, the launcher also uses a bounded attachability settle loop instead of a sub-second fixed probe, so transient tmux transport failures under load preserve the final probe detail in diagnostics before the launch is classified as failed. A healthy running state is reported only after the configured runtime has actually launched; an accidental shell-only prompt is not treated as a completed worker launch.

Pi-family readiness also inspects the bounded post-launch stabilization window for deterministic provider rejection before reporting running. Invalid or missing credentials fail as provider_credentials_invalid; disabled or unavailable model access fails as provider_model_unavailable. Diagnostics name provider, model, and node but redact credential material, while transient network and provider 5xx failures remain retryable. Project provider/model defaults remain preferred unless a caller intentionally supplies an override.

Early bootstrap diagnostics are persisted under the agent directory at logs/bootstrap.log. The generated init.sh records named steps such as sourcing env.sh, applying shell/bootstrap snippets, configuring git excludes, writing alive/readiness sentinels, verifying the managed caco command, and execing the runtime. If startup exits before readiness, the log includes the failing step, exit code, PWD, and PATH, so operators can distinguish env quoting, checkout bootstrap, sentinel, and runtime exec failures even when the tmux pane dies before it can be inspected.

No-tool-activity timeouts are also advisory when tmux/runtime liveness is still positive. After the configured threshold and grace, the daemon records a warning while preserving running; it does not fail, restart, recreate, discard, roll back, or reassign that agent without positive death evidence or operator approval.

Transient Blocker Triage

Managed agents should treat local lock files, proxy read failures, stale cached node-health summaries, and restart-window timeouts as provisional signals until a bounded direct check confirms them. Before declaring a target node down, idling on a claim blocker, or filing broad outage work, cross-check the authoritative surface when possible. Useful checks include caco @node bd status --project <project> --json, caco @node agent list, and the target node's caco status --json.

If the direct authoritative check is green, agents should continue implementation or reintegration and record the local/proxy drift as diagnostics. If the direct check confirms a real blocker, file or update a specific bead with the failing command, node, bead ID, and observed authority state instead of waiting silently or broadcasting a vague outage.

When startup reconciliation does have positive failure evidence, structured log_error events and summaries include failure_cause labels so operators can tell the path apart from advisory timeout noise. Common labels include confirmed_tmux_session_dead, provider_runtime_exited, runtime_launch_failed, tmux_socket_dead, and timeout_or_starting_grace.

Credential Propagation

The wrapper passes these environment variables to spawned agents:

VariableSource
OPENAI_BASE_URLConfig or environment
OPENAI_API_KEYResolved from secret file
ANTHROPIC_BASE_URLConfig or environment
ANTHROPIC_AUTH_TOKENResolved from secret file
CACO_NODECurrent node name
CACOPHONY_DIRRuntime directory
PULSE_SERVERAudio routing (if configured)

Completion

On completion, the wrapper writes result files:

PROJECT="project-name"
AGENT_ID="agent-id"
$CACOPHONY_DIR/agents/${PROJECT}/${AGENT_ID}/
  result/
    status.json         # Final state, profile, reintegration result, git info
    stdout.log          # Captured stdout
    stderr.log          # Captured stderr

Reintegration Policies

Reintegration is configurable per profile and per spawn request:

ModeBehavior
direct (default)Push branch, squash merge into remote default branch, record success
pr_auto_mergeOpen PR, merge when allowed
pr_reviewOpen PR, stop after reporting target
artifact_onlyRecord branch/commits/patch without merging
noneNo reintegration (for read-only agents such as controllers and analyzers)

Profiles still default to direct when no policy is set, but a project can steer successful reintegration with projects[].integration. default_intent: review maps the default success path to pr_review, while backend: pull_request uses the configured fork/upstream remotes and gh to create the review surface instead of doing a local squash merge. During a short-term PR rollout fallback such as operators can keep the fork/upstream topology declared but set default_intent: direct and backend: local_merge so routine completed work lands on main while PR auto-merge / agent-merge is verified separately.

For persistent agents, PR-backed mid-flight reintegration is intentionally non-destructive: caco agent reintegrate --mode pr_review / pr_auto_merge keeps the runtime alive until the PR actually lands, emits an explicit warning/event that the agent was not recreated, and expects an operator or controller to run AGENT_ID="agent-id"; caco agent recreate "$AGENT_ID" afterward when a fresh runtime is desired. Profile-source changes also affect future materialization before they affect already-running persistent prompts; use caco agent refresh --id "$AGENT_ID" to rewrite managed env.sh, Pi/runtime config, and friend-checkout env/backfill, and reserve recreate/restart coordination for cases that need a fresh runtime.

Project policy is resolved as intent plus backend: default_intent chooses direct, review, artifact-only, or none; backend chooses local safe merge or PR-backed delivery for direct intent. The topology remains in remote, remotes, integration.reintegrate_target, and integration.pr_base.

Direct Mode Rules

PR-Backed Rules

Agent Compatibility

Profiles use a canonical Claude-style format (Markdown + YAML frontmatter). Agent types that don't natively consume this format are supported through bridges:

TierAgentsProfile Support
Tier 1Claude-compatibleConsume profile format directly
Tier 2Codex, PiBridge layer adapts canonical profile

Runtime types accepted by caco agent new --type:

TypeRuns as
claude, codexThe vendor CLI in a Herdr workspace or tmux session.
piAn attachable Pi TUI in a terminal host.
pi-paratenicA Pi TUI whose prompts and nudges go through AHP first, with native fallback.
picoPi driven over JSON-RPC by Picophony; surfaces render a structured session.
pi-on-demand, pico-on-demandDormant between approved wakes, keeping workspace and session identity with no live process.
pi-daemonA logical session on the shared Pi daemon host; messages and heartbeats run as turns with no terminal.

Persistent Agent Restart Policy

Persistent agents (those declared in agents.persistent) support a granular restart policy that controls daemon auto-restart behavior:

PolicyBehavior
failure (default)Restart only on failure/crash, respecting backoff. Does not restart after operator stop or pause.
alwaysRestart on any non-running state (failure, stop, pause, daemon restart).
neverNever auto-restart.
agents:
  persistent:
    repo-observer:
      profile: repo-health
      restart: failure
      restart_backoff_secs: 300  # backoff before relaunch after an exit

restart_backoff_secs is a relaunch backoff, not the normal work cadence for a healthy persistent agent. Long-running Pi-backed specialists such as technical-writer keep running and use their composed runtime wake paths (for example pi-loop) for review cadence instead of completing every cycle just to be restarted.

Operator-facing status resolution treats persistent declaration IDs as first-class aliases for the live deterministic agent. If a declaration row such as persistent-69cd15d1 is stale but a matching persistent agent is already running for the same declaration name, node, and project, caco agent status --id persistent-69cd15d1 and aggregate persistent inventory should resolve to the live runtime rather than reporting agent_not_found or a stopped sentinel row. Agent summaries cross-check current in-progress bead claims, but closed or deleted bead IDs left in agent.json or historical assignee fields are watch-only stale_closed_bead_assignment residue rather than actionable stuck-worker evidence.

Pi Agent Runtime

Pi is a managed agent runtime supported as a Tier 2 bridge. For the Cacophony project itself, managed Pi agents compose a default stack of profile mixins:

MixinPurpose
pi-home-overrideIntentionally use the reviewed operator Pi home without replacing its settings or sessions
pi-heartbeatRepo-owned non-model receiver for daemon [caco-heartbeat] nudges; only a trusted task-specific CACO_PI_HEARTBEAT_PROMPT opts into an action, after any configured daemon timer conditions
pi-loopRepo-owned /loop <interval> <prompt> command for repeating prompts inside Pi
pi-caco-commandsRepo-owned /caco-* slash commands (help, agent-status, bd-list/show, msg-inbox, choices-current)
tendril-mcpMCP bridge to the Tendril checkout for tool access

Compose these as an ordered profile: list. Daemon-native direct delivery owns messages and configured heartbeat owns routine wakeups; use pi-loop only for a distinct recurring task. Speech/tool-call enforcement, the checkout wrapper, self-ops, independent inbox polling, automatic task-resume nudges, self-compaction, the image and profile-drift guards, and the sudo runner are retired. Their old profile names remain inert for external declaration compatibility, and refresh removes known generated entrypoints without changing the operator Pi home. Explicit /inbox inspection remains available. See profile lifecycle and runtime stacks for adoption and refresh guidance.

For non-Pi CLI sessions that need repeating narration, use PROJECT="project-name"; caco loop --project "$PROJECT" --interval 10m --body "..." instead of ad-hoc shell loops.

Package Pre-Installation

During Pi workspace materialization, the daemon reuses the host’s warmed ~/.pi/agent/git/ package cache (including node_modules) by linking it into the workspace when possible, then pre-clones any remaining git packages declared in the Pi settings.json packages array (e.g. git:github.com/owner/repo@tag). This prevents cold-start timeouts where Pi’s first-run package clone exceeds the daemon’s provider-ready or stall-detection window. Older workspaces that still contain copied .pi-agent/git caches can be relinked through caco prune run --pi-package-cache --current-agent or an explicitly selected prune run. Errors during pre-install are logged but non-fatal — Pi can still install packages on its own.

Spawn Concurrency

Nodes and projects can each declare concurrency ceilings for agent spawns:

nodes:
  - name: local-node
    concurrency:
      max_agents: 4

projects:
  - name: my-project
    concurrency:
      max_agents: 2

When a ceiling is omitted, that scope is unlimited. The daemon-local queue holds excess spawn requests until capacity frees up. Queued work is never re-routed between nodes.

Pre-Merge Validation

Profiles can define before_reintegration hooks that run after before_complete succeeds but before any git merge or PR operations. These are intended for pre-merge smoke tests such as config validation, template linting, and daemon startup checks. If any hook fails, reintegration is blocked and the agent is notified.

hooks:
  before_reintegration:
    - type: command
      command: caco config validate --strict
      timeout: 30

Project-Level Hooks

Projects can declare lifecycle hooks that fire on project events. These are configured under projects[].hooks:

EventWhen It FiresVariables
checkout_updatedCanonical checkout moves to a new commit (e.g. after reintegration merge, git pull)$commit_sha (message), $CACO_HOOK_COMMIT_SHA (shell env)
tag_pushedNew git tag detected (via polling, CI webhook, or explicit nudge)$tag (message), $CACO_HOOK_TAG (shell env)

Each event maps to a list of actions:

ActionDescription
notify_agentLifecycle-only nudge to a named persistent agent via runtime/tmux; suppresses chat/inbox fallback history
shellRun a shell command; hook variables injected as CACO_HOOK_-prefixed env vars
broadcastSend a broadcast message to the project
speakTTS announcement with optional voice override
projects:
  - name: cacophony
    hooks:
      tag_pushed:
        - action: notify_agent
          agent: update-helper
          message: "New tag $tag pushed"
      checkout_updated:
        - action: speak
          body: "Checkout updated to $commit_sha"