diff --git a/docs/cli/agent-skill.mdx b/docs/cli/agent-skill.mdx index 4aa321b5..09fcaaa2 100644 --- a/docs/cli/agent-skill.mdx +++ b/docs/cli/agent-skill.mdx @@ -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. 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 diff --git a/skills/tty7/SKILL.md b/skills/tty7/SKILL.md index 36c23f67..40ead057 100644 --- a/skills/tty7/SKILL.md +++ b/skills/tty7/SKILL.md @@ -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-` for Ctrl and `M-` +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-` for Ctrl and `M-` 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. diff --git a/skills/tty7/references/delegation.md b/skills/tty7/references/delegation.md new file mode 100644 index 00000000..1e9b13b8 --- /dev/null +++ b/skills/tty7/references/delegation.md @@ -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.