Drive 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",...
Scanned 9/12/2026
Install to Claude Code
npx -y skills add srid/emanote --skill kolu --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Kolu?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/srid-kolu)More formats (shields.io, HTML) on the badges page.
---
name: kolu
description: >-
Drive 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.
---
# kolu — drive one agent from another through its terminal
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
```sh
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 `send`s** — that is load-bearing, see below.
> **Read with `snapshot --viewport`, not `| tail`.** A bare `snapshot` prints the
> **whole scrollback** — thousands of lines on a long-running or compacted agent —
> so `snapshot | tail -8` hands you the bottom of the buffer (often just trailing
> blanks), not the live screen. `--viewport` asks 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 `--host` the 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 `--key`s) you pass, with NO implicit Enter**. It types; it does not
submit. **Submitting a prompt is its own second `send`:**
```sh
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-paste` force it). For a big prompt, pipe it —
`cat task.md | kaval-tui send "$id"` — then `send "$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:
```sh
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.
```sh
# 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.** `wait` matches the agent's
> state **the instant it connects**, replaying whatever it is right now. So right
> after a `send`, the agent may still report the *previous* turn's
> `waiting`/`awaiting` for a beat before it picks up the new prompt — and a lone
> `wait --until awaiting,waiting` would 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:
>
> ```sh
> 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 create`
> is the **raw** multiplexer — a plain `$SHELL`, no hooks by design — so an agent
> you spawn that way often isn't detected, and `wait` will just time out. `wait`
> is reliable when you drive **already-hooked** terminals: the ones a running
> **kolu-server** spawned (point `kaval-tui --socket $XDG_RUNTIME_DIR/kolu/pty-host.sock`
> at them), or a future `kolu-tui`. For a raw `kaval-tui create` loop, 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, so `pulam-tui wait` works
against them once a `pulam` reads 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,
not `send "text" --key Enter` in 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 / `--timeout`** so 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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!