Files
tty7/docs/agents/orchestration.mdx
T
l0ng-ai 449d38e538 docs(wait): say that an unknown pane answers exit, and pin it
`tty7 wait` is the one address-taking verb that does not refuse a pane the
server has no record of. `capture`, `procs`, `send` and `pane close` all
exit 1 on the same address; `wait` answers `exit`, `matched: true`,
`stale: true`, and exits 0.

That is the right behaviour and must not change. The server forgets a
pane once it is reaped, so "the worker finished and was cleaned up" and
"that id never existed" are one question to it — measured, not assumed: a
pane that really ran and exited comes back byte-identical to `%9999`,
and `pane ls --all` has forgotten both. Refusing would break the first
case, which is the ordinary end of an orchestration: you wait on work
that may already be over.

What was missing is that nobody had written it down. Neither the CLI
reference nor the orchestration page said what an unknown pane does, and
both define `exit` as "the pane is gone" — true of a typo, but not what a
reader takes from it when every neighbouring verb errors. An orchestrator
that trusts a bare `wait` as proof the work happened gets an instant
success from a stale id and reads an empty capture as "no output".

So both pages say it, and a test pins it. Without the test this is an
accident that reads like a bug, and the obvious "fix" — make it error like
its siblings — would silently break waiting on finished work.
2026-08-22 23:41:08 +08:00

199 lines
7.6 KiB
Plaintext

---
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 |
`exit` also answers for a pane id the server has never held. Once a pane is
reaped the server keeps no record of it, so "your worker finished and was
cleaned up" and "that id is wrong" arrive identically — `exit` with
`stale: true`, and exit status `0`. Unlike `capture` or `send`, a mistyped
address will not fail here. If a worker's result matters, read it back
(`tty7 capture`) rather than treating a bare `wait` as proof the work
happened.
### 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.
<Warning>
`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.
</Warning>
<Note>
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.
</Note>
### 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 →
Agents** 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
<Warning>
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.
</Warning>
- **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).