ATC (Air Traffic Control) — the single operator-visible fleet supervisor, in exactly one mode: `lite` (no LLM: deterministic scan, known-transient retry policy, safety caps, evidence) or `full` (the persistent orchestrating brain). Full mode is a plain `ainb` session (Claude or Codex) running a generated policy, woken on a 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 `curre...
Scanned 9/3/2026
Install to Claude Code
npx -y skills add stevengonsalvez/claudecode-bootstrap --skill atc --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Atc?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/stevengonsalvez-atc)More formats (shields.io, HTML) on the badges page.
---
name: ainb-fleet:atc
description: |
ATC (Air Traffic Control) — the single operator-visible fleet supervisor, in
exactly one mode: `lite` (no LLM: deterministic scan, known-transient retry
policy, safety caps, evidence) or `full` (the persistent orchestrating brain).
Full mode is a plain `ainb` session (Claude or Codex) running a generated
policy, woken on a 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, or to switch which controller owns the fleet.
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
- lite mode
- full mode
- supervisor mode
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.
## One supervisor, one mode
ATC is the single operator-visible supervisor for a fleet, and it is in exactly
one mode at a time. The mode is persisted in `meta.json`, and **both**
controllers re-read it from disk before every send — so the losing one stands
down by itself, rather than relying on having been stopped in time.
```
┌──────────────────────────────────────────────┐
meta.json ─▶│ mode ──▶ owner ──▶ the ONLY sender │
└──────────────────────────────────────────────┘
lite ─▶ lite scanner no LLM · known transient ERR only · caps · reports
full ─▶ full heartbeat scheduled beat ─▶ LLM session ─▶ triage + coordinate
```
| | lite | full |
|---|---|---|
| LLM | none | one brain session, woken every N minutes |
| ERR (known transient) | auto-`continue` inside the retry cap | same, decided by the brain |
| ASK / WAIT / IDLE | **reported, never answered** | triaged, answered, escalated |
| fleet coordination | none | yes |
| token spend | zero | every beat |
| provider | irrelevant (no brain) | must be one ainb can drive |
Both modes spend the **same** `continue_counts` ledger in
`heartbeat-state.json`, so switching never hands a permanently-broken session a
fresh set of retries.
```bash
ainb fleet atc mode tower # report the mode, provider, owner
ainb fleet atc mode tower --set lite # switch (explicit, persistent)
ainb fleet atc mode tower --set full --provider codex
```
The switch persists the mode BEFORE it stops anything, then dismantles the
outgoing controller and starts the incoming one. On the Daemons screen (`d`),
the ATC row offers the same switch and prints this help inline.
### Full-mode providers
`claude` (default) and `codex` today. The list is read off each provider's
declared capability, not hard-coded: a provider ainb cannot both host a resident
session for AND inject a heartbeat into is **refused**, never provisioned as a
brain that would silently never wake. Codex gets its policy in `AGENTS.md`,
which is the file it actually reads.
Copilot and Antigravity are not offered yet — they implement no ATC control, so
`--provider copilot` fails with the list of what does work rather than
pretending.
### This replaces `ainb fleet daemon`
Lite mode IS the standalone daemon's loop, moved inside ATC's safety rules: the
same scan, but capped, ledgered, and unable to run beside the heartbeat.
`ainb fleet daemon` still exists and still refuses to start against a fleet ATC
owns in either mode.
## Provision
```bash
# Default: 15-min heartbeat, 60-min idle-pause, spawns the session + installs
# the OS timer (launchd on macOS, systemd --user timer on Linux).
ainb fleet atc setup tower
# Tune the cadence.
ainb fleet atc setup tower --interval 10 --idle-pause 30
# Provision files only (no OS timer, no session) — useful for inspection / CI.
ainb fleet atc setup tower --no-spawn --no-heartbeat
# Provision straight into lite mode (no LLM, no brain session, no timer).
ainb fleet atc setup tower --mode lite
# Full mode on Codex instead of Claude.
ainb fleet atc setup tower --provider codex
```
`setup` CARRIES an existing instance's mode and provider forward. It is what
`ainb daemon atc start` re-runs to respawn a dead session, so rebuilding them
from defaults would flip a deliberately-lite fleet back to a paying brain.
`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, mode, provider, heartbeat_enabled, heartbeat_interval_min, idle_pause_min}` |
| `AGENTS.md` | the same policy under the name a Codex brain reads (codex provider only) |
| `state.json` | ATC's durable machine state (retry counts, escalations) — survives compaction |
| `task-log.md` | ATC's human-readable running log |
## Inspect
```bash
ainb fleet atc status tower # one instance: meta + timer + session liveness
ainb fleet atc list # all provisioned instances
ainb --format json fleet atc status tower # JSON for tooling
```
## Repair a broken heartbeat
When `status` reports `program MISSING (...)` or `list` shows `BROKEN`, the unit
names a binary that no longer resolves on the PATH the timer carries (a
cargo-to-homebrew move, a changed PATH). Rebuild just the scheduler:
```bash
ainb fleet atc repair tower # rewrite the unit against the current PATH
ainb fleet atc repair tower --dry-run # report what it would do, write nothing
AINB_BIN=/opt/homebrew/bin/ainb ainb fleet atc repair tower # pin an explicit path
```
`repair` READS `meta.json` and never writes it, so a customised
`--interval` / `--idle-pause` and a disabled heartbeat all survive. That is why
it exists instead of "just re-run setup": `setup` rebuilds `meta.json` from
defaults, rewrites `CLAUDE.md` and the `settings.json` hooks, and spawns a
session.
It refuses (non-zero exit, nothing written) when the rebuilt unit still could
not fire, so it never overwrites a working unit with a dead one. If the daemon
takes the heartbeat it removes the local timer, and if it does not it clears any
stale daemon registration, so exactly one scheduler ever fires.
`--dry-run` writes nothing and is safe as a health gate, with one property
stated precisely: it is **never greener than a real run**, rather than
bit-identical to one. Whether a real run hands the heartbeat to the daemon
depends on registration SUCCEEDING, which a read-only preview cannot determine
(registration also fails on generation conflicts, proto skew, and store
errors). So dry-run does not guess: it reports `daemon_registered` /
`daemon_unregistered` as `null` and previews the local-timer path, which is the
conservative branch and the one that can refuse. A green `--dry-run` therefore
implies a real run would not hit the refusal gate; a red one may still be
repairable if the daemon would have taken it.
The JSON surface carries `activation_skipped`, so a run under
`AINB_TIMER_SKIP_ACTIVATION=1` (which writes the unit but never loads it) cannot
be mistaken for a live heartbeat by an agent or CI job reading the payload.
## Tear down
```bash
ainb fleet atc teardown tower # remove the timer + kill the session (keeps state/log)
ainb fleet atc teardown tower --purge # also delete the instance dir
```
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)
```bash
# This is what the OS timer runs every N minutes — you do not call it by hand.
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.
```bash
# Spawn a child that reports completions back to ATC instance "tower":
ainb run --repo . --worktree --parent tower -p "fix the failing tests"
# Inspect / drain a parent's inbox directly (debug):
ainb fleet atc inbox peek tower
ainb fleet atc inbox drain tower
```
Full design + on-disk formats: [`docs/atc-plumbing.md`](../../../../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`](../../../../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).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!