kolu — drive one agent from another through its terminal
SkillDocs & knowledgeDrive one AI agent from another through kolu's terminals: spawn a Claude Code / Codex / opencode session in a PTY, prompt it, watch the screen for its reply, read it, and prompt again — a create→send→snapshot loop run with the `kaval-tui` CLI directly (no MCP). `kaval-tui` writes input and reads scrollback; `pulam-tui` adds a precise agent-state done-signal when you drive hooked terminals. Triggers on "drive another agent", "send a prompt to a terminal agent", "have one agent prompt another", "agent drives agent", "orchestrate agents in terminals", "make Claude drive Codex", "prompt the agent running in that terminal", or wiring a loop where one coding agent supervises another.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the kolu — drive one agent from another through its terminal skill
What this skill tells your AI
The instructions your AI receives, as published by srid/emanote in .agents/skills/kolu/SKILL.md and read by ahel’s review.
You can run a coding agent (Claude Code, Codex, opencode) inside a kolu-owned
PTY and steer it from the outside: type a prompt, submit it, watch the screen
until it's done, read what it said, type the next prompt. The whole toolkit is
kaval-tui — write input, read the screen, spawn, kill. The driver runs it
directly; there's no server to stand up and no MCP layer.
pulam-tui adds a precise done-signal (wait --until <state>) — but only for
hooked terminals (see the last section). For a raw kaval-tui-spawned
terminal, the done-signal is watching the screen settle, below.
The loop
id=$(kaval-tui create --json -- claude | jq -r .id) # spawn the inner agent
kaval-tui send "$id" "refactor the parser to use a lexer" # 1. TYPE the prompt
kaval-tui send "$id" --key Enter # 2. SUBMIT it (its own step)
wait_until_settled "$id" # 3. let its turn finish (below)
kaval-tui snapshot "$id" --viewport # 4. read the screen
kaval-tui send "$id" "now add tests for it"; kaval-tui send "$id" --key Enter # loop
Leaf commands, all kaval-tui: create (spawn) · send (type) ·
send --key Enter (submit) · snapshot (read) · kill. Typing and
submitting are two separate sends — that is load-bearing, see below.
Read with
snapshot --viewport, not| tail. A baresnapshotprints the whole scrollback — thousands of lines on a long-running or compacted agent — sosnapshot | tail -8hands you the bottom of the buffer (often just trailing blanks), not the live screen.--viewportasks the daemon for just its terminal's last screenful — the right "what's on screen now" read, and correct regardless of how tall your own shell is (over--hostthe remote terminal is a different size).--tail N(alias--lines N) bounds it to the last N lines when you want a fixed slice.
kaval-tui send — type, then submit (two steps)
kaval-tui send <id> [text...] writes input to the terminal — exactly the text
(and any --keys) you pass, with NO implicit Enter. It types; it does not
submit. Submitting a prompt is its own second send:
kaval-tui send "$id" "fix the failing test in parser.ts" # 1. type the prompt
kaval-tui send "$id" --key Enter # 2. submit it
Do this as two separate send commands — not send "text" --key Enter in one
call. The separation is load-bearing: an Enter sent in the same breath as the
text races Claude Code's bracketed-paste / debounced input handling — it arrives
before the pasted text has registered and is silently dropped, leaving the
prompt staged on the ❯ line while send reports success. A standalone
follow-up send --key Enter lands after the text has settled, so it actually
submits. (If a turn never seems to start, this is the #1 cause — snapshot and
look for the prompt sitting unsent on the ❯ line.)
Specifics:
- Multiline prompts and piped stdin go as one bracketed paste, so they land
in the input box as a block instead of submitting line-by-line. Automatic
(
--paste/--no-pasteforce it). For a big prompt, pipe it —cat task.md | kaval-tui send "$id"— thensend "$id" --key Enter. --key <name>(repeatable, sent after the text) is both the submit channel (Enter) and the control channel:Escape,C-c,Enter,Up/Down/Left/Right,Tab,Home,End,Backspace,M-<char>.--json→{ id, bytes, paste, keys }to confirm what was written.
send is blind — it writes whether or not the agent is ready for input.
Always pair it with snapshot so you don't fire a prompt into a not-yet-ready
session (e.g. before the TUI has drawn its input box, or over a trust prompt).
Interrupt a runaway before redirecting it:
kaval-tui send "$id" --key Escape # stop Claude Code mid-stream
kaval-tui send "$id" --key C-c # SIGINT whatever's running
The done-signal — watch the screen settle
After you submit, you need to know when the turn ends. For a raw
kaval-tui-spawned terminal there's no agent-state feed, so use the screen
itself: poll snapshot until it stops changing. A working agent streams
output, so a screen that's held still for a couple of polls means the turn ended
— it finished, or it's blocked asking you something (both mean "your move").
Then read the snapshot to see which, and respond.
# Block until <id>'s screen is unchanged across 2 polls, capped at a deadline so
# a wedged agent can't hang the loop forever (the manual equivalent of a timeout).
wait_until_settled() {
local id=$1 deadline=$(( $(date +%s) + 600 )) prev="" cur stable=0
while [ "$stable" -lt 2 ] && [ "$(date +%s)" -lt "$deadline" ]; do
sleep 3
cur=$(kaval-tui snapshot "$id" --viewport) # diff the live screen, not the whole scrollback
if [ "$cur" = "$prev" ]; then stable=$((stable + 1)); else stable=0; fi
prev=$cur
done
}
Poll --viewport, not a bare snapshot: a settle test diffs two reads, and
diffing two full-scrollback dumps is slow and noisy (any scrollback churn reads
as "still moving"), while two screenfuls compare cleanly.
Tune sleep/deadline to the work. It's coarser than pulam-tui wait (a busy
agent that pauses mid-thought can read as settled), so after it returns,
confirm the reply is actually present in the snapshot before moving on.
pulam-tui wait — the precise done-signal (hooked terminals only)
When you do have agent-state detection, pulam-tui wait <id> --until <buckets>
is the exact done-signal — it blocks until the agent reaches a coarse state, then
exits 0:
working— busy (thinking/tool_use/ background task).awaiting—awaiting_user: it's asking you a question.waiting— the just-finished post-turn lull.
awaiting and waiting both mean "your move", so --until awaiting,waiting
catches a turn ending; --timeout <ms> fails loud (exit 2) so a wedged agent
can't hang the loop; if the terminal exits before reaching the state, wait
fails loud too (exit 3 — the agent you were driving died); --json →
{ id, agent }.
Mind the stale-state race — wait in two phases.
waitmatches the agent's state the instant it connects, replaying whatever it is right now. So right after asend, the agent may still report the previous turn'swaiting/awaitingfor a beat before it picks up the new prompt — and a lonewait --until awaiting,waitingwould return immediately on that stale state, before the turn you asked for has even begun. For a robust loop, wait for the pickup first, then the turn-end:kaval-tui send "$id" "fix the parser"; kaval-tui send "$id" --key Enter pulam-tui wait "$id" --until working # 1. it picked up the prompt pulam-tui wait "$id" --until awaiting,waiting # 2. its turn ended
Caveat — agent state needs HOOKED terminals. Detection keys on kolu's shell rc-hooks (the OSC marks a terminal emits as commands run).
kaval-tui createis the raw multiplexer — a plain$SHELL, no hooks by design — so an agent you spawn that way often isn't detected, andwaitwill just time out.waitis reliable when you drive already-hooked terminals: the ones a running kolu-server spawned (pointkaval-tui --socket $XDG_RUNTIME_DIR/kolu/pty-host.sockat them), or a futurekolu-tui. For a rawkaval-tui createloop, use the screen-settle done-signal above.
Reach — which daemon you're driving
Bare kaval-tui autodiscovers a running daemon on this machine. Two ways to
point it elsewhere:
--socket <path>targets a specific local daemon — e.g. a running kolu-server's kaval ($XDG_RUNTIME_DIR/kolu/pty-host.sock), to drive the terminals you have open in kolu (these ARE hooked, sopulam-tui waitworks against them once apulamreads that kaval).--host <ssh>reaches a daemon on another machine (provisioned with Nix); a remote PTY survives the link.
Acceptance
Before calling a driven turn done:
- You submitted with a separate
send --key Enter(not an implicit Enter, notsend "text" --key Enterin one call) — a prompt left staged on the❯line is the #1 failure here. - The inner agent's reply is actually in the
snapshot— not an empty box or a half-rendered stream. The screen-settle wait is coarse; verify the content. - Your wait had a deadline /
--timeoutso a wedged agent fails instead of hanging the loop. - If the screen settled on a question (the agent is awaiting you), you read it and answered — you didn't send the next task on top of a blocked prompt.
Signals
- GitHub stars
- 959
- Forks
- 79
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
kolu-srid- Source
- github.com/srid/emanote