Documentation

chela is a tiny control plane over a single tmux session: it schedules long-lived agents, dispatches a TODO list into pull requests, tells you the moment one finishes or blocks, and lets you watch the whole fleet on a live terminal wall.

Quickstart

Five minutes from clone to a scheduled agent. For a fuller walkthrough through to your first dispatched agent, see the Getting Started guide.

1. Install

chela uses uv. The core has two small deps; the dashboard + live terminal wall ship as a separate install (keeps the core lean). You also need tmux, git, the claude CLI on PATH (and gh for the dispatcher's PR flow).

# core
git clone https://github.com/Devail1/chelamux && cd chelamux
uv sync
uv run chela status

# dashboard + live terminal wall (separate install — keeps the core lean)
uv sync --extra dashboard
uv run chela dashboard

# the Telegram bridge is its own extra. Name every extra you want in ONE
# command — `uv sync --extra X` replaces the env and drops the rest
uv sync --extra dashboard --extra telegram

Authenticate Claude once. chela never touches credentials — it drives the claude CLI inside your tmux windows. Log in once on the machine (claude, then /login — or claude setup-token for a headless token); every agent window reuses the cached ~/.claude credentials. The whole fleet therefore runs as one Claude account and shares its 5h / 7d rate limits.

2. Make a session, schedule an agent

A tmux session is your fleet; each window is an agent (the window name is its display name).

# one tmux session whose windows are your agents
tmux new-session -d -s chela -n researcher

# see what chela can see
uv run chela status

# poke the agent every hour, then run the daemon
uv run chela schedule add researcher --every 1h --prompt "Run your research cycle."
uv run chela run

3. Dispatch a TODO list into PRs

Drop a WORKFLOW.md + TODO.md into a repo (copy examples/), then point chela at it. Each open - [ ] becomes a worktree → an agent → a PR.

uv run chela dispatch /path/to/repo/WORKFLOW.md --once   # one pass
uv run chela dispatch /path/to/repo/WORKFLOW.md          # poll forever

Concepts

tmux is the source of truth

chela holds no separate registry of agents. Discovery is tmux list-windows + pane_current_path, read live every tick — so what chela sees is exactly what's running right now. A window rename keeps the same agent; nothing to re-register.

One window per agent

Each window of your session (CHELA_TMUX_SESSION, default chela) is an agent. The window name is how you target it — schedules, messages, and the dashboard all key off it.

Two ways to put agents to work

  • Schedule — for long-lived agents with a standing role. chela types a prompt into the agent's pane on an interval, a cron expression, or once at a set time. The agent persists between pokes; give it a CLAUDE.md for stable context.
  • Dispatch — for ephemeral, one-shot work. Each - [ ] in a TODO.md spawns a throwaway agent in its own git worktree that implements the item, opens a PR, and tears its window down. Many tasks run in parallel.

The wall

The dashboard streams every agent's live terminal in one grid (drag, lock, maximize), with a context-window bar per tile and account-wide rate-limit pills — a first-class feature, just a separate install. The wall is on by default but loopback-guarded: because it serves writable shells, the dashboard only serves it on a 127.0.0.1 bind. A non-loopback bind refuses it unless you set CHELA_TERMINALS_EXPOSE=true. See Remote access & security.

Scheduling agents

A schedule pokes an agent's pane with a prompt on a cadence — the agent does the rest.

# interval: 30s / 5m / 1h / 1d
chela schedule add researcher --every 1h --prompt "Run your research cycle."

# cron expression
chela schedule add reporter --cron "0 */8 * * *" --prompt "Post the 8-hourly summary."

# one-shot at an ISO timestamp
chela schedule add deployer --once "2026-06-01T09:00" --prompt "Cut the release."

chela schedule list            # every task + its id
chela schedule remove 3        # delete by id

Standing context: the agent CLAUDE.md

A scheduled agent wakes into whatever its working directory contains. Drop a CLAUDE.md at the root to give it a stable role — what it is, what to do each cycle, and its boundaries (see examples/agent-template.md). The schedule supplies the recurring nudge; CLAUDE.md supplies the identity.

Dispatching work (the headline feature)

Turn a markdown checklist into a stream of pull requests, each built by its own isolated agent.

TODO.md — the work list

Every unchecked - [ ] bullet is a work item. Append <!-- blocked: reason --> to make the dispatcher skip a line.

## Open
- [ ] Add a --version flag to the CLI
- [ ] Write a docstring for the public API entry point
- [ ] Add a unit test for the config loader <!-- blocked: waiting on fixtures -->

WORKFLOW.md — the config + agent brief

A YAML front-matter block configures the dispatcher; the markdown body below it is the prompt template handed to each agent (with {{placeholders}} like {{task_title}}, {{branch_name}}, {{workspace_path}}). Put both files in the repo root.

project_key: PROJ          # branches/windows are <key>-<n>, e.g. proj-1

tracker:
  kind: markdown          # markdown TODO.md — also: gh_issues
  path: TODO.md           # relative to this file

workspace:
  root: ~/.chela/worktrees/proj   # where per-task worktrees go
  base_branch: main            # branch worktrees fork from + PRs target

concurrency:
  max: 1                  # tasks in flight at once

agent:
  cmd: claude --permission-mode auto   # or: bypassPermissions on a trusted repo
  startup_delay_seconds: 4
  ready_timeout_seconds: 60

hooks:                       # all optional, run in the worktree
  # after_create: seed .claude/settings.local.json (least-privilege perms)
  # before_run:   uv sync --quiet || true   (lockfile sync / codegen)
  # after_done:   runs in the repo when the PR merges (e.g. a deploy)

The lifecycle

Each task is keyed by a stable SHA of its source line (idempotent — a task is never picked up twice). For each open item the dispatcher:

  • creates a git worktree on branch <project_key>-<n>, forked from base_branch;
  • runs the optional after_create / before_run hooks, then spawns an agent in that worktree with your prompt body;
  • the agent implements the task, strikes its - [ ] → - [x] on its own branch, pushes, and opens a PR;
  • the agent's last step is chela task-finished <task_id>, which marks the run awaiting_review, records the PR URL, and kills its window;
  • when you merge, the struck line lands on base_branch, the item disappears, and the run flips to done on the next tick.

The dispatcher shape (task-list → isolated worktree → autonomous agent → PR) is an adaptation of OpenAI's Symphony pattern.

The orchestration loop

An orchestrator is just another agent — and it can only act when something messages it. So an agent finishing is invisible to it. The decisions inbox closes that: completion is pushed back into the orchestrator's own session.

Without it, delegating means polling a pane, or a human walking over to say "he's done". With it, the daemon reports — once — when a watched window finishes, blocks on a prompt, or dies mid-task, plus dispatcher runs that reach awaiting_review, failed or needs_human.

# in the orchestrator's session, after dispatching work to window @3:
chela drive @3 "Fix the parser bug in src/lex.rs; commit when tests pass."
chela watch @3 --note "parser bug"   # register interest — you'll be told
chela watching                       # what's watched, what's queued, and whether
                                     #   the address it delivers to is still real
chela watch                          # no window: (re-)register THIS session as the
                                     #   orchestrator — the fix after a tmux restart

The report arrives as one line typed into the orchestrator's session:

📥 @3 · chelamux finished the task you dispatched — verify + commit. — note: “parser bug”

Four rules make writing into a live session safe

  • Delivery is gated on idle, strictly. A busy session is mid-thought and is never interrupted — the event queues durably and goes out on the next idle tick. A waiting session is worse than busy: it is sitting on a permission prompt, and a paste there would be read as the answer to that prompt. So not busy is not good enough; only a genuinely idle session is ever written to.
  • You must register interest. Every agent turn ends busy→idle, including the orchestrator's own replies, so only work you explicitly chela watched produces an event. That also covers plain chela drive delegation, which is not a dispatcher run and has no run state of its own. The watch clears when the completion fires: one dispatch, one report.
  • The orchestrator is identified explicitly, never guessed. Whichever session runs chela watch registers itself (via $CHELA_WID); pin it with CHELA_ORCHESTRATOR_WID=@0. Until something registers, the inbox is inert — it cannot push into a random agent's session. It never reports on the orchestrator's own window either, so it cannot notify itself in a loop.
  • @3 is an address, not an identity — so every stored one carries its tmux epoch. tmux issues window ids per server: restart it and the fleet comes back renumbered from @0. On 2026-07-14 an OOM did exactly that, and the inbox spent the day pushing at a @0 that no longer existed — five finished PRs went unreviewed, in total silence, with chela doctor green. Every persisted id is now stamped with the server that issued it, and one from a dead server is never acted on. Being undeliverable is loud: an ERROR every tick, an inbox_undeliverable event in the Feed, a phone push, and a red chela doctor. Nothing is lost — the queue waits, and chela watch with no arguments re-registers the session that is really there.

An idle prompt is not necessarily a prose prompt. Claude Code's input line has modes: ! runs a shell command, # writes to memory. A session in ! mode is perfectly idle — and it will execute the next line it receives. (It did: a notification built from an agent-authored PR title was run by /bin/bash.) So the mode is read off the pane and an unsafe one is refused — the event is held in the queue, never dropped — and, independently, every summary is neutralised of shell metacharacters, control bytes and mode-switching prefixes before it is typed. Agent-authored text must never be indistinguishable from something you typed.

Turn it off entirely with CHELA_INBOX_ENABLED=false.

Agent rooms

A typed, durable ledger two or more windows are members of — plus active dispatch, so a question wakes the peer, they answer, and the answer routes back to the asker with no human in the middle.

chela msg fires a string into a pane and vanishes: no record, no kind, no reply path. A room is the relationship that message never had.

chela room create wire
chela room join wire --wid @3            # no --wid: your own window
chela room join wire --wid @4
chela room post wire --kind question --from @3 --to @4 \
  -- "Does the retry live in the parser or the client?"
chela room digest wire                   # the ledger — read from the event log

@4 finds this typed into its prompt, along with the command it needs to answer:

[chela room] question from @3 (parser) in room "wire" (post #128):
> Does the retry live in the parser or the client?

Answer by posting back to the asker — this wakes them, with no human in the middle:
  chela room post wire --kind handoff --from @4 --to @3 --reply-to 128 -- "<your answer>"
  • A room is membership plus a filter over the event log. Every post is one event (room_<kind>), so there is no second event store to drift. Only the membership gets a file of its own ($CHELA_DIR/rooms.json): it is mutable, and the log is append-only and rotates.
  • Only a targeted handoff / question / blocker may interrupt. Every other kind (status, finding, summary, task, …) is recorded and never injected, and an untargeted post is never injected at all — a fleet where any post can paste into any pane is an interrupt storm.
  • The inbox's rails, reused. A waiting agent is never pasted into — that paste would answer its gate — so the delivery is parked until the gate clears. A dead or unknown recipient fails loudly, exit 1. Messaging yourself is refused.
  • The loop guard is structural, not advisory. An echo between two live agents burns a real machine. A relayed prompt can never be re-posted (every injected prompt opens with a fixed [chela room] header), a reply chain is capped at 6 hops, and no window may be injected into by the same peer more than 6 times in 5 minutes. A tripped guard still records the post; it just wakes nobody.
  • A body is untrusted input on its way into a terminal. ANSI escapes and control characters are stripped, and the body is capped and quoted — so a /slash-command, an Escape or a Ctrl-C in a message cannot drive the recipient's TUI.
  • A restarted agent is handed its rooms back. Everything a room told an agent lived in that session's context, and a dispatched agent is a fresh session every run — so a restart forgot the lot, silently. The plugin's SessionStart hook injects a short recap (chela room recap prints the same thing): the last few posts per room, newest first, sanitised, with the --reply-to cursor to answer on. An agent in no room gets nothing — not a header, not a "no shared context" line — because this text is prepended to every fresh context in the fleet.

Agent autonomy (permission modes)

chela never manages permissions itself — it launches claude, so an agent's autonomy is whatever --permission-mode it was started with.

ModeBehaviourGood for
defaultAsks before every non-trivial actionWatching closely / untrusted repo
planRead-only; proposes a plan, changes nothingScoping before you let it run
acceptEditsAuto-accepts file edits, still gates the restLight supervision
autoA classifier auto-approves safe ops and gates dangerous oneschela's dispatcher default — rarely hangs, still gated
dontAsk / bypassPermissionsNo gating at allZero-hang autonomy on a repo you fully trust

Two launch paths set the mode independently — this is the part to know

  • Dispatcher agents read agent.cmd from each repo's WORKFLOW.md (version-controlled, per-repo). Default: claude --permission-mode auto.
  • Dashboard Start button / launcher use the CHELA_AGENT_CMD env (global). Default: plain claude — i.e. default mode, which asks on every action, so a launched agent you are not watching will sit waiting on a prompt.

To change the launcher default, set the full command — e.g. export CHELA_AGENT_CMD="claude --permission-mode acceptEdits" — then restart the daemon. Prefer auto or acceptEdits for agents on a wall you do not babysit; reserve bypassPermissions for repos you fully trust.

Resource isolation — a known gap

Permission modes bound what an agent may do; nothing bounds what it may consume. A dispatched agent runs pytest / uv sync / a backtest unisolated, and chela's supervisor shares a failure domain with the workers it spawns — a job that eats the box takes tmux, the daemon and the orchestrator down with it. chela does not put agents in cgroups; run heavy work under a shared memory slice instead. The failure mode, the measured numbers, and why a per-job cap does not protect the machine: docs/RESOURCE_ISOLATION.md.

Dashboard & the wall

uv sync --extra dashboard
uv run chela dashboard           # http://127.0.0.1:5001

The web UI has tabs for agents (live liveness — alive / waiting / offline), schedules, the dispatcher, and a Kanban of runs. Liveness is derived from the native session status — no heartbeat daemon.

The embedded terminal wall streams the live ttyd panes in a grid. It's on by default but loopback-guarded: it serves writable shells, so the dashboard only serves it on a 127.0.0.1 bind. On a non-loopback bind it's refused unless you opt in explicitly:

CHELA_TERMINALS_EXPOSE=true uv run chela dashboard --host 0.0.0.0
# or turn the wall off entirely:
CHELA_TERMINALS_ENABLED=false uv run chela dashboard

The hooks plugin (recommended)

chela works without any Claude Code hooks — it scrapes each tmux pane as a fallback. But the event-log plugin (chela plugin) is strongly recommended: it POSTs every tool call, prompt and permission gate to the daemon before the fact, which unlocks lossless blocked-agent questions on Telegram (a scraped multi-question or preview selector otherwise reaches your phone with no options), answering a question with zero keystrokes, and the live event Feed. It fails open — if the daemon is down the hook logs a warning and your agent carries on.

Install it straight from this repo, inside Claude Code — works out of the box on the default dashboard port (5001):

/plugin marketplace add Devail1/chelamux
/plugin install chela@chela

On a non-default dashboard port? A hook URL is a literal (Claude Code doesn't expand env vars in it), so render your own copy with the port baked in:

chela plugin --dir ~/.chela/plugin        # bakes in the port the dashboard actually bound
claude --plugin-dir ~/.chela/plugin       # or: /plugin marketplace add ~/.chela/plugin

Context & rate-limit pills

Exact context-window usage and the 5h / 7d rate-limit pills come from Claude Code's statusLine payload, which chela caches via a tiny hook. Install it once for precise numbers (without it, the context bar falls back to a coarse transcript estimate):

chela install-statusline           # prints the snippet
chela install-statusline --write   # writes it (won't clobber an existing one)

Keys not reaching the terminal

If Esc (or other keys) never reaches an embedded terminal, a vim-style browser extension such as Vimium is almost certainly capturing them at the page level — it injects into the terminal's iframe too and swallows the keypress.

Exclude the dashboard's URL in the extension's settings. In Vimium: Options → "Excluded URLs and keys" → add the dashboard URL and leave the Keys field blank to disable it on that site. Quick workaround: Ctrl+3 (or Ctrl+[) sends a literal Escape.

Collaborative terminals (end-to-end encrypted)

Share a live terminal over the internet — encrypted end to end, through a relay that only ever sees ciphertext.

Share a session

In the dashboard, click Share (the link icon) on any pane's header — or Share current session from the ⋮ overflow menu on mobile. chela mints a share and shows a join link plus a short pairing code. Send both to whoever's joining; they open the link, paste the code, and they're in the same live terminal with full access — watch, type, and scroll. Everyone sees each other as live, labeled cursors and a facepile of who's watching.

Sharing is off until you set CHELA_COLLAB_RELAY (see below) — chela never phones home. Once a relay is configured, the Share control appears on every pane.

How the encryption works

  • The pairing code is the key. It's 16 random bytes shown as base32. Both browsers derive AES-256-GCM keys from it with HKDF-SHA256 — directional keys for the terminal stream and a symmetric group key (k_pres) for presence — entirely in the browser. The keys are never sent anywhere; a wrong code just fails to decrypt (you get "wrong code", not garbage).
  • The relay is zero-knowledge. It's a dumb WebSocket fan-out (one room per share) that broadcasts opaque frames it cannot read — it never holds a key. It sees metadata only: the room name (derived from your tmux session + window id — not a secret) and message timing/size. The code is the sole security boundary, so a guessed room just yields undecryptable frames.
  • Revocable. Stop a share and the room dies and the code rotates. The topbar's active-shares indicator gives one-tap Stop / Stop-All.

Presence

Everyone in a shared session is a live, labeled cursor mapped to the terminal grid, plus a facepile of who's watching (the host gets a ★). Cursors and names ride the same encrypted channel (k_pres), so the relay can't see who's present or where they're pointing. It's colorblind-safe — every cursor carries a name label, not just a color.

On a phone

Joiners on mobile get the full experience: the terminal letterboxes to fit, an on-screen keys-line (Esc / Tab / arrows / a sticky Ctrl / …) sits above the keyboard, swipe scrolls the session, and your touch shows to everyone else as a cursor.

Run your own relay

The relay is a small Cloudflare Worker (source in chela/collab-relay/) — a dumb, opaque fan-out with one Durable Object per room. Deploy your own and point chela at its wss:// URL so even the room-name/timing metadata stays yours:

# deploy the relay (Cloudflare Workers)
cd chela/collab-relay && npx wrangler deploy

# point chela at it, then start the dashboard
CHELA_COLLAB_RELAY=wss://your-relay.workers.dev uv run chela dashboard

Full access is the model. A paired joiner holds the code, so they can both watch and drive (scroll is input on a full-screen TUI). Share only with people you'd hand the keyboard to.

Command reference

One CLI, chela. Every command targets agents by their tmux window name.

Core

chela status

List the agent windows discovered in your tmux session — the source of truth.

chela run

Run the daemon: scheduler tick + dispatcher + needs-input notify. Leave it running.

chela dashboard [--host] [--port]

Launch the dashboard + live terminal wall (needs the dashboard install). Binds 127.0.0.1:5001.

Scheduling

chela schedule add <agent> --prompt … (--every|--cron|--once)

Schedule a prompt — an interval (30s/5m/1h/1d), a cron expression, or a one-shot ISO timestamp.

chela schedule list

List every scheduled task with its id.

chela schedule remove <id>

Delete a task by id.

Dispatch

chela dispatch <WORKFLOW.md> [--once] [--interval N] [--dry-run]

Turn each open - [ ] item into a worktree, an agent, and a PR. Polls every 60s; --once runs one tick.

chela dispatch-runs

List dispatcher runs and their status.

chela task-finished <task_id>

(agents call this) mark a run awaiting-review, record the PR, and kill its window.

Messaging

chela msg <agent> <message> [--from] [--priority]

Drop a message into one agent's pane. Priority: critical|high|normal|low.

chela broadcast <message> [--from] [--priority]

Send the same message to every other live agent at once.

Setup

chela plugin [--dir PATH] [--port N]

Render the Claude Code hooks plugin (event log + zero-keystroke Telegram answers). Recommended — see above.

chela install-statusline [--write] [--force] [--settings]

Print (or --write) the statusLine snippet for ~/.claude/settings.json so panes report live context + rate limits.

Configuration

All configuration is environment variables, with sensible defaults.

VariableDefaultPurpose
CHELA_TMUX_SESSIONchelatmux session chela orchestrates
CHELA_DIR~/.chelaState dir (scheduler.db, worktrees, context)
CHELA_SCHEDULER_POLL_INTERVAL30Daemon loop interval (s)
CHELA_DISPATCH_WORKFLOWS—Colon-separated WORKFLOW.md paths the daemon dispatches
CHELA_DISPATCH_TICK_INTERVAL60Dispatcher tick interval in the daemon (s)
CHELA_AGENT_CMDclaudeLaunch command for the dashboard Start button
CHELA_NOTIFY_URL—Needs-input notification target (ntfy / Telegram / webhook)
CHELA_DASH_HOST / CHELA_DASHBOARD_PORT127.0.0.1 / 5001Dashboard bind address
CHELA_TERMINALS_ENABLEDtrueEmbedded ttyd terminal wall (streams live; loopback-guarded)
CHELA_TERMINALS_EXPOSEfalseServe the writable wall on a non-loopback bind too (RCE risk — opt-in)
CHELA_COLLAB_RELAY— (off)Relay wss:// URL for collaborative terminal sharing (end-to-end encrypted). Empty = sharing off; chela never phones home. Details.
CHELA_DEFAULT_CONTEXT_WINDOW200000Window size assumed by the transcript-based context estimate (fallback)

Needs-input notifications

When an agent's pane enters the waiting state (a permission prompt or a question), chela fires one edge-triggered notification — so you don't have to babysit. Transport is auto-detected from the URL:

CHELA_NOTIFY_URL=https://ntfy.sh/my-chela-topic                                  # ntfy
CHELA_NOTIFY_URL="https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>"  # Telegram
CHELA_NOTIFY_URL=https://example.com/hook                                       # generic webhook

Remote access & security

chela ships with zero built-in auth, by design. The dashboard and the ttyd terminals bind 127.0.0.1. The wall is a writable shell — exposing it on an untrusted network is remote code execution. The tailnet is the trust boundary, not a password.

For remote access, put the loopback dashboard behind one of:

  • Tailscale — tailscale serve 5001 gives you TLS + tailnet ACLs for free (recommended).
  • An SSH tunnel — ssh -L 5001:127.0.0.1:5001 host.
  • A reverse proxy with your own auth.

Or skip the web UI entirely: SSH/Mosh in from a mobile terminal and tmux attach -t chela for the live panes straight from your phone.

Or run the built-in Telegram bridge — chela telegram gives every agent window its own forum topic (1 topic = 1 window = 1 session), so you can drive or supervise any agent from your phone, two-way (text, images, and file attachments flow both ways). It's a full bridge, distinct from the one-shot needs-input notifications above — and it ships with chela, no separate service to run.

To share a session with someone else over the internet — rather than exposing your whole dashboard — use collaborative terminals: end-to-end encrypted, through a relay that only ever sees ciphertext.

How it works

  • Discovery is tmux-native. tmux list-windows + pane_current_path are the single source of truth — no external state file, no daemon to coordinate with. tmux never lies about what is live right now.
  • The dispatcher keys each task by a stable SHA of its source line, creates a worktree per task under ~/.chela/worktrees/ on branch <project_key>-<n>, and tracks runs in ~/.chela/scheduler.db.
  • A PR that fails review goes back to the agent that wrote it. awaiting_review used to be the end of the line: the reviewer found real defects and had nowhere to put them, so a human climbed into the worktree and hand-started a fix agent. Now the verdict is written on the run and the next tick re-spawns that agent in its original worktree, on its original branch, with the verdict as its prompt — so the branch history and the open PR survive, and the PR simply updates when it pushes. The loop is bounded (CHELA_MAX_REWORKS, default 2): past the cap the run stops at needs_human, keeping its branch, worktree and PR, and surfaces to you through the decisions inbox carrying every verdict it was given.

The run row is the authority, not GitHub. gh pr review --request-changes is refused on a PR your own account authored, and a fleet is one account — so the verdict lives on the run and the PR comment is its human-readable copy.

⚖️ The judge: every guard is corrupted, and one that survives sends the PR back

CI proves the suite passes. It cannot prove the suite can fail — it runs the tests, and the tests are the thing that might be broken. Measured here on 2026-07-14: of five CI-green PRs, four had a guard that survived deliberate corruption — a test that still passed with the state it guarded folded back in, a colourblind cue whose glyph could be emptied with 0 failures, a feature whose entire production wiring could be reverted with 1112 passed. Every feature worked; the proof that they kept working did not exist.

Why not a structured agent protocol (e.g. ACP)?

A protocol like the Agent Client Protocol would hand chela typed events directly, with no pane-reading at all — and in the abstract that is the cleaner interface. chela deliberately does not depend on one, because its whole value is that an agent is a real claude process in a real terminal a human can also watch and grab — the live wall, the collaborative terminals, tmux attach. A headless protocol session trades that away. So chela keeps the human-drivable PTY and recovers the structure a protocol would give from the channel that already has it — the transcript — reserving the pane for the one thing only it knows: that an agent is waiting for you right now.

And increasingly, not even that. The transcript only records an interactive tool when it is answered, which is why a pending gate had to be read off the terminal at all. Claude Code hooks — shipped as a plugin, POSTing into the daemon — close that gap from the other side: a permission request or an AskUserQuestion lands in the event log while the agent is still blocked on it, typed, with every option's label and description attached. And the answer goes back the same way, so a question is answered with zero keystrokes at the terminal. The pane-scraped gates remain the fallback — hooks are read at agent startup, so a running fleet has none.

HTTP API (selected)

RouteReturns
GET /api/agentsPer-window liveness/health, session status, context, schedules
GET /api/summaryHeader counts (agents online, schedules)
GET /api/schedules · POST · DELETE/PATCH /<id>Scheduled tasks CRUD
GET /api/dispatcherOpen tasks + active/awaiting/recent runs per workflow
POST /api/dispatcher/runs/<id>/merge · /merge-allSquash-merge PRs + clean up
POST /api/agents/{start,stop,restart,msg,broadcast,trigger}Agent controls
GET /api/eventsServer-Sent Events stream (reactive UI accelerator)

The dispatcher shape (task-list → isolated git worktree → autonomous agent → PR) is an adaptation of OpenAI's Symphony pattern.