mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
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:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user