Nermal
Coding agents

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.

FlagDefault
--untilwaiting,done,exitWhich states end the wait — see below
--changedoffIgnore the state the pane was already in — only wake on one it moved into after the wait began
--timeoutnoneGive up after this many seconds, exiting 124
--interval500 msHow often to poll

Exit codes are made for scripts:

CodeMeaning
0A state you asked for was reached
124Timed out — the timeout(1) convention, so "not yet" is distinguishable from "broken"
1The 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:

StateMeans
idle working waiting doneWhat the agent's hooks last reported
no-agentNothing reports status in this pane — a plain shell, or an agent whose hooks are missing
freeThe foreground command has exited; the pane is back to its bare shell
exitThe 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.log

free costs one extra request per poll, so it is only checked when you name it — and only once the agent ladder has not already answered, so pairing it with waiting,done never costs you a state you asked for.

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 altogether

Keys are delivered as separate events 200 ms apart, so a raw-mode TUI reads a sequence as a sequence rather than as a paste. The full vocabulary 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, parseable

If 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-ide

That covers the pane-driving half — where it is, how to open a pane, send into one, read one back, and the rules below — see the agent skill. 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 send into a pane you did not open. Check Nermalagents first.
  • Never close a pane, tab, or workspace you did not create.
  • Never server stop or server restart. Every pane on the machine dies with it.
  • Clean up what you did create — Nermalpane close %83 when you are done. An interrupted run leaves its pane behind; Nermalpane ls --all shows those, and Nermalpane close --orphans clears them. That last one is a human's broom, not an agent's: it closes every orphan on the machine, including ones somebody else abandoned mid-command.

The full agent-facing contract is in the skill.

Sponsor