| name | ainb-fleet:atc |
| description | ATC (Air Traffic Control) — the persistent orchestrating brain of the fleet.
A plain `ainb` Claude session running a generated CLAUDE.md policy, woken on
an OS-timer heartbeat (default 15 min) built from the LLM-free `fleet needs`
read — which is itself HOOKS-PRIMARY (event-sourced): ATC learns ASK/WAIT/IDLE
from the materialized `current_state` table, not by scraping panes, with a
tmux fallback for non-Claude agents. `atc setup` installs the global Claude
Code hooks into ~/.claude/settings.json. It auto-clears the safe/blocked
sessions via the fleet verbs (confident ASK → broadcast; ERR → continue within
a retry cap) and escalates the uncertain ones to the phone bridge. Supersedes
the `daemon` skill for managed fleets. Use to provision / inspect / tear down
an ATC instance.
|
| version | 0.1.0 |
| user-invocable | true |
| triggers | ["ainb-fleet:atc","fleet atc","air traffic control","fleet brain","conductor","watch the fleet","auto-clear and escalate"] |
| allowed-tools | ["Bash"] |
ainb fleet:atc — Air Traffic Control
ATC is the orchestrating brain that ties the fleet verbs together on a schedule
with a policy. It is a real ainb run Claude session reading a generated
CLAUDE.md, woken every N minutes by an OS timer. On each heartbeat it pulls
the LLM-free ainb fleet needs --format json read, auto-clears the safe
cases itself, and pages you (via the phone bridge) only for the calls that
genuinely need a human.
hooks ─▶ events.jsonl ─▶ notifyd ─▶ current_state (ASK/ERR/WAIT/IDLE/RUNNING/DONE)
│ (materialized per session)
OS timer ──[HEARTBEAT]──▶ ATC session ──reads──▶ ainb fleet needs --format json
│ (hooks-primary, tmux fallback)
├─ auto-clear ─▶ ainb fleet broadcast (answer / continue)
└─ escalate ───▶ phone bridge ─▶ your phone
How ATC learns a session is blocked — HOOKS-PRIMARY
The fleet needs read ATC runs each heartbeat is event-sourced, not pane-
scraped. Claude Code hooks append lifecycle events to events.jsonl; notifyd
ingests them into a SQLite current_state table that materializes the latest
stage per session (ASK / ERR / WAIT / IDLE / RUNNING / DONE). ATC reads that
materialized state as the PRIMARY source — so it learns ASK/WAIT/IDLE/ERR from
hooks, the instant they fire, without scraping panes. The tmux pane scan is
only the fallback for sessions the hooks don't cover: non-Claude agents
(Codex/Gemini fire no Claude hooks) and any session not yet materialized.
ainb fleet atc setup is what installs those global hooks into
~/.claude/settings.json (the --no-hooks flag skips it). The consumer skills
(needs, fleet-needs, standup) do NOT install hooks — they only read the
materialized state. So provisioning ATC is also what turns the whole fleet
hooks-primary; without it (or without atc setup's hook install), fleet needs
falls back to the live pane scan everywhere and still works.
Provision
ainb fleet atc setup tower
ainb fleet atc setup tower --interval 10 --idle-pause 30
ainb fleet atc setup tower --no-spawn --no-heartbeat
setup is idempotent: re-running re-renders CLAUDE.md + meta.json and
re-installs the timer, but never clobbers the accumulated state.json /
task-log.md. It writes to ~/.agents-in-a-box/atc/<name>/:
| file | role |
|---|
CLAUDE.md | the rendered policy ATC reads (verbs, playbooks, safety, memory) |
meta.json | {name, heartbeat_enabled, heartbeat_interval_min, idle_pause_min} |
state.json | ATC's durable machine state (retry counts, escalations) — survives compaction |
task-log.md | ATC's human-readable running log |
Inspect
ainb fleet atc status tower
ainb fleet atc list
ainb --format json fleet atc status tower
Tear down
ainb fleet atc teardown tower
ainb fleet atc teardown tower --purge
Teardown is idempotent and safe when nothing is installed — it removes the OS
timer cleanly, so there is no stale launchd/systemd unit left behind.
The heartbeat (internal)
ainb fleet atc heartbeat tower
It builds the [HEARTBEAT ...] nudge from ainb fleet needs --no-enrich --format json (cheap, 0-token) and tmux-sends it into the ATC session, so ATC
spends tokens only deciding, not scanning. When the fleet is quiet it sends
a one-line idle ping; when the session is gone it no-ops harmlessly until
teardown.
Event-driven mode (the plumbing)
setup also installs an event-driven lifecycle hook set into
~/.claude/settings.json (skip with --no-hooks). With it, a child session
spawned via ainb run --parent <atc-name> commits its completion to ATC's
durable inbox the instant its turn ends — so ATC learns about a finished
child without waiting for the next heartbeat. On each heartbeat ATC drains its
inbox first and prepends those completions to the nudge (a pending completion
even overrides idle-pause). The poll-mode fleet needs scan stays as the
always-on fallback, so ATC behaves identically whether or not children have the
hooks — the plumbing is a pure drop-in enhancement.
ainb run --repo . --parent tower -p "fix the failing tests"
ainb fleet atc inbox peek tower
ainb fleet atc inbox drain tower
Full design + on-disk formats: docs/atc-plumbing.md.
The policy (what ATC decides)
The generated CLAUDE.md encodes a conservative, escalate-on-uncertainty
policy. Per signal:
| signal | ATC action |
|---|
| ASK (blocked on a question) | answer via broadcast only if confident; ambiguous / opinionated / irreversible → ESCALATE |
| ERR (API error) | send continue via broadcast, capped at 3 auto-continues per session; cap reached + still erroring → ESCALATE |
| IDLE (turn finished, silent) | leave it (work likely done); only nudge if mid-orchestration |
WAIT (WAITING: marker) | unblock via broadcast if it is waiting on another session; waiting on you → ESCALATE |
ATC never auto-runs destructive actions (delete / force-push / merge /
deploy / spend / touch secrets) — those always escalate.
Escalation
ATC's voice to you is the phone bridge (plugins/ainb-fleet/bridge/). It raises
a call by emitting a self-contained line the bridge surfaces:
NEED: <session> — <decision required> — <options>
Supersedes ainb fleet daemon
For any fleet ATC manages, it replaces the standalone daemon skill: the
daemon's blind 5-second regex auto-continue becomes ATC's ERR playbook, now
with the retry cap the daemon never had. Do not run ainb fleet daemon
against sessions ATC manages — they would race. The daemon skill remains in
place for unmanaged one-off recovery, marked superseded.
Safety model + limitations
- Safety is prompt-enforced. ATC's conservatism lives in the
CLAUDE.md
policy, not in a hard sandbox. The default is deliberately to escalate on any
uncertainty; a wrong autonomous action is costlier than an extra page.
- Poll-mode latency (mitigated by the plumbing). In pure poll-mode ATC
reacts at its next heartbeat, not the instant a session blocks. The plumbing
(hooks → status files → durable inboxes → Stop-hook drain), installed by
setup and wired into the heartbeat, upgrades ATC to event-driven for any
child spawned with ainb run --parent: completions arrive the instant the
child finishes. It is a drop-in enhancement, not a prerequisite — ATC still
works standalone in poll-mode for any session without the hooks. See
docs/atc-plumbing.md.
- tmux fire-and-forget. Like the rest of the fleet, sends have no ACK; ATC
confirms effect on the next heartbeat read.
Caveats
- The heartbeat timer and the ATC session are independent: tearing down only the
session leaves the timer firing into a dead pane (it no-ops). Use
teardown
to remove both.
--no-heartbeat provisions without a timer — you can drive heartbeats
manually (ainb fleet atc heartbeat <name>) for testing.
- One ATC instance manages the whole host fleet; run multiple named instances
only if you deliberately want to partition (they will each see all sessions).