Orchestrating agents
One agent opening a pane for another, waiting on it, and reading the result.
Once an agent's status is a thing a program can ask about, one agent can run another. Nermalgives that loop a primitive instead of leaving it to screen scraping.
The loop
# 1. give the worker a pane
PANE=$(Nermalsplit --v)
# 2. hand it a task
Nermalsend "$PANE" 'claude -p "add tests for the parser"' --enter
# 3. sleep until it needs you or finishes
Nermalwait "$PANE" --until waiting,done --changed --timeout 600
# 4. read what happened
Nermalcapture "$PANE" --plain
# 5. clean up
Nermalpane close "$PANE"That is the whole shape. The interesting step is the third.
Nermalwait
Nermalwait [%PANE] [--until STATE,…] [--changed] [--timeout SECS] [--interval MS]Blocks until the pane reaches one of the states you named.
| Flag | Default | |
|---|---|---|
--until | waiting,done,exit | Which states end the wait — see below |
--changed | off | Ignore the state the pane was already in — only wake on one it moved into after the wait began |
--timeout | none | Give up after this many seconds, exiting 124 |
--interval | 500 ms | How often to poll |
Exit codes are made for scripts:
| Code | Meaning |
|---|---|
0 | A state you asked for was reached |
124 | Timed out — the timeout(1) convention, so "not yet" is distinguishable from "broken" |
1 | The worker died first; the JSON says "status": "exit" |
The reply carries the agent's own message and its native session id, so a wake-up is directly actionable.
The states
Four of them are the agent's own status. The other three are about the pane, because not everything worth waiting on is an agent:
| State | Means |
|---|---|
idle working waiting done | What the agent's hooks last reported |
no-agent | Nothing reports status in this pane — a plain shell, or an agent whose hooks are missing |
free | The foreground command has exited; the pane is back to its bare shell |
exit | The pane is gone. Ends every wait, whether you asked for it or not |
Waiting on a command instead of an agent
An agent says when it is done. A cargo test does not — so for a plain pane the
question is whether anything is still running in front of the shell, which is
what free answers:
Nermalsend "$PANE" 'cargo test > /tmp/t.log 2>&1; echo $? > /tmp/t.rc' --enter
Nermalwait "$PANE" --until free --changed --timeout 900
cat /tmp/t.rc /tmp/t.logfree costs one extra request per poll, so it is only checked when you name it —
and only once the agent ladder has not already answered, so pairing it with
waiting,done never costs you a state you asked for.
free is read off the process tree, which has two blind spots. A pane whose
own root process is the command — what Nermalrun spawns — looks free the
whole time it runs; wait on Nermalrun itself instead, it already blocks. And
a backgrounded job (… &) keeps the pane busy after the foreground command
has finished.
A pane with no agent reports no-agent, not idle. That distinction is
why --until idle cannot be used to mean "the command finished" — idle is a
thing an agent says about itself, and a busy shell never says it.
Why --changed matters
The status the server keeps is a level, not an event. done stands until
the next turn begins; waiting stands until the agent moves again.
So a wait issued immediately after a send can answer with the previous
turn's state, before the worker has even read the input. --changed refuses the
state the pane was already in, which is what every round after the first needs.
Without it, the JSON's stale flag tells you whether that happened.
free has the same problem and a different fix: a shell that goes free → busy →
free ends up where it started, so there is no new state to compare against.
There --changed means "something ran while I was watching", which is exactly
what you want in the line after a send.
That does mean a command which starts and finishes between two polls is never
seen running, and the wait sits there until it times out. If the thing you are
waiting on can be that quick, poll faster (--interval 100) or drop --changed
and let a sentinel file carry the answer. The timeout says as much when it
happens.
Answering a prompt
A worker that stops at waiting is usually showing something that keystrokes,
not text, are the answer to — a permission prompt driven by the arrow keys, a
menu, a TUI to be dismissed. send --key presses keys:
Nermalwait "$PANE" --until waiting --changed # it needs something
Nermalcapture "$PANE" --plain | tail -20 # see what it is asking
Nermalsend "$PANE" --key down --key enter # answer it
Nermalsend "$PANE" --key C-c # or stop it altogetherKeys are delivered as separate events 200 ms apart, so a raw-mode TUI reads a sequence as a sequence rather than as a paste. The full vocabulary is in the reference.
When an agent never moves
If Nermalwait times out and Nermalagents shows a status that never changes,
the likely cause is that the agent's status hooks are not installed or are out
of date — the agent is working fine, it just has no way to say so. Nermaldoctor
reports where every agent's hooks stand, and Nermalagents names the specific
one when it can see the gap.
Watching everything at once
Nermalagents # every agent on the machine: pane, agent, status, message
Nermalagents --json # the same, parseableIf you are an agent yourself, you are in that list too.
Teaching an agent to do this
Nermalinstalls nothing into ~/.claude for it — no switch in Settings →
Agents writes a skill, and none ever will. What the agent needs to know ships
in the repository instead, as a skill you install yourself:
npx skills add Nermalcat69/Nermal-ideThat covers the pane-driving half — where it is, how to open a pane, send into
one, read one back, and the rules below — see the agent
skill. The wait step is documented on this page.
A skill rather than a global instruction, on purpose: only its one-line description rides in context until something reaches for it, so an agent that never touches another pane pays nothing for it.
Rules of the road
The panes on a machine are somebody's real work, and some of them are other agents mid-task. Treat anything you did not create as read-only.
- Never
sendinto a pane you did not open. CheckNermalagentsfirst. - Never close a pane, tab, or workspace you did not create.
- Never
server stoporserver restart. Every pane on the machine dies with it. - Clean up what you did create —
Nermalpane close %83when you are done. An interruptedrunleaves its pane behind;Nermalpane ls --allshows those, andNermalpane close --orphansclears them. That last one is a human's broom, not an agent's: it closes every orphan on the machine, including ones somebody else abandoned mid-command.
The full agent-facing contract is in the skill.