docs(skill): restructure the agent skill around a delegation playbook (#702)

* docs(skill): restructure around a delegation playbook

SKILL.md becomes a slim routing layer: a what-are-you-here-to-do section up
front, the pane/run/wait primitives, and four delegation rules that survive
even when the reference is skipped. Everything specific to running another
agent moves to references/delegation.md, which adds what the old text never
had: per-worker git worktree isolation, a delivery contract collected through
git instead of screen scraping, a launch-verification checklist, a babysit
loop, and a fan-out harvest with short per-worker timeouts so one stuck
worker cannot stall the round. Also replaces the last remaining 'claude -p'
example (the fan-out one #699 missed) and keeps every snippet valid under
both bash 3.2 and zsh.

* docs(skill): un-deadlock the fan-out harvest loop

Fresh read of SKILL.md and references/delegation.md. Every internal anchor
resolves and the two files agree on the primitives; three things did not
hold up:

- The harvest loop passed `--changed`, which cannot work there. `wait`
  compares against the state standing when *that* wait began, so a worker
  that reached `done` while you were waiting on a different one is already
  in `done` when its own turn in the round comes up — refused, every round,
  forever. Each pane runs one turn, so a standing `done` is this turn's;
  drop the flag and note the one thing it was buying (a just-answered
  `waiting` worker needs to leave that state before it is requeued).
- The same loop folded `wait`'s exit 1 into its 124 branch, so a pane that
  died got requeued instead of reported — and requeued at full speed,
  since a dead pane answers immediately. Split the three codes.
- SKILL.md described `--plain` unwrapping "a line the shell wrapped at
  column 249" while two other passages state a pane is 120 columns.
  Say "at the pane's width", as references/commands.md already does.

No typos or grammar slips found. Every bash block in both files parses
under bash 3.2 and zsh.

* docs(skill): two failure modes from the playbook's first live run

Dogfooded the delegation playbook end to end (worker reviewing this very
file). Two failures it hit that the text did not cover:

- A turn aborted by an API error emits no turn boundary, so the status
  stands at 'working' forever and wait sleeps through it. Diagnose from
  the screen's error line; recover by telling the still-alive interactive
  session to continue.
- A short capture tail cuts off the spinner line and shows only the TUI's
  always-present input box, which reads as idle. Tail 15+ lines and read
  for the spinner; 'bottom looks like a prompt' is only evidence on a
  shell pane.
This commit is contained in:
l0ng-ai
2026-08-20 21:02:11 +08:00
committed by GitHub
parent 8a950a343b
commit 024d368925
3 changed files with 356 additions and 161 deletions
+11 -1
View File
@@ -15,7 +15,8 @@ npx skills add l0ng-ai/tty7
The source lives at
[`skills/tty7/`](https://github.com/l0ng-ai/tty7/tree/main/skills/tty7) in the
repository: a `SKILL.md` and a full command reference.
repository: a `SKILL.md` and two references — the full command table, and a
delegation playbook the agent reads before handing work to another agent.
<Note>
This is the only skill tty7 has — nothing in **Settings → Agents** installs
@@ -89,6 +90,15 @@ tty7 send "$PANE" 'cargo test > /tmp/t.log 2>&1; echo $? > /tmp/t.rc' --enter
tty7 wait %3 --until waiting,done --changed --timeout 600
```
### How to delegate
`references/delegation.md` is the part the agent reads before spawning a
worker: give the worker its own git worktree and workspace, hand the task over
in interactive mode with the delivery contract in the prompt, prove the
command actually started, babysit with `tty7 wait`, collect the result out of
git rather than the screen, clean up — and, for a fan-out, harvest with short
per-worker timeouts so one stuck worker cannot stall the round.
## For humans writing their own tooling
The same material is worth reading even if you are not an agent — it is the
+58 -160
View File
@@ -1,7 +1,7 @@
---
name: tty7
description: >-
Drive the tty7 terminal workbench from the shell with the `tty7` binary — list workspaces/tabs/panes, split a pane, send text or keystrokes into one, capture what is on a pane's screen, run a command in a real PTY and pass its exit code through, block until a pane finishes or needs input, see which coding agents are running and which ports a pane is listening on. Use this whenever tty7, panes, workspaces, or `%42`/`@7`/"the other pane"/"the other agent" come up; whenever you want to hand work to another agent and collect the result ("get Claude/Codex to do X", "派个活", "let another agent handle this", running several agents in parallel); whenever you need to start something long-running or interactive (dev server, REPL, ssh session, `tail -f`, a TUI) that should not sit blocking your Bash tool; whenever a program needs a real terminal to behave the way the user sees it; and whenever you need to look at or report on what is running in some *other* terminal on this machine. Cheap to check: if `$TTY7_PANE` is set you are already inside tty7 and every command here works with no setup.
Drive the tty7 terminal workbench from the shell with the `tty7` binary — list workspaces/tabs/panes, split a pane, send text or keystrokes into one, capture what is on a pane's screen, run a command in a real PTY and pass its exit code through, block until a pane finishes or needs input, see which coding agents are running and which ports a pane is listening on. Use this whenever tty7, panes, workspaces, or `%42`/`@7`/"the other pane"/"the other agent" come up; whenever you want to hand work to another agent and collect the result ("get Claude/Codex to do X", "派个活", "let another agent handle this", running several agents in parallel and merging what they produce); whenever you need to start something long-running or interactive (dev server, REPL, ssh session, `tail -f`, a TUI) that should not sit blocking your Bash tool; whenever a program needs a real terminal to behave the way the user sees it; and whenever you need to look at or report on what is running in some *other* terminal on this machine. Cheap to check: if `$TTY7_PANE` is set you are already inside tty7 and every command here works with no setup.
---
# Driving tty7 from the command line
@@ -34,27 +34,25 @@ If `tty7 doctor` says the server is unreachable, stop and tell the user — do
not run `tty7 server start` on your own initiative. Starting a server they
didn't ask for changes what their GUI attaches to.
## When to use this instead of the Bash tool
## What are you here to do?
The Bash tool is right for anything that starts, does its job, and exits.
Reach for tty7 when one of these is true:
Four jobs, four shapes:
- **It shouldn't block you.** A dev server, a watcher, `tail -f`, a long test
run you want to check on later. Put it in a pane, come back and read it.
- **It's interactive or stateful.** A REPL, `ssh`, a database shell, anything
where you send one thing, read the answer, then send the next. A pane keeps
the session alive between your turns; a Bash call cannot.
- **It needs a real TTY.** Programs that detect a pipe and change behaviour —
colour, progress bars, TUIs, `top`, anything using raw mode. `tty7 run`
gives a genuine PTY at 120×30.
- **The user should be able to watch it.** Anything in a pane shows up in their
tty7 window, live. That is often the whole point.
- **You're being asked about something you didn't start.** "What's running in
that pane?", "why is port 3000 taken?", "what are my agents doing?" — you can
answer those from here without touching anything.
- **Someone else should do the work.** Another coding agent can run in a pane,
and you can wait on it and read its answer. See [Handing work to another
agent](#handing-work-to-another-agent).
1. **Run something that shouldn't block you or needs a real TTY** — a dev
server, a long test run, a TUI. [Running a command](#running-a-command-two-shapes).
2. **Talk to something stateful over time** — a REPL, `ssh`, a debugger.
Same primitives: [send](#non-blocking-a-pane-you-talk-to-over-time),
[read](#reading-a-pane), repeat.
3. **Look at what this machine is doing** — other panes, other agents, ports.
[Looking around](#looking-around), strictly read-only.
4. **Hand work to another coding agent** — one worker or a fan-out of several.
Read `references/delegation.md` first; the short version is
[below](#handing-work-to-another-agent).
The Bash tool remains right for anything that starts, does its job, and exits
without needing a terminal or an audience. A pane earns its keep when the
process outlives your turn, needs a real PTY, or should be visible to the user
in their tty7 window — that last one is often the whole point.
## Addresses
@@ -131,8 +129,11 @@ wait and it does not tell you what happened — reading is a separate step, and
waiting is `tty7 wait`.
For keystrokes rather than characters — Ctrl-C, Escape, the arrow keys — use
`--key` (see [Answering a prompt](#answering-a-prompt)). Typing `^C` as text
does nothing; it arrives as two characters.
`--key`: it takes `enter escape tab backtab space backspace delete up down
right left home end pageup pagedown`, plus `C-<char>` for Ctrl and `M-<char>`
for Alt. Repeat it for a sequence; text and keys compose, text first. Typing
`^C` as text does nothing — it arrives as two characters; `--key C-c` is the
real interrupt.
**A brand-new pane can swallow the Enter.** A shell still working through its
startup files — a prompt framework, `fastfetch`, anything that paints on login —
@@ -161,9 +162,10 @@ tty7 capture %83 --plain
`capture` hands back what the daemon stored — the pane's bytes, escapes and
all — and `--plain` replays them through a terminal grid and prints the
resulting text instead. Not a stripper: colour and cursor escapes are gone, but
also a line the shell wrapped at column 249 comes back as one line, a progress
bar that rewrote itself with `\r` reads as its final value, and a TUI's screen
lands where it was drawn. Use it whenever a human would want to read the output.
also a line the shell wrapped at the pane's width comes back as one line, a
progress bar that rewrote itself with `\r` reads as its final value, and a
TUI's screen lands where it was drawn. Use it whenever a human would want to
read the output.
Two details about what you get back either way: capture returns a *snapshot*,
not a stream — call it again for a newer one. And by default it prints the
@@ -218,143 +220,35 @@ For something that quick, `--interval 100`, or drop `--changed` and read the
If you want the process tree itself — "what is running in there", "which port is
this pane serving" — that is `tty7 procs %83`: indented by depth, `*` on the
foreground process, then the ports those processes are listening on.
It is not the way to check on a coding agent, though. `procs` reports
`nothing running in this pane` for a pane with a busy agent in it, so reading it
as "the worker died" is wrong. Ask `tty7 agents` about those, or see
[When a worker never moves](#when-a-worker-never-moves).
foreground process, then the ports those processes are listening on. It is not
the way to check on a coding agent, though: `procs` reports `nothing running in
this pane` for a pane with a busy agent in it, so reading it as "the worker
died" is wrong. Ask `tty7 agents` about those.
## Handing work to another agent
Everything above also works when the thing in the pane is a coding agent, and
that is where this stops being a terminal wrapper and starts being useful. An
agent reports its own status, so you can wait on *it* rather than on its
process tree:
A pane can hold another coding agent, and every primitive above works on it —
plus one that only agents have: status hooks report `working` / `waiting` /
`done`, so `tty7 wait` can block on the *agent* rather than its process tree.
```bash
PANE=$(tty7 split --v)
tty7 send "$PANE" 'claude --dangerously-skip-permissions "add tests for the parser"' --enter
tty7 wait "$PANE" --until waiting,done --changed --timeout 900
tty7 capture "$PANE" --plain | tail -40
tty7 pane close "$PANE"
```
**Delegation has a playbook — `references/delegation.md`. Read it before you
spawn a worker.** It covers the whole arc: giving the worker its own git
worktree and workspace, handing the task over with the delivery contract in the
prompt, proving the command actually started, babysitting the states, collecting
the result out of git, fanning out several workers, and cleaning up.
Five steps: give it a pane, hand it the task, sleep until it needs you or
finishes, read what happened, clean up. The third is the one worth
understanding.
Four rules from it survive even if you read nothing else:
### Give the worker its interactive mode
Hand the task as an argument, **not** with `-p`. Both run one turn and stop, so
the difference is not what the worker does — it is what anybody can see while it
does it.
Interactive is the mode that draws a TUI, so the pane fills with the worker's
reasoning and tool calls as they happen. That is visible to the user in their
tty7 window, and it is what `capture --plain` reads back. `-p` is the piped
mode: it draws nothing, streams its answer to stdout when the turn ends, and
until then the pane's screen stays **empty**`capture --plain` on it returns
nothing at all, which reads exactly like a worker that hung. Putting a `-p`
worker in a pane throws away the only reason it is in a pane.
Interactive also leaves the session alive at the prompt, so you can `send` a
follow-up into the same context. A `-p` worker is gone after its one turn.
Reach for `-p` only when you want the answer as a string and nobody needs to
watch — and then prefer `tty7 run` or the Bash tool, which is what that shape
is for.
### What the states mean
| State | The pane is |
|---|---|
| `working` | mid-turn |
| `waiting` | **stopped, needing you** — a permission prompt, a question |
| `done` | finished its turn |
| `idle` | an agent that has not started a turn |
| `free` | no agent: the foreground command exited (see above) |
| `no-agent` | nothing reports status here — a plain shell, or hooks not installed |
| `exit` | the pane is gone; ends every wait whether you asked for it or not |
`--until waiting,done,exit` is the default because those are the three that mean
"your turn again". Note that `idle` is something an agent says about *itself*
a pane running a build is `no-agent`, never `idle`, so `--until idle` is never
the way to ask "is the command finished". That is `free`.
Mixing the two is safe: `--until waiting,done,free` covers a pane whose kind you
don't know, because `free` is only consulted when none of the agent states you
named matched first.
### `--changed` is not optional in a loop
The status is a **level, not an event**: `done` stands until the next turn
begins. So a `wait` issued right after a `send` will happily answer with *last*
turn's `done` before the worker has even read the input, and you will read a
stale screen and think it failed. `--changed` refuses the state the pane was
already in. Every round after the first needs it; the JSON's `stale` flag tells
you when it mattered.
### Answering a prompt
A worker that stops at `waiting` is usually showing something that text cannot
answer — a permission prompt driven by arrow keys, a menu, a TUI. Look first,
then press keys:
```bash
tty7 capture "$PANE" --plain | tail -20 # what is it asking?
tty7 send "$PANE" --key down --key enter # answer it
tty7 send "$PANE" --key C-c # or stop it
```
`--key` takes `enter escape tab backtab space backspace delete up down right
left home end pageup pagedown`, plus `C-<char>` for Ctrl and `M-<char>` for
Alt. Repeat it for a sequence; text and keys compose, text first. This is also
how you interrupt a runaway command in a pane you own — `--key C-c` — which
plain `send` cannot express.
### Running several at once
Panes are independent, so fan out and then collect:
```bash
for task in parser lexer codegen; do
P=$(tty7 split --v)
tty7 send "$P" "claude -p 'add tests for the $task'" --enter
echo "$P" >> /tmp/workers
done
while read -r P; do
tty7 wait "$P" --until done,exit --changed --timeout 1800 || echo "$P did not finish"
tty7 capture "$P" --plain | tail -40
tty7 pane close "$P"
done < /tmp/workers
```
Splitting repeatedly makes the user's window very busy; `tty7 new` gives each
worker its own workspace instead if you would rather not.
### When a worker never moves
A `wait` that times out while `tty7 agents` shows a status that never changes
almost always means the agent's status hooks are missing or out of date — the
worker is fine, it just has no way to say so. `tty7 agents` names the agent when
it can see the gap, and `tty7 doctor` reports where every agent's hooks stand.
Hooks are installed from the GUI's **Settings → Agents**; tell the user rather
than trying to install them yourself.
Before concluding anything, check whether it is moving. Those same hooks emit an
OSC 777 line on every tool call, and `capture` **without** `--plain` shows them —
one of the few times the raw bytes beat the rendered screen:
```bash
tty7 capture "$PANE" | grep -c 'tool-complete' # rising = alive and working
```
That is also the answer when a worker's screen looks empty: a `-p` worker paints
nothing until its turn ends, so `capture --plain` is blank the whole way through
while the event stream underneath is busy. Two things that do *not* answer this
question: `tty7 procs`, which reports nothing running for a pane with a live
agent in it, and the absence of output on a `--plain` capture.
- **Interactive mode, never `-p`.** `claude -p` draws no TUI: the pane stays
blank, `capture --plain` returns nothing, the user watches an empty
rectangle, and the session dies after one turn so you cannot follow up. Hand
the task as an argument to the interactive command instead.
- **A worker that writes files gets its own git worktree.** Two agents in one
checkout trample each other and the user's working tree.
- **Collect results from git, not from the screen.** Tell the worker to commit;
read the diff. A screen is a rectangle and the top of it is gone.
- **After the first send into a new pane, confirm the command left the
prompt** — the swallowed-Enter check above.
## Looking around
@@ -420,8 +314,12 @@ from the GUI.
`ws stop`, `machine connect` and `machine disconnect` exit with a message saying
they're not implemented. Don't build a plan around them.
## Full command reference
## References
`references/commands.md` has every verb, subcommand and flag in one table, plus
the JSON shape each one emits. Read it when you need a verb that isn't above,
or when you're about to parse `--json` output and want to know the field names.
- `references/delegation.md` — the delegation playbook: worktrees, handover,
babysitting, collection, fan-out, cleanup. Read it whenever another agent is
about to do the work.
- `references/commands.md` — every verb, subcommand and flag in one table, plus
the JSON shape each one emits. Read it when you need a verb that isn't above,
or when you're about to parse `--json` output and want to know the field
names.
+287
View File
@@ -0,0 +1,287 @@
# Delegating work to another coding agent
A worker in a pane is a real agent session the user can watch, interrupt, and
take over — that is what makes a pane better than an API call. This playbook is
the arc of one delegation, in order:
1. [Give it a place to work](#1-a-place-to-work) — its own worktree and workspace
2. [Hand over the task](#2-handing-over-the-task) — interactive mode, delivery contract in the prompt
3. [Prove it started](#3-prove-it-started) — the three ways a launched worker silently isn't
4. [Wait, and babysit](#4-wait-and-babysit) — states, `--changed`, answering prompts
5. [Collect through git](#5-collect-through-git-not-the-screen) — the screen is a diagnostic, not a deliverable
6. [Clean up](#6-clean-up)
[Fan-out](#running-several-workers) builds on the same six steps, one worker at
a time.
## 1. A place to work
**A worker that will write files gets its own git worktree.** Two agents in one
checkout — or one agent in the checkout the user is editing — trample each
other: half-written files show up in each other's diffs, builds race, and
`git status` stops meaning anything. The whole industry of parallel-agent
tooling converged on the same answer: one task, one worktree, one branch.
```bash
REPO=/path/to/repo
WT=/tmp/agent-wt/parser-tests
git -C "$REPO" worktree add "$WT" -b agent/parser-tests
read -r WS PANE < <(tty7 new --json "$WT" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["id"], "%%%d" % d["pane"])')
```
`tty7 new` rather than `split` for the same reason as the worktree, one level
up: a workspace of its own keeps the worker out of the user's window layout.
Splitting your own pane is fine for one short-lived worker the user wants to
watch; three splits deep the window is unusable.
Skip the worktree only when the worker will not write: a research question, a
code-reading task, a second opinion. For those, `split` in the current checkout
is fine. And if the user explicitly wants the worker operating on their live
checkout, say what the risk is and do as asked.
## 2. Handing over the task
Hand the task as an argument to the **interactive** command — never `-p`:
```bash
TASK="Add tests for the tokenizer edge cases in src/lexer.rs.
When you are done, commit to the current branch with a message
saying what you did and what you verified. Do not push."
tty7 send "$PANE" "claude --dangerously-skip-permissions \"$TASK\"" --enter
```
Why interactive: both modes run one turn and stop, so the difference is not
what the worker does — it is what anybody can see while it does it.
Interactive draws the TUI, so the pane fills with the worker's reasoning and
tool calls as they happen; that is what the user watches and what
`capture --plain` reads back. `-p` is the piped mode: it draws nothing, the
pane's screen stays **empty** until the turn ends — which reads exactly like a
hung worker — and the session is gone afterwards, so you cannot ask a
follow-up. Putting a `-p` worker in a pane throws away the only reason it is
in a pane. When you want an answer as a string and nobody needs to watch,
that's `tty7 run` or your own Bash tool, not a pane.
Two things belong in every task prompt:
- **The delivery contract.** "Commit to the current branch when done" turns
the result into something git can hand you complete — see
[step 5](#5-collect-through-git-not-the-screen). Without it you are left
reading a 40-line tail of a screen and guessing.
- **The boundaries.** "Do not push", "stay in this directory", "ask before
deleting" — a worker inherits none of your context, only its prompt.
## 3. Prove it started
Three independent ways a worker you just launched is silently not running, and
the checks that catch each:
| Failure | What you see | The check |
|---|---|---|
| A fresh shell swallowed the Enter | command sits on the prompt, never runs | `capture --plain \| tail -3`; still on the prompt → `tty7 send "$PANE" --enter` |
| `tty7 agents` reports stale state | a pane stuck at the prompt can still show `working` | the screen's last lines are the authority, not the status |
| `tty7 procs` says `nothing running` | looks like the worker died | it hasn't — `procs` cannot see agents; ignore it here |
The first check is mandatory after the first `send` into any pane you just
created. Thirty seconds here beats a 900-second `wait` that times out on a
worker that never began.
## 4. Wait, and babysit
```bash
tty7 wait "$PANE" --until waiting,done --changed --timeout 1800
```
### What the states mean
| State | The pane is |
|---|---|
| `working` | mid-turn |
| `waiting` | **stopped, needing you** — a permission prompt, a question |
| `done` | finished its turn |
| `idle` | an agent that has not started a turn |
| `free` | no agent: the foreground command exited |
| `no-agent` | nothing reports status here — a plain shell, or hooks not installed |
| `exit` | the pane is gone; ends every wait whether you asked for it or not |
`--until waiting,done,exit` is the default because those are the three that
mean "your turn again". `idle` is something an agent says about *itself* — a
pane running a build is `no-agent`, never `idle`, so `--until idle` is never
the way to ask "is the command finished"; that is `free`. Mixing agent states
with `free` is safe: `free` is only consulted when no named agent state
matched first.
### `--changed` on every wait that follows a send
The status is a **level, not an event**: `done` stands until the next turn
begins. A `wait` issued right after a `send` will happily answer with *last*
turn's `done` before the worker has even read the input, and you will read a
stale screen and think it failed. `--changed` refuses the state the pane was
already in. Every wait that follows input into the pane needs it; the JSON's
`stale` flag tells you when it mattered. A wait that follows *nothing* — the
harvest round in [fan-out](#running-several-workers) — is the case that must
not use it.
### The babysit loop
`waiting` means the worker stopped for a human. Be that human when you can:
```bash
tty7 capture "$PANE" --plain | tail -20 # what is it asking?
tty7 send "$PANE" --key down --key enter # answer a menu / permission prompt
tty7 send "$PANE" 'yes, use the existing fixture file' --enter # answer a question
```
Then go back to waiting. Answer what the task's boundaries already cover;
anything outside them — a destructive action, a scope change, credentials —
gets reported to the user instead, with the pane id so they can look
themselves. A worker you cannot safely answer is a worker the user takes over;
that handover is a feature, not a failure.
To stop a runaway worker: `tty7 send "$PANE" --key C-c` — typing `^C` as text
arrives as two harmless characters.
Exit codes from `wait`: `0` a state you asked for was reached, `124` timeout
(the `timeout(1)` convention — "not yet", distinguishable from broken), `1`
the pane died.
## 5. Collect through git, not the screen
The delivery contract from step 2 pays off here:
```bash
git -C "$WT" log --oneline main..HEAD # what it says it shipped
git -C "$WT" diff main...HEAD # the changes themselves
git -C "$WT" status --short # anything it left uncommitted
```
Complete, unwrapped, with nothing scrolled away — and reviewable before a
single byte reaches the user's branch. You are the merge point: read the diff,
run the tests if the task warranted them, then merge or report.
`capture --plain | tail` is for *diagnosis* — what is it stuck on, what did it
just print — not for collecting results. A screen is a 120-column rectangle
and the interesting part has usually scrolled off the top of it.
If the worker finished but committed nothing, the screen is where you find out
why; that is the one time the tail is the deliverable.
## 6. Clean up
```bash
tty7 ws rm "$WS" # hangs up the workspace's panes
git -C "$REPO" worktree remove "$WT" # refuses if dirty — that's a feature
git -C "$REPO" branch -D agent/parser-tests # once merged, or rejected
```
`worktree remove` refusing means uncommitted work is sitting there — look at
it before deciding anything, and ask the user rather than `--force`-ing away
changes you have not read. A worker you opened with `split` instead is just
`tty7 pane close "$PANE"`.
## Running several workers
Fan-out is the six steps above per worker — **each with its own worktree and
branch** — plus a harvest loop that no single stuck worker can stall:
```bash
: > /tmp/agent-workers
for task in parser lexer codegen; do
WT=/tmp/agent-wt/$task
git -C "$REPO" worktree add "$WT" -b "agent/$task"
read -r WS PANE < <(tty7 new --json "$WT" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["id"], "%%%d" % d["pane"])')
tty7 send "$PANE" "claude --dangerously-skip-permissions \"Add tests for the $task. Commit to the current branch when done; do not push.\"" --enter
echo "$task $PANE $WT $WS" >> /tmp/agent-workers
done
# prove each one started (step 3) before settling in to wait
cp /tmp/agent-workers /tmp/agent-pending
while [ -s /tmp/agent-pending ]; do
: > /tmp/agent-still
while read -r task PANE WT WS; do
tty7 wait "$PANE" --until waiting,done --timeout 120; rc=$?
if [ $rc -eq 0 ]; then
: # done → collect (step 5); waiting → babysit (step 4), then requeue
elif [ $rc -eq 124 ]; then
echo "$task $PANE $WT $WS" >> /tmp/agent-still # not yet — come back
else
echo "$task: pane $PANE is gone" >&2 # 1: died; nothing to requeue
fi
done < /tmp/agent-pending
mv /tmp/agent-still /tmp/agent-pending
done
```
The short per-worker timeout is the point: with one long `wait` per worker in
sequence, the first stuck worker blinds you to every worker behind it. Round
trips of 120 seconds keep you circulating — collecting the finished, answering
the stuck, and telling the user about the one that has moved nothing for three
rounds.
No `--changed` here, unlike step 4, and that is deliberate: a worker that
reached `done` while you were waiting on a *different* one is already standing
in that state when its own `wait` finally starts, and `--changed` would refuse
it — every round, forever. Each pane runs one turn, so a standing `done` is
this turn's. What that costs you is on the other side: after answering a
`waiting` worker, give it a `tty7 wait "$PANE" --until working --changed
--timeout 30` before you requeue it, or the next round hands you the same
prompt again.
What not to build: workers do not talk to each other, and their branches never
merge into each other. Keep the topology a star — you hand out tasks that do
not overlap, each worker delivers to its own branch, and every diff comes back
through you, serially. Cross-cutting conflicts between two workers' branches
are yours to resolve at merge time, which is exactly why the tasks should not
overlap in the first place.
## When a worker never moves
A `wait` that times out while `tty7 agents` shows a status that never changes
almost always means the agent's status hooks are missing or out of date — the
worker is fine, it just has no way to say so. `tty7 agents` names the agent
when it can see the gap, and `tty7 doctor` reports where every agent's hooks
stand. Hooks are installed from the GUI's **Settings → Agents**; tell the user
rather than trying to install them yourself.
Before concluding anything, check whether it is moving. Those same hooks emit
an OSC 777 line on every tool call, and `capture` **without** `--plain` shows
them — one of the few times the raw bytes beat the rendered screen:
```bash
tty7 capture "$PANE" | grep -c 'tool-complete' # rising = alive and working
```
Two things that do *not* answer this question: `tty7 procs`, which reports
nothing running for a pane with a live agent in it, and an empty
`capture --plain` — a worker mistakenly launched with `-p` paints nothing all
turn while the event stream underneath is busy.
### A stuck `working` can be an aborted turn
Hooks report turn boundaries, so a turn that dies without one — an API error,
a dropped connection — leaves the status standing at `working` forever, and
your `wait` sleeps through it. The screen is where the truth is: capture the
tail and look for an error line (`API Error: Connection closed mid-response`
and its relatives) sitting above the input box.
The recovery is the reason workers run in interactive mode: the session is
still alive at its prompt, with all its context and any half-made edits
intact. Tell it to pick the work back up —
```bash
tty7 send "$PANE" 'Your last turn was cut off by an API error. Continue and finish the task as instructed.' --enter
```
— and go back to waiting.
### Tail enough lines to see the spinner
A TUI keeps its input box at the **bottom** of the screen, so a short tail —
`tail -5` — shows an empty prompt box whether the worker is idle or three
files deep in an edit: the line that distinguishes them is the spinner/elapsed
line a dozen rows up, and a short tail cuts it off. `tail -15` or more
whenever the question is "is it doing anything", and read for the spinner, not
the prompt. "The bottom looks like a prompt" is evidence about a *shell* pane
(step 3's launch check); on a TUI pane it is what the screen always looks
like.