zirvDocs

DocsSupervision (zirv ctx)

zirv chat and zirv agent

README.md, lines 290–649View source

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 behavior zirv ctx exec gives 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:

bash
zirv agent codex "fix the failing test" --json
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) or inline (this process supervised the worker itself).
  • state — launched (a pane was admitted/claimed; nothing has run yet — check zirv ctx inbox later for the worker's own report), launch_failed (refused, or the worker process never started at all — see reason), 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 at result_path, with an empty report), reported (final text extracted, no --result-schema/--result-kind contract declared), reported_validated (a declared contract was satisfied), reported_contract_failed (a declared contract failed even after the one bounded retry — see errors).
  • result_path — where the worker's own record was persisted, when one was; on the harness runtime exited_no_report always 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:

text
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 you badge or ✓ all clear, the seat harness's usage 5h reading (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, with step 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 says No workflow running.
  • FLOW: the seat card with its counts, the Jev box (its three newest decisions with site, verdict, a confidence bar, sure or unsure and age; the seat's line runs through it; jev off only 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 clickable FINISHED pills. A finished native subagent of a live session reads idle (it can still be messaged), and so does a Codex pane whose rollout's last turn ended (task_complete or turn_aborted): its TUI waits at its prompt; past four idle cards the rest fold into IDLE pills. 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 AskUserQuestion until 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 (the permission_prompt notification, after about six seconds): a PermissionRequest alone 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, and y Allow once, a Always allow, d Deny, ⏎ Open its harness. y and d are drawn faint and do nothing unless the whole command is shown; a unless the request offers it. 1 of N counts the pending ones. Open supervisor rulings (not_done, revise, stop) are listed too, and o overrides 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), PREVIEW while 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, x need 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/d only 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):

KeyDoes
← → h lprevious / next: the seat, Jev, then the agents
↑ ↓ k jthe seat, Jev, then a row of agents at a time
Tab Shift+Tabevery node in turn
⏎open the selected agent's own harness (Jev has none)
oOverride the oldest open supervisor ruling shown in NEEDS YOU
y a dallow once / always allow / deny the request NEEDS YOU draws; a only when it offers it
mmail the agent; for a subagent without a pane, its host session, the text started For your subagent <job>:
nnudge the agent (the nudge dialog)
xstop the agent, after stop <agent>? y / n (anything but y cancels)
+spawn a new agent (the spawn dialog)
rretry a failed pane (relaunch from its original request)
Aactivity: everything, or only the selected node's
sscope: this dashboard, this repo, all sessions
PgUp PgDnscroll the agent rows
?the key list (any key closes it)
Escback to the classic dashboard
^A tthe 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:

toml
[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