AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
tmux에서 다중 AI 에이전트 세션을 관리하는 터미널 대시보드
A terminal dashboard for managing multiple AI agent sessions in tmux
개발 도구TypeScript · PythonGitHub @nicknisi직접 설치운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A terminal dashboard for managing multiple AI agent sessions in tmux.
Fleet watches your Claude Code sessions, shows which agents need attention, and lets you send prompts — all from a single pane. It replaces a sprawl of bash scripts with a compiled Bun binary and a Claude Code plugin that hooks into the event system automatically. Beyond hooked agents (Claude Code, Codex, pi), it also surfaces ones it has no integration for — aider, opencode, and the like — by spotting them in the process table.README 전문 보기 ↗A terminal dashboard for managing multiple AI agent sessions in tmux.
Fleet watches your Claude Code sessions, shows which agents need attention, and lets you send prompts — all from a single pane. It replaces a sprawl of bash scripts with a compiled Bun binary and a Claude Code plugin that hooks into the event system automatically. Beyond hooked agents (Claude Code, Codex, pi), it also surfaces ones it has no integration for — aider, opencode, and the like — by spotting them in the process table.
brew install nicknisi/formulae/fleet
git clone https://github.com/nicknisi/fleet.git
cd fleet
bun install
bun run build
# Binary is at dist/fleet — add to your PATH
Fleet hooks into Claude Code's event system to track agent state in real time. Install the plugin after building:
fleet install
This does three things:
run-shell line to your tmux.conf (marked # fleet-managed)No settings.json editing required. Install also offers two optional tmux keybindings — a sidebar split and a popup (see Sidebar & popup).
To remove everything cleanly:
fleet uninstall
Fleet also tracks Codex sessions. Codex has no plugin marketplace, so fleet install codex wires fleet into Codex's own config instead:
fleet install codex
This:
~/.cache/codex-status)PreToolUse + Stop hooks to ~/.codex/hooks.json (your own Codex hooks are preserved)[features] hooks = true in ~/.codex/config.tomlcodex in ~/.config/fleet/agents.jsonRe-run it after a brew upgrade to re-point fleet's hook path. Codex panes then appear on the dashboard labeled codex, alongside claude. To reverse it (leaving your own Codex hooks intact):
fleet uninstall codex
Fleet also tracks pi sessions. pi has no shell hooks, but it loads extensions from installed packages. fleet install pi registers fleet's hooks/pi directory as a local pi package:
fleet install pi
This:
hooks/pi to packages in ~/.pi/agent/settings.json (your own pi extensions and packages are untouched)~/.cache/pi-status)pi in ~/.config/fleet/agents.jsonThe extension publishes fleet status from pi's lifecycle events, so pi panes appear on the dashboard labeled pi (working / idle / done). A live composer spinner also corroborates working activity during long gaps between hook events, without overriding structured questions or completion. pi auto-runs its tools, so there is no permission-prompt state to surface. Question tools do surface QUESTION while awaiting input: @juicesharp/rpiv-ask-user-question via its stable blocked event, and compatibility-shimmed AskUserQuestion tools via pi's tool_call lifecycle. Homebrew upgrades update the registered package in place; in an already-running pi session, /reload picks it up. To reverse it (leaving your own pi extensions and packages intact):
fleet uninstall pi
fleet # Launch dashboard (preview auto-opens on wide terminals)
fleet --preview # Force preview pane on
fleet --no-preview # Force preview pane off
The dashboard shows every Claude Code pane grouped by urgency. Agents that need you sort to the top. Plain shell panes are hidden — you're here for the agents.
Each row leads with the tmux session name. When Claude Code has auto-named a session (the descriptive title it generates per task), Fleet shows that name in the detail column so you can tell sessions apart at a glance. Filtering with / matches on session name, Claude name, and project path.
The header carries a live summary strip — N need you · N working · N ready · N idle — so you can read the fleet's overall state without scanning rows. When there's nothing to list, Fleet tells you why with a distinct empty state: tmux isn't running, the hooks aren't installed yet, your filter matched nothing, or everything's genuinely quiet.
A few more cues while you work:
↑ N more / ↓ N more appear when the list outruns the viewport.fleet install offers two optional tmux keybindings (each confirm-gated, marked # fleet-managed so fleet uninstall strips them again):
prefix + f — open or focus Fleet in a persistent 34-column sidebar on the left. Press it while inside the sidebar, or press q, to close.prefix + F — open Fleet in a popup that floats over your current pane (display-popup -E).The sidebar follows its invoking tmux client across windows and sessions. Enter, double-click, and n jump to an agent without closing Fleet; your selection, filter, preview, and resized width survive. Fleet moves the same pane and Bun process rather than starting a scanner in each window. Other clients do not pull it away. A detached sidebar stays parked until a single client returns to its window. If the sidebar is the only remaining pane in its window, it stays there rather than implicitly destroying that window or session. It also waits outside a zoomed destination until you unzoom, and preserves a split source window's zoom when leaving.
Below 48 columns, Fleet reflows into stacked cards. Attention/ready counts and working/idle counts lead the sidebar; search and LIVE passthrough hints remain visible at narrow widths. Normal dashboard and popup launches still exit when you jump.
After upgrading, re-run fleet install to migrate the managed binding, and fleet statusline --inject --force to refresh click bindings. Custom bindings should pass both targets:
bind-key f run-shell "fleet sidebar --from '#{pane_id}' --client '#{client_name}'"
| Key | Action |
|---|---|
j / k or Up / Down | Navigate sessions |
Enter | Switch to selected session |
n | Jump to next waiting agent (cycles) |
p | Toggle preview pane |
s | Send prompt to selected session |
i | Enter passthrough (preview mode) |
y | Approve permission prompt (preview) |
/ | Filter sessions by name or project |
x | Kill selected session (confirms first) |
R | Rename selected session |
g | Toggle repo-group view (sibling worktrees) |
d | State provenance overlay (why this state?) |
? | Help overlay |
q or Esc | Quit (or clear filter) |
Press d on any agent to open a read-only state-provenance overlay: the
final state, how Fleet tracked the pane (hook / discovery / shell), the
hook/event/scrape candidates (with hook/event ages), the winning source, the
reason, matched detection rule id, decision timestamps, and whether the
working-timeout fired. It renders the fused StateDecision the last refresh
already attached — no live re-scrape — so it agrees with the --json observers.
fleet explain deliberately re-scrapes the current screen and may be newer.
Press any key to close.
Press g to toggle an opt-in repo-group view: agents that are sibling
worktrees of the same repository (they share a git common dir) group under a
single repo header instead of grouping by tmux session. It is off by default —
default ordering and navigation are unchanged until you press g, and toggling
keeps the same agent selected so your cursor position is stable across the
regroup.
You can also click a session row to select it; clicking a ready agent acknowledges it in place (see Acknowledge).
Fleet tracks seven states, sorted by urgency. The icon and color tell you what's happening at a glance:
| Icon | State | Meaning |
|---|---|---|
⚠ | waiting | Tool approval needed ([y/n] prompt) |
? | asking | Agent asked you a question (AskUserQuestion) |
● | ready | Turn ended — your move (finished, or asked in prose); green dot |
⠋ | working | Thinking or running tools (animated yellow spinner) |
○ | idle | Up but no recent activity (muted outline circle) |
■ | shell | No agent running (hidden by default) |
○ | down | No live process (hidden by default) |
asking vs. ready: A turn that ends — whether the agent finished the task or asked you something in prose — looks identical at the hook layer (both are a plain Stop). Fleet can't tell them apart, so both land in ready ("your move"). The dedicated asking state is only reachable through structured signals the agent emits: the AskUserQuestion tool and MCP elicitation dialogs. Either way both sort into the attention tier, so nothing that needs you gets buried.
Press s to send a prompt to the selected agent. Fleet never substitutes another agent when the selected one is busy or waiting for input. Submission rechecks the original pane's identity, lifecycle state, and current screen; failed or rejected sends retain the draft. Delivery can fail partway through, so inspect the target before retrying.
State gating: Fleet refuses to send to sessions with permission prompts (won't accidentally approve), sessions asking questions (won't answer for you), or dead sessions. Use --force in the CLI to override the busy check.
Press x to kill the selected session's pane. Fleet names the exact pane and asks you to confirm with y; any other key, including another x, cancels. Confirmation revalidates that original target. If it disappears or respawns, Fleet cancels rather than acting on the next row.
State gating: Same philosophy as send. Fleet only reaps sessions that are finished, idle, or already dead. It refuses to kill a working agent, one waiting on a permission prompt, or one asking a question — so you don't discard work or a pending decision by reflex.
Press p to toggle a live tmux capture-pane view of the selected session. Shows actual terminal output so you can verify state visually. Opens automatically on terminals wider than 120 columns.
The preview shows:
⌁3000)Resize the split: Drag the divider between the session list and the preview with the mouse, just like dragging a tmux pane border. The divider highlights while you drag and the split clamps between 20% and 80%.
When the preview pane is open, Fleet shows context-aware actions at the bottom of the preview based on the agent's current state:
y to approve, n to deny the permission prompti to answer inline via passthroughi for passthrough, s to send the next prompti for passthrough (watch and interact)Press i from the preview to enter passthrough mode. Every keystroke is forwarded directly to the agent's tmux pane — the preview updates live so you can see the result without leaving Fleet. Press Esc to exit back to the dashboard.
The footer shows LIVE and the exact target pane while passthrough is active. Forwarding stays bound to that pane and process; losing or respawning it stops forwarding instead of adopting another selection. Quick approval/denial requires a currently detected permission dialog, not just a cached hook state.
Fleet also surfaces agents it has no hook integration for. On its 5-second refresh it scans the process table for known agent commands — aider, cursor, opencode, gemini, amp, droid (plus claude/codex/pi) — maps each back to its tmux pane, and shows it on the dashboard labeled by type. No install, no config: start aider in a pane and it appears within ~5s.
Discovered agents get (nearly) the full status vocabulary, fused from three scrape-tier signals:
opencode showing △ Permission required reads as waiting, not idle) and its pane-title rules (Codex retitles to Action Required while blocked — caught on the fast 500ms cycle, since the title rides the same list-panes call).esc to interrupt).A hooked agent still always wins its pane: when a .status file exists, the hooked reading takes over — hooks add tool names, question-vs-permission disambiguation, and authoritative done. Discovery only ever fills the gap where there's no hook.
Tune it with tmux options (see Configuration): @fleet_discover off turns it off, @fleet_discover_agents overrides the allowlist, @fleet_discover_idle_secs sets the debounce.
When an agent finishes a turn — or stops to ask you something — while you're not looking, Fleet fires a silent OS-native desktop notification (osascript on macOS, notify-send on Linux). It's deliberately soundless: at fifteen agents, a chime per finish is noise, not a signal. Delivery is best-effort and no-ops cleanly when there's no desktop session (headless, SSH).
Two suppressions keep toasts from being redundant: none for the pane you're currently focused on, and none at all while you're watching the Fleet dashboard itself (you can already see the change on screen). A toast fires only on a real working → stopped transition, exactly once, and re-arms when the agent starts working again.
The in-tmux status-line flash still fires as before. The terminal bell, though, is now off by default — turn it back on with tmux set -g @fleet_bell on if you want the audible cue.
Fleet ships two state palettes — Catppuccin Mocha for dark terminals, Catppuccin Latte for light ones — and picks between them automatically. You can customize the seven agent-state colors with ${XDG_CONFIG_HOME:-$HOME/.config}/fleet/theme.toml:
[colors]
permit = "yellow"
question = "magenta"
done = "green"
busy = "bright-red"
idle = "blue"
shell = "bright-black"
down = "white"
Each role is required and accepts one of the 16 ANSI foreground names (black through white, plus their bright- variants) or a six-digit #rrggbb value. ANSI colors follow your terminal palette; RGB colors remain fixed, and the two forms can be mixed:
[colors]
permit = "yellow"
question = "#cba6f7"
done = "green"
busy = "#fab387"
idle = "blue"
shell = "bright-black"
down = "#45475a"
This is state-palette customization, not complete UI theming: chrome, style modifiers, pane previews, glyphs, and the tmux status line are unchanged. A missing file is normal. An invalid or unreadable file prints one warning and falls back to automatic selection.
Selection walks this chain and stops at the first hit:
FLEET_THEME — FLEET_THEME=light or FLEET_THEME=dark forces the theme outright.tmux set -g @fleet-theme light (or dark) pins it for every Fleet launched in that tmux server.theme.toml selects its ANSI/RGB colors.COLORFGBG if the terminal exports it. Current tmux doesn't forward the OSC 11 query, so this rung only fires when you run Fleet directly, not through tmux.NO_COLOR is always honored: set it and Fleet emits no color or styling escape sequences, whatever the theme would have been. It produces plain terminal text; it is not an ANSI-color fallback.
Beyond the theme chain above, Fleet reads a handful of tmux user options — set them live with tmux set -g <option> <value> or drop them in ~/.tmux.conf. All are optional, and changes take effect within one 5-second refresh (no restart).
| Option | Default | Description |
|---|---|---|
@fleet-theme | auto | Pin the palette to light or dark (see Theming). |
@fleet_bell | off | Ring the terminal bell on a cross-session transition. Off keeps Fleet silent at scale; the visual flash fires regardless. |
@fleet_discover | on | Discover agents with no hook integration from the process table. Set off to disable. |
@fleet_discover_agents | built-in list | Comma-separated allowlist of command names to treat as agents. Replaces the default claude,codex,pi,aider,cursor,opencode,gemini,amp,droid, so keep the built-ins you still want. |
@fleet_discover_idle_secs | 3 | Grace period (seconds) before a discovered agent flips working → idle, absorbing a single spinner-less frame. |
Environment variables FLEET_THEME (light/dark) and NO_COLOR are honored too — see Theming.
Each agent classifies its pane against a built-in detection manifest (ordered
regex rules, first match wins). You can tune one per agent by dropping a JSON file
at ~/.config/fleet/detection/<agent>.json (respects XDG_CONFIG_HOME). Two
formats are supported:
1. Override envelope (recommended) — schemaVersion: 1. Inherits a built-in
and applies explicit, stable-id operations on top of it, so you keep Fleet's
field-tested rules and only change what you name:
{
"schemaVersion": 1,
// Which built-in to inherit. Optional — defaults to this file's own agent.
"extends": "claude",
// Scalar overrides (each optional; omitted keys keep the inherited value):
"linesFromBottom": 15,
"promptMarker": "❯",
"approveKeys": ["1"],
"denyKeys": ["Escape"],
// Screen-rule operations (all optional):
"appendRules": [{ "id": "permit.my-tool", "pattern": "approve MyTool\\?", "flags": "i", "state": "PERMIT" }],
"replaceRules": [{ "id": "permit.yn", "pattern": "\\[y/n\\]", "state": "PERMIT" }], // swap in place, by id
"disableRules": ["permit.tab-to-amend"], // remove by id
// Title-rule operations mirror the screen ones:
"appendTitleRules": [],
"replaceTitleRules": [],
"disableTitleRules": [],
}
Rule state must be one of PERMIT, QUESTION, BUSY, IDLE. ids are
stable and must be unique; a duplicate or bad-regex rule is dropped with a
warning (its siblings survive), and replaceRules/disableRules ids that match
no base rule warn and are ignored. First-match ordering (base rules, then
appended rules) is preserved. An unknown schemaVersion or extends warns and
falls back to the built-in — detection never throws.
2. Legacy override — a file with no schemaVersion replaces the built-in
manifest wholesale (the pre-existing behavior; nothing is inherited).
Fleet also works as a non-interactive CLI for scripting and tmux integration.
| Command | Description |
|---|---|
fleet status [--tmux] <session> | Query agent state. --tmux outputs a tmux format string for status bars. |
fleet status --statusline | Render the multi-agent chip strip for tmux (second row by default, or your own bar). |
fleet next | Switch to the next waiting agent pane (cycles through PERMIT > QUESTION > DONE). |
fleet switch <pane-id> | Acknowledge a ready agent and switch to it (used by the statusline click binding). |
fleet ack <pane-id> | Acknowledge a ready agent in place (clear it from the attention tier, no switch). |
fleet send <session> <prompt> | Send a prompt to a session. Refuses unsafe states unless --force. |
fleet wait <sel...> --state <s> | Block until agent(s) reach a state. See Scripting & JSON API. |
fleet list [--json] | List every agent (human roster, or the versioned JSON envelope). |
fleet status --json [<sel>] | Query agent state as JSON, optionally narrowed by a selector. |
fleet watch [<sel>...] --jsonl | Stream state changes as JSON Lines (read-only) until interrupted. |
fleet capture --pane <sel> | Print a pane's current buffer as plain text (read-only). --json to wrap it. |
fleet doctor | Check tmux version, plugin installation, status directories, hook health. |
fleet reconcile [--dry-run] [--verbose] | Remove orphan status files for dead panes, fix stale working states. |
fleet install | Register Fleet as a Claude Code plugin + add second tmux status row. |
fleet install codex | Wire fleet into Codex's hooks.json + config.toml (preserves your own hooks). |
fleet install pi | Wire fleet into pi as a package extension (preserves your own packages). |
The observability verbs (list, status --json, watch, capture, wait) are a stable, read-only surface for orchestration. They only read tmux and agent state — they never write status files, acknowledge, or mutate anything.
Selectors — one grammar across every verb (first rule that fits wins):
| Selector | Matches | Example |
|---|---|---|
%<n> | a tmux pane id | %42 |
@<n> | every pane in a tmux window id | @5 |
<session>:<win> | a session and window name | api:build |
<session> | every pane in a session | api |
JSON envelope — agent-listing verbs (list, status, and watch snapshots) share one versioned shape. capture --json uses a command-specific payload with the same schema, outcome, queriedAt, and selector base fields:
{
"schema": "fleet.observe/v1",
"outcome": "ok",
"queriedAt": 1737064800000,
"selector": "api",
"count": 1,
"agents": [{ "pane": "%42", "session": "api", "status": "PERMIT", "...": "..." }]
}
watch --jsonl emits one type:"snapshot" envelope, then one type:"change" line per transition ({pane, from, to, agent}; from:null on appearance, to:null on disappearance). capture --json returns {schema, outcome, queriedAt, selector, pane, session, lines}; failures replace the capture fields with error.
Git metadata (additive, v1) — every agent view carries a read-only git
object (or null on a non-git dir), refreshed on the slow tick only (zero
fast-tick subprocesses):
{
"git": {
"repoId": "/home/u/proj/.git",
"commonDir": "/home/u/proj/.git",
"worktreeRoot": "/home/u/proj-feature",
"branch": "feature",
"detached": false,
"head": "a1b2c3d4e5f6789012345678901234567890abcd",
"dirty": true,
"staged": 1,
"unstaged": 2,
"untracked": 0,
"ahead": 3,
"behind": 0,
"upstream": "origin/feature",
"diffstat": { "files": 2, "added": 40, "removed": 5 }
},
"repoSiblingCount": 1
}
repoId is the absolute git common dir, shared by every linked worktree of a
repo — so repoSiblingCount reports how many other sibling worktrees appear in
the same listing (the current worktree is excluded). The legacy branch and
project fields are preserved unchanged.
Outcomes (the outcome field, and what drives exit codes):
| Outcome | Meaning |
|---|---|
ok | every requested agent resolved cleanly |
no_agents | tmux answered, but no agents are present at all |
no_match | a selector was given and matched nothing (agents exist) |
ambiguous | a selector matched multiple panes where one was required |
stale_data | tmux is unavailable; a prior snapshot is being reported |
tmux_unavailable | tmux could not be queried and no cached data was available |
Exit codes — scripts pipe on these; the numeric values are part of the contract:
| Code | Name | When |
|---|---|---|
0 | OK | success (ok, no_agents, and stale_data still exit 0) |
1 | USAGE | bad or missing arguments |
2 | NO_MATCH | a selector matched no agent |
3 | AMBIGUOUS | capture selector matched multiple panes |
4 | TMUX_UNAVAILABLE | tmux could not be queried |
124 | TIMEOUT | a bounded wait crossed its deadline (timeout(1) convention) |
fleet wait blocks until the target state is reached, polling on the dashboard's refresh cadence:
# Block until any pane in session `api` is ready, giving up after 5 minutes
fleet wait api --state ready --timeout 300
# Wait for BOTH sessions to finish (default is ALL selectors)
fleet wait api db --state ready
# Wait for the FIRST of several to need you, then jump to it
fleet wait api db --state waiting --state asking --any && fleet next
States accept display labels or raw names: ready (DONE), waiting (PERMIT), asking (QUESTION), working (BUSY), idle. Repeat --state to accept any of several; pass multiple selectors and --any to succeed on the first match instead of all. A selector that matches nothing exits 2; in --any mode, missing selectors are ignored while another selector is still live.
Hook-less DONE is observation-relative: long-running watch/wait processes can see a discovered agent transition from working to ready, while a cold one-shot status has no prior frame and reports that same prompt as idle. The JSON tracking and timestampKind fields make that distinction explicit.
# Live change stream for a session, one JSON object per line
fleet watch api --jsonl
# One-shot machine-readable snapshot of everything
fleet list --json | jq '.agents[] | select(.needsAttention)'
# Read a pane's screen without touching it
fleet capture --pane %42 --lines 100
fleet capture --pane %42 --json | jq '.lines'
Fleet supports two levels of tmux integration:
Second status row (recommended): A dedicated row showing all agents that need attention, with clickable entries. Set up automatically by fleet install, or manually:
fleet statusline --inject
Each entry is clickable (tmux 3.2+). Left-click an agent name to switch to that session; right-click to mark it read in place without switching. When any agent is ready, a ✕ clear chip appears at the end of the row — click it to dismiss every ready agent at once. Only agents whose turn it is for you appear: PERMIT (tool approval), QUESTION (a question to answer), and DONE/ready (finished, waiting on your next move). Working and idle sessions stay out of the bar — they don't need you to act, so they'd just be noise. Watch those in the dashboard instead.
The ☰ button sits at the far left of the row and is always there, even when no agent needs you. Click it (either mouse button) to open or focus the persistent 34-column sidebar; click again while the sidebar is focused to close it. The sidebar follows the invoking client, including with several clients attached to different windows. Same thing as prefix+f, minus the keyboard — and the same as fleet sidebar, which you can bind however you like.
After upgrading Fleet, run fleet statusline --inject --force once to update the click bindings and focus hook.
One-row bar (bring your own layout): If you'd rather keep a single status row, tell fleet not to own the second one and mount the chips yourself. Both options go in tmux.conf above the fleet-managed run-shell "fleet statusline --inject" line:
set -g @fleet_statusline_row none # inject installs the click bindings + focus hook only
set -g @fleet_chip_separator none # optional: no │ between chips (any string works too)
set -g status on # switch an existing two-row bar back to one row
set -g status-right '#(fleet status --statusline) #[default]…'
After reloading tmux.conf, run fleet statusline --inject --force once if Fleet was already injected. Restart any running Fleet dashboard after changing the separator, since it caches that setting at startup.
The chips stay clickable wherever they land: the bindings key off the range name, not the row. Running fleet status --statusline is also what tints windows when the rollup is on, so keep it on the bar even when it renders only ☰. A custom separator is spliced into tmux format output verbatim, so write a literal # as ##.
Status-right icon (lightweight): A single icon in your existing status bar:
set -g status-right '#(fleet status --tmux #{session_name})'
Shows a colored icon when agents in the current session need attention. Empty otherwise.
bind-key y display-popup -E -w 90% -h 70% "fleet"
bind-key n run-shell "fleet next"
Fleet has two halves that talk through the filesystem:
Hooks (bash, fast) TUI (Bun, interactive)
────────────────── ──────────────────────
Claude Code fires events ──> Reads status files
Notification Reads JSONL event logs
PreToolUse Scrapes pane content
Stop / SubagentStop Fuses into 7-state model
SessionEnd Renders dashboard
│ │
▼ ▼
~/.cache/claude-status/ Single-string ANSI frames
{pane_num}.status (no flicker, no framework)
{pane_num}.events.jsonl
Fleet doesn't trust any single signal. It fuses three layers for high-confidence state:
Hook signals (Layer 1, ~0ms) — Claude Code hooks write JSON status files on every event. Fast but can disagree with reality.
JSONL event stream (Layer 2) — Each hook appends to a per-pane event log. The TUI reads only the last event. Key insight: a stop_reason of tool_use means the agent is about to run another tool (BUSY), while end_turn means actually done.
Pane scraping (Layer 3, ~50ms) — tmux capture-pane as the visual arbiter. Detects permission prompts ([y/n]), question dialogs (Enter to select), the working token counter ((1m 11s · ↓ 3.4k tokens)), the animated braille spinner glyph (U+2800–U+28FF), and idle prompts. The spinner is a strong working signal: a harness paints it only while actually working, so — unlike the English strings — it can't be spoofed by a transcript that quotes them, and it still reads as working after the token-counter line scrolls off. Detection is an ordered, first-match-wins rule list. For Claude the live-only working indicators (token counter, esc to interrupt) are checked before the prompt rules: they vanish whenever a real dialog is up, so a genuine prompt still reads PERMIT/QUESTION, while an already-answered prompt lingering on screen during the next tool run correctly reads working instead of a false "waiting". The weaker spinner-glyph rule stays last, so a glyph co-present with a real [y/n] never hides it. For working-vs-idle the scraper defers to the hooks — a scraper miss can't downgrade a fresh working hook to idle — but a scraped idle prompt does clear a stale permission. (The same spinner check drives hook-less discovery for agents with no hook at all.)
Manifests can also carry title rules, matched against #{pane_title} instead of the screen. The title comes free with the fast cycle's one list-panes call, so title-sourced state lands in ~500ms instead of waiting for the ~5s scrape: Codex's Action Required title reads PERMIT (covering its missing Notification hook), and the braille title prefix Claude/Codex paint while working reads BUSY.
Freshness invariant: A state transition is only accepted if its timestamp is newer than the current state's timestamp. Prevents out-of-order hook deliveries from causing flicker.
Verify on switch: When you navigate to a pane (Enter or click), Fleet scrapes it immediately and updates the status file. Stale states get corrected the moment you look at them.
Acknowledge: Once you've seen a ready agent it drops to idle and leaves the attention tier (and the statusline). Ways to acknowledge:
choose-tree) clears it too, via a pane-focus-in hook, so you don't have to go through Fleet. Only a lingering ready chip clears this way; a pending PERMIT/QUESTION stays until you answer it on screen.✕ clear chip at the end of the statusline — acknowledges every ready agent at once.fleet ack <pane> — from the CLI, for scripting or bulk-clearing.A ready agent's completion can come from two independent places: the hook status file (done/completed) or an event-derived turn-end (a Stop/SubagentStop the status file may not reflect yet — the bar shows ready from the event stream while the file lags at idle). Acknowledgement retires both: it flips a ready status file to idle, and when the event stream shows a completion it appends an Acknowledged event so the derived ready can't re-assert. It survives Fleet restarts with no separate store. So: green ready = needs your eyes; muted idle = seen, nothing pending.
Decay: ready never auto-decays — a finished turn is waiting on you and stays until you act on it (switch to it, send a prompt, or it starts working again). Only working times out to idle, after 3 minutes, so a crashed turn doesn't spin forever.
The Claude Code plugin (hooks/) fires on five events:
permission_prompt → permit, elicitation_dialog → question, idle_prompt → readyAskUserQuestion tool is the exception — it means the agent is asking you, so it maps to asking, not working.tool_use stop reason = still working. end_turn = turn over (ready). Background tasks suppress completion. 3-second grace period.Each hook script sources hooks/lib.sh which handles status file writes, JSONL event appends, and the in-tmux notification flash (with self-notification suppression). The flash always fires; the terminal bell it used to ring is now gated behind @fleet_bell (default off — see Configuration). The silent desktop toasts are fired separately by the TUI, not the hooks (see Desktop notifications).
The TUI separates cheap and expensive operations:
.status files + one tmux list-panes call + JSONL last-line read. No subprocesses beyond that.lsof) and pane scraping (tmux capture-pane per pane, ~50ms each).git argv spawns per unique pane cwd (identity, worktree root, branch/dirty/ahead-behind, diffstat). Zero git subprocesses on the fast tick.bun scripts/bench-sidebar.ts ./dist/fleet 12 runs an isolated tmux workload, reporting subprocess counts and median/p95 follow, input-delivery, and preview-echo latency. It requires tmux and Python's standard-library PTY and never touches your existing server.Fleet reads agent directories from (in priority order):
~/.config/fleet/agents.json (new format)~/.config/agent-status/agents.conf (legacy format)~/.cache/claude-status + ~/.cache/codex-status + ~/.cache/pi-statusagents.json){
"agents": [
{ "name": "claude", "statusDir": "~/.cache/claude-status" },
{ "name": "codex", "statusDir": "~/.cache/codex-status" }
]
}
agents.conf)# name=directory
claude=$HOME/.cache/claude-status
pi=$HOME/.cache/pi-status
This file is only for hooked agents — ones that write status files. Agents picked up by hook-less discovery need no entry here; they're found in the process table.
Fleet is a zero-dependency Bun project.
bun install # Install dev dependencies
bun run dev # Run without compiling
bun run build # Compile to standalone binary (dist/fleet)
bun test # Run tests (921 tests)
bun run typecheck # tsc --noEmit
bun run lint # oxlint
bun run format # oxfmt
bun run format:check # oxfmt --check
Tests are collocated (*.test.ts next to source). The state engine, ANSI utilities, TUI model, and CLI commands are unit-tested. Tmux-dependent code has integration-style tests that gracefully degrade outside tmux.
bun test # Full unit suite
bun test src/state/ # State engine only
bun test src/terminal/ # Terminal primitives only
bun test src/tui/ # TUI model only
bun test src/cli/ # CLI commands
https://github.com/user-attachments/assets/2a9ce8db-767e-4260-a0e4-0a61562acef7
MIT
fleet uninstall | Remove plugin registration + tmux status row. |
fleet uninstall codex | Remove fleet's Codex hooks + config (leaves your own Codex hooks intact). |
fleet uninstall pi | Remove fleet's pi extension + registration. |
fleet statusline --inject | Manually add the second tmux status row and the click bindings. |
fleet statusline --remove | Manually remove the second tmux status row and the click bindings. |