mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 16:02:24 +00:00
* feat(cli): wait on commands, press keys, and reap orphan panes
`tty7 wait` was the orchestration primitive for agents only. A pane with
nothing reporting agent status read as `idle`, so `--until idle` returned
success instantly about a shell that was midway through a build, and there
was no state that meant "the command finished" at all.
Panes with no agent now report `no-agent`, and `free` ends the wait when the
foreground command has exited — the process-tree question `procs` could
already answer but nothing could block on. `send --key` covers the keystrokes
text cannot express, which is what a worker stopped at `waiting` is usually
asking for. `pane close` takes several panes and `--orphans` clears what an
interrupted `run` leaves behind. `doctor` finally performs the hooks check
its own help has advertised.
The skill shipped in this repo predated `wait` entirely and taught a
hand-rolled `procs` polling loop with no notion of delegation; it now covers
the loop, and its agent statuses, `ws rm` orphan claim and not-implemented
list are corrected against the code.
* fix(cli): close the gaps review found in wait, --key and pane close
Five things the first pass got wrong, in the order they bite.
`--until free --changed` waited on a command it had already missed: the
"something ran" edge is only set by a poll that catches the pane busy, and a
command that starts and finishes inside one 500ms interval never is. That is
indistinguishable from a command that never ran, so the timeout now names both
doors instead of letting a finished build read as a hang.
`free` also outranked the agent ladder, which is backwards. A pane whose depth-0
process *is* the agent — the tree cannot tell that apart from a shell at its
prompt — reads free for its whole turn, so a `waiting` the caller explicitly
asked for could be overwritten by a process-tree fact and then withheld by the
`--changed` rule that comes with it. `free` is now consulted only when none of
the requested agent states answered, which is both cheaper and what the docs
already claimed. An empty process tree is "we could not see in" rather than
"free" for the same reason `no-agent` exists.
`--key M-X` sent `ESC x`: the whole spelling was folded to lowercase, which is
free for Ctrl (the C0 rule clears the case anyway) and wrong for Alt, where the
character rides through as itself.
`send --help` listed the key vocabulary by hand next to the table it is a list
of; it had already drifted by one alias. It is generated now.
And a `pane close` batch that could not close everything raised an error, which
left `--json` holding prose exactly when a cleanup script needs to know which
panes are still its problem. It exits 1 with `{"closed":[…],"failed":[…]}`, with
the complaint still on stderr so `-q` reports it.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
191 lines
7.2 KiB
Plaintext
191 lines
7.2 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 |
|
|
|
|
### 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).
|