DocsSupervision (zirv ctx)
zirv chat and zirv agent
zirv chat and zirv agent are shorter top-level aliases for zirv ctx chat and zirv ctx agent. Both are reserved command names, compared
case-insensitively (see Reserved Command Names),
so a script or shortcut can never shadow them, and — unlike the
bare-invocation alias above — an explicit zirv chat always starts a
session regardless of the local-.zirv/terminal checks bare zirv applies.
zirv ctx chat --help (and zirv chat --help) prints Usage: zirv ctx chat... even when reached through the zirv chat alias — a cosmetic
side effect of the alias reusing zirv ctx's own clap tree rather than
having a separate one, not a bug in the alias routing itself.
zirv chat— the same interactive orchestrator session the bare invocation starts.zirv agent <name> <prompt> [-- flags]— delegates one task to a supervised worker on another enabled harness: the same pacing, rot detection and restart-with-handoff behaviorzirv ctx execgives a hand-written invocation, as one command. It lands in a dashboard pane whenever a dashboard is live on this machine, and runs inline in this terminal (announced in one line) when none is. Pass-as the prompt to read it from stdin instead.
Delegation receipt (--json)#
--json prints one machine-readable receipt to stdout instead of the human
lines, so a scripted orchestrator does not have to scrape prose:
zirv agent codex "fix the failing test" --json{
"schema_version": 1,
"harness": "codex",
"model": "gpt-5.6-terra",
"mode": "inline",
"state": "reported_validated",
"exit_code": 0,
"session": "abcd1234",
"workdir": "/repo",
"result_path": "/state/logs/delegation-results/<session>.json",
"report_truncated": false,
"mail_delivered": true,
"note": "full report stored at result_path"
}Every optional field is omitted rather than written as null or [] when
it has nothing to say: model (unresolved), exit_code (no result yet),
session, task, workdir, result_path (nothing persisted), reason
(no launch failure), and errors/capability_warnings (empty). Exit codes
are unchanged by --json; it only changes what reaches stdout, and stderr
notices still print normally.
mode—dashboard_pane(a live dashboard admitted or fulfilled the request) orinline(this process supervised the worker itself).state—launched(a pane was admitted/claimed; nothing has run yet — checkzirv ctx inboxlater for the worker's own report),launch_failed(refused, or the worker process never started at all — seereason),exited_no_report(the process exited but no final assistant text could be extracted — treat the task as unverified; a post-mortem record still lands atresult_path, with an empty report),reported(final text extracted, no--result-schema/--result-kindcontract declared),reported_validated(a declared contract was satisfied),reported_contract_failed(a declared contract failed even after the one bounded retry — seeerrors).result_path— where the worker's own record was persisted, when one was; on the harness runtimeexited_no_reportalways gets one too (outcome: "exited_no_report", no report text) so a clean exit with nothing usable is never left with no durable trace (see Sending mail between sessions below for the file's own shape).
Only zirv ctx agent/zirv agent (a one-shot delegation) has --json today
— zirv ctx exec/zirv ctx loop do not.
Nested sessions are refused#
zirv chat (and zirv ctx wrap) refuse to start when they can tell they are
already running inside an agent session — ZIRV_CTX_SESSION or
ZIRV_CTX_SOCKET is set, or Claude Code's own CLAUDE_PID+CLAUDECODE
pair is:
zirv ctx chat: refusing to start inside an existing agent session
(ZIRV_CTX_SESSION=abcdef12). A nested interactive session can post turn
signals into the outer supervisor and get the outer session compacted,
restarted or killed. Run it from a plain terminal, or pass --allow-nested
(or set ZIRV_ALLOW_NESTED=true) to override.This is not a tidiness rule. A nested interactive supervisor shares the outer
session's console, and if its own turn-signal socket fails to bind, its child
would report turn boundaries into the outer supervisor's rot engine —
which eventually verdicts a restart and ends the session the human was
actually talking to. Pass --allow-nested, or set ZIRV_ALLOW_NESTED=true,
if you mean it.
The delegating verbs — zirv ctx exec, zirv ctx loop and zirv agent —
are deliberately not gated: delegating a task to a worker from inside a
session is exactly what they are for, and a worker never takes the shared
console over. Each of them still scrubs ZIRV_CTX_SESSION,
ZIRV_CTX_SOCKET and ZIRV_CTX_TRANSCRIPT off every child it launches
before setting its own, so a worker can never inherit another session's
identity.
The dashboard: multiple sessions in one terminal#
On a real, large-enough terminal (at least 80x20 — a taller floor than
wrap's), zirv chat (bare zirv included) opens a session multiplexer
instead of a single wrapped session: a dashboard process owning several
interactive sessions at once, each a supervised PTY child (ConPTY on Windows,
a native PTY elsewhere) rendered through its own embedded terminal-screen
model, behind a persistent header and sidebar. Too small a terminal falls back
to the single-pane wrap session instead, with a one-line notice naming the
floor; --simple skips the dashboard entirely.
The first pane is always the orchestrator you are talking to. Further panes
come from the s (spawn) dashboard command, or from any zirv ctx agent
invocation on this machine — inside one of the dashboard's own panes, or from
a plain terminal that simply found this dashboard live — which asks the
dashboard to open a fresh pane rather than supervising the child itself. That
is an untrusted request the dashboard re-validates against live configuration
(pane cap, adapter gate, working-directory match) before honoring it, never
treated as authority on its own; a request that arrives through the drop
directory also has its widening fields (force, trailing harness flags)
stripped before anything reads them. Every live pane and its role are tracked in the same
session registry zirv ctx status reports (see Session registry and
nudging above), so zirv ctx nudge/zirv ctx send --to-session can address one pane directly.
All dashboard keybindings live behind one Ctrl+A prefix: digits 1-9
switch panes, Tab/arrows navigate, s/n/m open spawn/nudge/mail
overlays, o opens the handover picker (swap the focused pane's model or
harness in place — see Cross-harness fallback and
handover below), z zooms the focused
pane, b shows/hides the session column (it hides itself below 100 total
columns; b forces it back regardless of width, or hides it again), e
shows recent errors, t switches to the agent tree (below), ?/h shows
help, and q quits. On quit, the dashboard writes a restore roster so a next
launch can offer to reopen the same panes.
Ctrl+A t (also in the palette and the help overlay) swaps the dashboard for
the orchestrator dashboard (issues #833, #843) and back. The classic
dashboard stays the default view and panes keep running and receiving output
underneath; the toggle keeps the selection and focus, and while the orchestrator
dashboard is shown unprefixed keys and the mouse never reach a hidden pane. It
is a dashboard about the session, not a chat window: no chat is visible until you
open an agent, and it answers what is going on, how far the workflow has got and
how the agents work together.
From 100 columns and 35 rows up it has these panels, top to bottom. It is drawn
in a fixed palette with real panel backgrounds and rounded borders (24-bit
colour when COLORTERM is truecolor or 24bit, else the nearest of the 256
colours, decided once) and never uses the terminal's dim attribute:
- Header:
zirv <repo> · seat <model> · <role>; on the right a breathing⚑ N need youbadge or✓ all clear, the seat harness'susage 5hreading (the sidebar's own cached fact, never computed again; an unknown reading is left out), and? keys. No dashboard shows a USD figure. - Workflow stepper:
WORKFLOW <pack> · <task>, each step✓done, a turning spinner on the current one or○to do, joined by━━━after a done step (a light runs along them) and───after the others, withstep i/n · <elapsed> · next gate <gate>on the right. The steps win over the title, which is cut at a word. With no active workflow it saysNo workflow running. - FLOW: the seat card with its counts, the Jev box (its three newest
decisions with site, verdict, a confidence bar,
sureorunsureand age; the seat's line runs through it;jev offonly when no[jev]site is on and the harness proxy does not use TypeSafe), a bus, and one 11-row card per agent: a status glyph (turning braille spinner for running,⚑waiting,✓done,✗failed), the job as its title (two lines, cut at a word),model · age, a rule, what it is doing now and its last four steps with their ages. Light flows down the bus to working agents; a dispatch, mail, finish, ask or answer sends a glyph along it, the borders of working and waiting cards breathe, and a new Jev decision flashes Jev's border. Finished agents older than four minutes fold into clickableFINISHEDpills. A finished native subagent of a live session readsidle(it can still be messaged), and so does a Codex pane whose rollout's last turn ended (task_completeorturn_aborted): its TUI waits at its prompt; past four idle cards the rest fold intoIDLEpills. The agent order and the scrolling of many rows are as before (wheel,PgUp/PgDn, arrows). Terminals that are too short shrink the cards, then Jev, before they drop anything. - NEEDS YOU (right column): every pending operator wait shows here (a Claude or
Codex approval naming its command, an
AskUserQuestionuntil the next tool call, a workflow awaiting approval); a wait only its harness can answer is a card with⏎ Open its pane. A Claude permission wait appears only once Claude reports its dialog shown (thepermission_promptnotification, after about six seconds): aPermissionRequestalone can resolve with no dialog at all, such as an auto-mode background subagent's. The oldest pending approval is a card: who asks (the agent's job), where, how long ago, the whole command wrapped by column to four lines and coloured, why it needs you, andy Allow once,a Always allow,d Deny,⏎ Open its harness.yanddare drawn faint and do nothing unless the whole command is shown;aunless the request offers it.1 of Ncounts the pending ones. Open supervisor rulings (not_done,revise,stop) are listed too, andooverrides the oldest. Under it a compact list: further requests, failed agents, sessions whose attention latch says they wait for you, stalled sessions, and the workflow's next gate. Nothing pending reads✓ Nothing needs you right now. - SELECTED (right column),
PREVIEWwhile the pointer hovers another node: status, model, kind (Codex pane,Claude subagent, inside seat), where it runs, workflow step, what it is doing now and its recent steps. For Jev: today's decisions and how many were unsure, the enabled sites and what it does. Chips⏎ Open,m Message,n Nudge,x Stop; one whose action does not apply (n,xneed a pane) is faint and does nothing. - ACTIVITY: six rows of the merged event log,
time from ──◆──▶ to text, with the glyph for mail✉, a Jev or proxy decision◆, the supervisor», a dispatch●and an agent's ask of you⚑; a new row is lit for two seconds. A click on a row selects its agent. - Key bar: chips for the keys that apply now (
y/a/donly while a request is pending), a chip is drawn whole or not at all, and a faint hint row under it. A toast shows on the FLOW border for a few seconds.
Everything animated is drawn from an injected clock and starts only when a background gather brings something the previous one did not hold; the view redraws about 15 times a second while it shows and the classic dashboard's tick is unchanged. Below 100 columns, or when the terminal is shorter than 35 rows, the earlier tiers draw instead: a compact A with the seat and Jev cards hung on the bus, and below 80x24 an indented list.
Keys (plain keys while the view shows; every ^A chord keeps working):
| Key | Does |
|---|---|
← → h l | previous / next: the seat, Jev, then the agents |
↑ ↓ k j | the seat, Jev, then a row of agents at a time |
Tab Shift+Tab | every node in turn |
⏎ | open the selected agent's own harness (Jev has none) |
o | Override the oldest open supervisor ruling shown in NEEDS YOU |
y a d | allow once / always allow / deny the request NEEDS YOU draws; a only when it offers it |
m | mail the agent; for a subagent without a pane, its host session, the text started For your subagent <job>: |
n | nudge the agent (the nudge dialog) |
x | stop the agent, after stop <agent>? y / n (anything but y cancels) |
+ | spawn a new agent (the spawn dialog) |
r | retry a failed pane (relaunch from its original request) |
A | activity: everything, or only the selected node's |
s | scope: this dashboard, this repo, all sessions |
PgUp PgDn | scroll the agent rows |
? | the key list (any key closes it) |
Esc | back to the classic dashboard |
^A t | the same, from an open chat back to the flow first |
Mouse: one click on a card, the seat or a FINISHED pill opens that agent's own
harness, never a custom view: a pane agent (Codex or Claude) opens its pane chat, and a
native Claude subagent, which has no pane because it runs inside its root session's
Claude Code, drives the host pane's own subagent list to that subagent when the host is
idle with an empty prompt, and otherwise opens a read-only live view of its transcript
(Esc or ^A t closes it). The Jev box counts only this seat's session, its
descendants and nothing from before it started. A click on Jev, a NEEDS YOU
row or an activity row selects. A click on a chip does what its key does. Hovering
previews a node in SELECTED; the terminal must report plain pointer motion for that (the
dashboard asks for any-motion reports, ?1003, only while the flow shows and never on
Windows; otherwise only button, drag and wheel reports, so a terminal that sends no
motion keeps the previous behaviour). The wheel scrolls the agent rows over the flow and
the event log over the activity box.
An agent's chat opens in place of the flow, under the header and stepper and a bar,
‹ Flow / <glyph> <agent> · <where it runs> · <harness model>, where ‹ Flow is a
button, and with an others strip below it listing the other agents with their
state glyphs. Clicking a name there opens that chat, ^A ← and ^A → move to the
previous or next agent's chat, ^A t or a click on ‹ Flow goes back to the flow, and
every other key, Esc included, goes to the agent. Terminals smaller than the
orchestrator dashboard keep the one-line bar.
Nothing is computed while the classic dashboard shows: the data is gathered on a background thread only while the view is visible, at the sidebar facts cadence (once a second), with no network call, and every frame is drawn from that and the dashboard's cached facts. The classic dashboard's rendering is untouched.
Approvals inbox (issue #840, off by default). With [approvals] inbox = true, a Claude PermissionRequest from a pane of a live dashboard is held for
that dashboard's operator instead of showing the pane's own dialog at once, but only in
the default, plan and acceptEdits permission modes (auto, dontAsk and
bypassPermissions resolve without a dialog, so nothing is held). A
strip appears across the bottom of the dashboard and of an open agent chat ONLY
while something is pending (with nothing pending the screens are byte-identical
to a build without the feature): the oldest request first, with the agent,
harness and model, pane, wait time, the tool and a redacted one-line preview.
^A y allows it once, ^A Y allows it and applies the always-allow rule Claude
itself offered (shown as ^A Y always: <label>, only when Claude sent one),
^A d denies it, ^A ] shows the next, ^A g releases
it to its pane's native dialog and goes there, and ^A a lists every live
dashboard's requests (a request another dashboard holds reads "answer in
dashboard <pid>"). A ⚑ N approvals header badge and a ⚑ marker on each
waiting session row draw only while N is above zero. The terminal is shorter by
the strip's five rows while it shows, so no pane output is hidden behind it;
below 16 rows or 40 columns the strip is skipped and the badge and markers
remain. The orchestrator dashboard draws no strip: its NEEDS YOU card shows the
oldest request with y/d instead, and answers only one whose whole command it drew.
A decision answers ONE call: the hook prints
hookSpecificOutput.decision.behavior and sets updatedPermissions only for
^A Y (always allow, below), so nothing else is cached as a lease. After hold_secs, on any error, a missing
dashboard or a dashboard that quits, the hook prints nothing and the native
dialog shows; the request then reads "waiting in pane" until a PostToolUse,
PostToolUseFailure or PermissionDenied for that call clears it. A request
answered in the dashboard, or every main-thread prompt once you submit the
session's next prompt (a subagent's open dialog stays until that subagent stops), clears its wait at once; one answered in the pane's own dialog clears
only at that PostToolUse, after the allowed command has finished, because Claude
Code fires no hook when its dialog is answered. Each dashboard tick also reads
the last 256 KB of the session transcript for a released request: a tool_result
for the call's tool_use_id (an allow or a deny in the pane, either one) clears
it, ends its prompt and lowers the latch. That id is paired from the transcript
when the request is made; without one, a request clears as before. Mail and advisories are never typed into a pane while any
of its permission requests is open, shown or not. The installed PermissionRequest hook
timeout is hold_secs + 30 (only with the inbox on). zirv setup apply and
zirv's own Claude launches register zirv ctx hook permission on
PermissionRequest, PermissionDenied and the Notification matcher
permission_prompt. Codex has no hook
surface zirv can read, so its panes are never listed and never answered.
The inbox is deliberately unreachable by agents: decisions travel only from the
dashboard process to the hook over the dashboard's unix socket
(<state>/s/a<pid>.sock), the hook checks the socket peer's pid against the
session's recorded owner_pid before it sends anything, and the dashboard
discards anything a client sends after its one request line. There is no
approve or deny verb, no MCP tool and no mail path, and the request records
under <state>/approvals/ are display-only data the dashboard never trusts.
The dashboard's own mouse reporting stays on for the whole session (subject
only to dash.mouse below, the operator's on/off switch — there is no
mid-session toggle, Ctrl+A v/"select mode" has been removed). Click-drag
inside any pane — including one whose child has turned on its own mouse
reporting, such as the Claude Code or Codex TUI — selects that pane's text:
a plain click still reaches the child (a press is forwarded only once
release proves it moved no more than one cell, so a real click never turns
into an accidental drag), while a genuine drag is the dashboard's own
selection, built strictly from that pane's screen contents so the sidebar,
borders and other chrome can never be copied, with trailing whitespace
trimmed from each line. The wheel keeps scrolling — the dashboard's own
scrollback, or forwarded to the child, exactly as before — without cancelling
an in-progress or already-highlighted selection, and dragging past the top or
bottom edge of the pane auto-scrolls it. Releasing the drag copies to the
system clipboard via OSC 52, with a platform fallback (pbcopy on macOS,
wl-copy then xclip on Linux, clip.exe on Windows) run in the background
for a terminal that silently ignores OSC 52 (macOS Terminal.app is the one on
record); if neither lands, the header shows a notice rather than losing the
copy silently. The sidebar's own JEV section (see [jev] below) is
click-through too: a left click on its errors N · <reason> line opens a
read-only dialog listing the window's most recent gated-call failures (site,
reason, age), the same list zirv ctx jev status folds from
jev-decisions.jsonl.
Every dashboard control below is repo-forbidden (see Trust
boundary below — a checkout cannot switch it on/off or
change its own limits) with two deliberate exceptions: idle_quiet_ms is a
pure per-session timing knob over a session the operator already chose to run
interactively, not a cap standing between an untrusted layer and something it
must not raise for itself, so a repository may set it; motion is the same
kind of knob, purely presentational (spinners, shimmer, gauge easing,
pending-rollover breathing, row flashes and toast fades vs. state changes
only), so it is repo-settable too:
[dash]
enabled = true # ZIRV_CTX_DASH
sidebar_cols = 28 # ZIRV_CTX_DASH_SIDEBAR_COLS
roster_max_age_secs = 604800 # ZIRV_CTX_DASH_ROSTER_MAX_AGE_SECS
max_panes = 9 # ZIRV_CTX_DASH_MAX_PANES
mouse = true # ZIRV_CTX_DASH_MOUSE
idle_quiet_ms = 10000 # ZIRV_CTX_DASH_IDLE_QUIET_MS -- repo-settable
motion = "full" # ZIRV_CTX_DASH_MOTION -- "full" | "reduced", repo-settable