--- title: "Orchestrating agents" description: "One agent opening a pane for another, waiting on it, and reading the result." --- Once an agent's status is a thing a program can ask about, one agent can run another. tty7 gives that loop a primitive instead of leaving it to screen scraping. ## The loop ```bash # 1. give the worker a pane PANE=$(tty7 split --v) # 2. hand it a task tty7 send "$PANE" 'claude -p "add tests for the parser"' --enter # 3. sleep until it needs you or finishes tty7 wait "$PANE" --until waiting,done --changed --timeout 600 # 4. read what happened tty7 capture "$PANE" --plain # 5. clean up tty7 pane close "$PANE" ``` That is the whole shape. The interesting step is the third. ## `tty7 wait` ```bash tty7 wait [%PANE] [--until STATE,…] [--changed] [--timeout SECS] [--interval MS] ``` Blocks until the pane reaches one of the states you named. | Flag | Default | | |---|---|---| | `--until` | `waiting,done,exit` | Which states end the wait — see below | | `--changed` | off | Ignore the state the pane was *already* in — only wake on one it moved into after the wait began | | `--timeout` | none | Give up after this many seconds, exiting 124 | | `--interval` | 500 ms | How often to poll | Exit codes are made for scripts: | Code | Meaning | |---|---| | `0` | A state you asked for was reached | | `124` | Timed out — the `timeout(1)` convention, so "not yet" is distinguishable from "broken" | | `1` | The worker died first; the JSON says `"status": "exit"` | The reply carries the agent's own message and its native session id, so a wake-up is directly actionable. ### The states Four of them are the agent's own [status](/agents/status). The other three are about the pane, because not everything worth waiting on is an agent: | State | Means | |---|---| | `idle` `working` `waiting` `done` | What the agent's hooks last reported | | `no-agent` | Nothing reports status in this pane — a plain shell, or an agent whose hooks are missing | | `free` | The foreground command has exited; the pane is back to its bare shell | | `exit` | The pane is gone. Ends every wait, whether you asked for it or not | ### Waiting on a command instead of an agent An agent says when it is done. A `cargo test` does not — so for a plain pane the question is whether anything is still running in front of the shell, which is what `free` answers: ```bash tty7 send "$PANE" 'cargo test > /tmp/t.log 2>&1; echo $? > /tmp/t.rc' --enter tty7 wait "$PANE" --until free --changed --timeout 900 cat /tmp/t.rc /tmp/t.log ``` `free` costs one extra request per poll, so it is only checked when you name it — and only once the agent ladder has not already answered, so pairing it with `waiting,done` never costs you a state you asked for. `free` is read off the process tree, which has two blind spots. A pane whose own root process *is* the command — what `tty7 run` spawns — looks free the whole time it runs; wait on `tty7 run` itself instead, it already blocks. And a backgrounded job (`… &`) keeps the pane busy after the foreground command has finished. A pane with no agent reports `no-agent`, **not** `idle`. That distinction is why `--until idle` cannot be used to mean "the command finished" — `idle` is a thing an agent says about itself, and a busy shell never says it. ### Why `--changed` matters The status the server keeps is a **level, not an event**. `done` stands until the next turn begins; `waiting` stands until the agent moves again. So a `wait` issued immediately after a `send` can answer with the *previous* turn's state, before the worker has even read the input. `--changed` refuses the state the pane was already in, which is what every round after the first needs. Without it, the JSON's `stale` flag tells you whether that happened. `free` has the same problem and a different fix: a shell that goes free → busy → free ends up where it started, so there is no new state to compare against. There `--changed` means "something ran while I was watching", which is exactly what you want in the line after a `send`. That does mean a command which starts *and* finishes between two polls is never seen running, and the wait sits there until it times out. If the thing you are waiting on can be that quick, poll faster (`--interval 100`) or drop `--changed` and let a sentinel file carry the answer. The timeout says as much when it happens. ## Answering a prompt A worker that stops at `waiting` is usually showing something that keystrokes, not text, are the answer to — a permission prompt driven by the arrow keys, a menu, a TUI to be dismissed. `send --key` presses keys: ```bash tty7 wait "$PANE" --until waiting --changed # it needs something tty7 capture "$PANE" --plain | tail -20 # see what it is asking tty7 send "$PANE" --key down --key enter # answer it tty7 send "$PANE" --key C-c # or stop it altogether ``` Keys are delivered as separate events 200 ms apart, so a raw-mode TUI reads a sequence as a sequence rather than as a paste. The [full vocabulary](/cli/reference#tty7-send-pane-text-enter-key-key) is in the reference. ## When an agent never moves If `tty7 wait` times out and `tty7 agents` shows a status that never changes, the likely cause is that the agent's status hooks are not installed or are out of date — the agent is working fine, it just has no way to say so. `tty7 doctor` reports where every agent's hooks stand, and `tty7 agents` names the specific one when it can see the gap. ## Watching everything at once ```bash tty7 agents # every agent on the machine: pane, agent, status, message tty7 agents --json # the same, parseable ``` If you are an agent yourself, you are in that list too. ## Teaching an agent to do this tty7 installs nothing into `~/.claude` for it — no switch in **Settings → Integrations** writes a skill, and none ever will. What the agent needs to know ships in the repository instead, as a skill you install yourself: ```bash npx skills add l0ng-ai/tty7 ``` That covers the pane-driving half — where it is, how to open a pane, send into one, read one back, and the rules below — see [the agent skill](/cli/agent-skill). The `wait` step is documented on this page. A skill rather than a global instruction, on purpose: only its one-line description rides in context until something reaches for it, so an agent that never touches another pane pays nothing for it. ## Rules of the road The panes on a machine are somebody's real work, and some of them are other agents mid-task. Treat anything you did not create as read-only. - **Never `send` into a pane you did not open.** Check `tty7 agents` first. - **Never close a pane, tab, or workspace you did not create.** - **Never `server stop` or `server restart`.** Every pane on the machine dies with it. - **Clean up what you did create** — `tty7 pane close %83` when you are done. An interrupted `run` leaves its pane behind; `tty7 pane ls --all` shows those, and `tty7 pane close --orphans` clears them. That last one is a human's broom, not an agent's: it closes every orphan on the machine, including ones somebody else abandoned mid-command. The full agent-facing contract is in [the skill](/cli/agent-skill).