zirvDocs

DocsSupervision (zirv ctx)

Interactive use

README.md, lines 6510–6641View source
bash
alias claude='zirv ctx wrap -- claude'

The wrapped session is byte-for-byte identical to an unwrapped one until an intervention, injection happens only at a turn boundary while you are idle, and any supervision failure drops it back to pure passthrough. Flags you wrap are kept across a restart, so zirv ctx wrap -- claude --model opus comes back as an opus session.

wrap types agent-specific text into the session it supervises (/compact, /exit), so it has to know which agent it is driving. It recognises a command whose program is named claude; anything else — a wrapper script, npx claude, a differently named binary — needs --agent claude to say so explicitly, or it refuses rather than typing claude syntax into a program that may not understand it. --no-supervise and --simple are exempt, since neither injects anything:

bash
alias claude='zirv ctx wrap --agent claude -- my-claude-wrapper.sh'

Note that a restart relaunches the adapter's program, so a wrapped wrapper script comes back as a bare claude.

wrap learns the transcript path from the turn signals the Stop hook sends, and forgets it on a restart because the fresh session writes a new file. Set ZIRV_CTX_TRANSCRIPT to pin a path instead; it outranks every signal and survives restarts, which is what you want when the agent's hook cannot report one, and what tests use.

Inline supervision#

A supervised run is either a dashboard pane — visible in a sidebar, its result mailed back — or an inline run in the terminal that started it. Those are the only two shapes; there is no invisible stdout-captured child.

zirv ctx exec and zirv ctx loop are always inline: they supervise a child in this terminal, by definition. zirv agent chooses: if any dashboard is live on this machine it asks that dashboard for a pane — preferring the one you were launched from, then one already hosting this repository, then any live one — and prints the pane's short id. If none is live it says so in one line and supervises the child right here:

text
zirv ctx agent: no live dashboard -- running codex inline in this terminal

It never refuses, and it never launches a dashboard of its own. A pane carries everything a delegation asks for except a tool-call ceiling (no verified counter) and trailing -- <flags> beyond a --model pin (they would become argv on the pane's own harness child, and a spawn request is untrusted data); both are announced on stderr rather than dropped silently. --timeout-secs and --max-restarts are honoured either way.

A dashboard pane gets a short kebab-case name (at most 24 characters), shown in the pane bar and sidebar, the agent tree and its SELECTED panel, NEEDS YOU cards, the activity log, zirv ctx status and the From-session line of the mail it sends. --name <label> sets it; without it one is derived from the first words of the prompt, never worker. A clash with an existing worker gets a numeric suffix. zirv ctx send --to-session and nudge accept the name as well as the short id; a name two sessions share is an ambiguity error. The role is a separate field and is still shown. Inline (non-pane) runs are unnamed.

bash
zirv ctx exec --prompt "$PROMPT" -- claude -p "$PROMPT" --session-id "$SID"

--prompt is what a restart re-sends; without it (and without a -p, --print or exec argument that zirv can read the prompt out of), a rot verdict ends the run instead of restarting it. --session-id names the session, and --transcript points at the first child's transcript when the adapter cannot derive it. Both describe the first child only: every restart is a new session whose transcript path is derived again.

Passing the whole command is optional. With --prompt and nothing after -- that names a program, exec builds the launch from the adapter itself — the same way every restart does — so the prompt never has to be encoded into argv and read back out:

bash
zirv ctx exec --agent claude --prompt "$PROMPT"
zirv ctx exec --agent claude --prompt "$PROMPT" -- --model opus  # extra flags

This is what a YAML agent step uses, and it is why a prompt that happens to begin with - or to look like a flag is still just a prompt.

Exit codes for supervised runs#

<!-- zchk-doc-exit-codes:start -->
CodeMeaning
the child's own codethe run finished on its own
75rot was detected and exec could not carry on, either because the restart budget was spent or because no prompt was available to restart with. loop also returns it when consecutive cycle failures hit max_failures
76the same, for a wall-clock timeout rather than rot
77the token or tool-call budget was reached; the run checkpoints and stops without restarting
78provider capacity or overload errors persisted until the restart budget was spent
79the provider account ran out of credits or quota; the run stops without retrying
80a writing delegation was refused because the tree already has a live writer; retry after it finishes or use --worktree
81progress stalled after a steering nudge and its grace period, and the restart budget was spent
82the worker report failed its result contract or claimed deliverables that do not exist
<!-- zchk-doc-exit-codes:end -->

The code names the reason, not which limit ran out: 75 means rot and 76 means timeout, whether the run stopped because the budget was exhausted or because there was no prompt to restart with. A usage-limit hit is neither, it parks and relaunches without consuming the restart budget.

Migrating an existing loop#

Replace a long-lived orchestrator session with a stateless loop, and wrap worker dispatch so individual runs get restarted rather than merely killed:

yaml
# .zirv/commands/issue-loop.yaml
name: Issue Loop
commands:
  - command: zirv ctx loop --prompt-file .zirv/issue-loop-prompt.md --interval-secs 900
bash
zirv ctx exec --prompt "$WORKER_PROMPT" -- claude -p "$WORKER_PROMPT" --session-id "$SID"

Durable state must live outside the session (GitHub issues and labels, for example), because every cycle starts with a clean context. Once zirv ctx hook stop is registered, remove any older canary Stop hook from ~/.claude/settings.json: two Stop hooks scoring the same session is noise, and the older one blocks stops, which this one deliberately never does.