# Zirv CLI > **Native harness: coming soon.** This release ships the existing meta harness. > `zirv native` only displays this notice. Native CLI flags, configured defaults, > workers, helper calls, dashboard sessions, provider execution, recovery and > rollover cannot enable it. There is no environment variable, configuration > setting or Cargo feature that unlocks native execution in a normal build. > Use `zirv chat` (or bare `zirv`) for the existing harness. If you previously > tested native defaults, use `--runtime harness` or remove the native runtime > overrides from your operator configuration. > > Native implementation and design documentation below is retained for development > of a future release; its native launch and setup examples are unavailable here. [![Release](https://img.shields.io/github/v/release/Glubiz/zirv-cli)](https://github.com/Glubiz/zirv-cli/releases) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) > **Zirv CLI** is a cross-platform command-line interface for developers to automate and streamline workflows with YAML, JSON, or TOML scripts. --- ## Table of Contents - [Just Run `zirv`](#just-run-zirv) - [AI setup and harness migration](#ai-setup-and-harness-migration) - [The dashboard: multiple sessions in one terminal](#the-dashboard-multiple-sessions-in-one-terminal) - [Features](#features) - [Script runner](#script-runner) - [Harness supervision (`zirv ctx`)](#harness-supervision-zirv-ctx) - [Development workflow commands](#development-workflow-commands) - [Verification](#verification) - [Housekeeping](#housekeeping) - [Installation](#installation) - [Upgrading](#upgrading) - [Usage](#usage) - [Initialize a Project](#initialize-a-project) - [Creating a New Script](#creating-a-new-script) - [Running Scripts](#running-scripts) - [Passing Parameters](#passing-parameters) - [Optional Parameters](#optional-parameters) - [Capture Output](#capture-output) - [Failure Hooks](#failure-hooks) - [Dry Run](#dry-run) - [Chaining Scripts](#chaining-scripts) - [Configuration](#configuration) - [Directory Structure](#directory-structure) - [Schema Examples](#schema-examples) - [Shortcuts](#shortcuts) - [Reserved Command Names](#reserved-command-names) - [Development Workflows](#development-workflows) - [The full verb set](#the-full-verb-set) - [Lifecycle and artifacts](#lifecycle-and-artifacts) - [Deploy tiers](#deploy-tiers) - [Workflow adoption](#workflow-adoption) - [Agent registry](#agent-registry) - [Team composition](#team-composition) - [Maintain loop](#maintain-loop) - [Frontend quality](#frontend-quality) - [Context Management (zirv ctx)](#context-management-zirv-ctx) - [MCP bridge](#mcp-bridge) - [Cross-harness fallback and handover](#cross-harness-fallback-and-handover) - [Permission auditing and safe-list proposals](#permission-auditing-and-safe-list-proposals-issue-178) - [Supported harnesses and models](#supported-harnesses-and-models) - [Model catalogue](#model-catalogue) - [Supported Platforms](#supported-platforms) - [Contribution](#contribution) - [License](#license) - [Contact](#contact) --- ## Just Run `zirv` The fastest way to start a session: run `zirv` with no arguments, in a zirv-managed repo (one with a **local** `.zirv/` directory), from a real terminal. ```bash cd my-project zirv ``` | Situation | Result | |---|---| | a local `./.zirv` exists and both stdin and stdout are a real terminal | starts `zirv ctx chat` — an interactive orchestrator session | | No local `.zirv`, or stdin/stdout is piped or redirected | shows this same `zirv help` listing, exit 0 | | `zirv --help` / `zirv -h` | always shows help, even with nothing else on the command line | This is a deliberate behavior change: before, a bare `zirv` was a clap usage error (missing the required `command` argument, exit 2). A **global** `~/.zirv` alone does not count — only a local `./.zirv` says "this directory is zirv-managed" — and **both** stdin and stdout have to be a real terminal: piped stdin (`echo hi | zirv`, a CI job) or a redirected stdout (`zirv | less`) always falls back to help instead, so a bare invocation never blocks waiting on a chat session, or opens one into a pipe, when nothing interactive is on the other end. ### AI setup and harness migration Run the guided setup when moving an existing Claude Code or Codex repository to Zirv: ```bash zirv setup ``` The non-interactive equivalent is `zirv setup apply`. It initializes `.zirv/`, migrates root `CLAUDE.md`/`AGENTS.md` instructions into the canonical `.zirv/context/` layer without deleting or overwriting the native files, bootstraps a small shared memory bank, and merges Zirv's Stop, UserPromptSubmit, PreCompact, and guarded PreToolUse hooks into Claude's existing `settings.json` and Codex's existing `hooks.json`. Unrelated hooks are preserved and both files are backed up before modification. Codex asks you to review new hooks with `/hooks`. An existing Claude statusline is preserved; when none is configured, setup installs `zirv ctx usage tee`. ```bash zirv setup status zirv setup status --json zirv setup apply --dry-run zirv setup apply --memory-source /path/to/docs-or-obsidian-vault ``` Canonical common context reaches both Claude and Codex; optional `.zirv/context/claude.md` and `.zirv/context/codex.md` additions apply only to that harness. Direct Codex launches receive the compiled prompt through the CLI's per-run `developer_instructions` config override. Windows shell-shim launches remain fail-closed: Zirv never puts repository-authored prompt text on an argv that `cmd.exe` or PowerShell would reparse. AI-specific settings can be factory-reset separately from Zirv setup. Reset is refused without `--yes`, supports `--dry-run`, backs up every exact target with a manifest under `.zirv/backups/ai-reset/` (project) or `~/.zirv/backups/ai-reset/` (global), and preserves authentication, sessions/history, and caches unless `--include-auth` is explicitly passed: ```bash zirv setup reset claude --scope project --dry-run zirv setup reset codex --scope global --yes zirv setup reset all --scope all --yes ``` ### `ZIRV.md` instruction files Zirv's own native instruction file. Sources, closest scope first: 1. nested `ZIRV.md` files between the repository root and the files/worktree scope currently being acted on; 2. repository `ZIRV.md` at the repo root, or `.zirv/ZIRV.md` when the root file is absent — if both exist, the root file wins and `.zirv/ZIRV.md` is reported shadowed, never merged; 3. optional operator-global `~/.zirv/ZIRV.md`. The portable `AGENTS.md` convention is a first-class source alongside it, and existing `CLAUDE.md` repositories work unmigrated. At the same directory, `ZIRV.md` outranks `AGENTS.md`, which outranks `CLAUDE.md`, which outranks the singular `AGENT.md` compatibility alias — `AGENT.md` is only ever a candidate when no `AGENTS.md` exists in that directory, and always carries a migration diagnostic recommending rename to `AGENTS.md`. A compatibility file whose content duplicates the winning file (identical text, a symlink resolving to it, or a lone `@AGENTS.md`-style import of it) is reported as a duplicate consumed once, not as separate shadowed content — the same rules never reach a session twice. `zirv ctx optimize`'s report and `zirv context status` (see [Reviewing your instruction files](#reviewing-your-instruction-files)) list every discovered `ZIRV.md`/`AGENTS.md`/`CLAUDE.md`/`AGENT.md` surface with its trust class, scope, content hash, and precedence decision (included/shadowed/duplicate/excluded, with a reason). **Instructions are context, never permissions**: like every native instruction file, `ZIRV.md` can steer a session's prose but can never change sandboxing, approvals, credentials, provider/account/billing routing, tool grants, workflow policy floors, or settings precedence — see [Trust boundary](#trust-boundary) below. **Migration is opt-in and never destructive.** `zirv context sync --init-zirv-md` idempotently writes a starting `/ZIRV.md` from the canonical `.zirv/context/common.md` layer plus any root `AGENTS.md`/ `CLAUDE.md` content that is not already zirv-managed; a repeat run with nothing changed is a no-op, and an existing `ZIRV.md` — including one you have since hand-edited — is never overwritten without `--force`. Content that looks secret-shaped is skipped and named, never copied. A repository with only `AGENTS.md` needs no migration at all: `zirv context sync --report` (the default, read-only mode) offers a one-line compatibility-link plan — a `ZIRV.md` containing just `@AGENTS.md` — for anyone who would rather link than duplicate. ### `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](#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 [-- 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/.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), `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 full report was persisted, when one was (see [Sending mail between sessions](#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](#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](#cross-harness-fallback-and-handover) below), `z` zooms the focused pane, `e` shows recent errors, `?`/`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. Every dashboard control below is repo-forbidden (see [Trust boundary](#trust-boundary) below — a checkout cannot switch it on/off or change its own limits) with one deliberate exception: `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: ```toml [dash] enabled = true # ZIRV_CTX_DASH sidebar_cols = 44 # 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 ``` ### `zirv memory` `zirv memory` manages this repository's memory bank without starting an AI session: ```bash zirv memory init --dry-run zirv memory init zirv memory init --source /path/to/docs --merge zirv memory status zirv memory list zirv memory recall staging-db zirv memory remember staging-db-creds "the staging DB creds live in 1Password under staging-db" zirv memory remember deploy-cmd "cargo build --release" --importance high --confidence high --tag deploy --tag release zirv memory forget staging-db-creds zirv memory verify staging-db-creds ``` `remember` also takes `--importance `, `--confidence `, and a repeatable `--tag ` — all optional, unset by default. They land in the stored entry and feed `zirv memory recall`'s ranking (`retrieval::score_one`); `zirv ctx remember` has no equivalent flags. `--importance`/`--confidence` reject any value outside the three listed. `memory init` proposes a bounded set of durable shared entries from repository validation/toolchain surfaces and high-signal Markdown sections. `--dry-run` changes nothing, `--source` accepts a Markdown file or directory (including an Obsidian vault), and a non-empty shared bank is refused unless `--merge` is passed; merge mode adds missing keys and never silently overwrites curated entries. `--max-entries` and `--max-bytes` cap initialization. Every verb defaults to the **private** (machine-local) bank; pass `--shared` to act on the **shared**, repository-owned bank instead — see [Memory bank](#memory-bank) below for what the two scopes mean. `list` and `recall` respect each scope's own gate (`memory.enabled` / `memory.shared_enabled`): a disabled scope lists or recalls empty rather than showing what it holds. `status` never hides a disabled scope's counts — it marks the scope `disabled` but still reports its entry count and stored bytes, since a byte count is not the entry content the gate exists to withhold. `forget` and `verify` work even while a scope is disabled — disabling a scope must never trap data behind it. `status` never prints an entry's key or body, only scope availability, entry counts, stored bytes, and the configured injection budget. `zirv ctx remember --key --text ` / `zirv ctx recall` / `zirv ctx forget ` (flag-based, private-scope only) are untouched and keep working exactly as before — `zirv memory` is a newer, scope-aware surface alongside them, not a replacement. `forget` on a missing key exits `0` (it is idempotent — "already gone" is success); `verify` on a missing key exits `1` (it is stamping a claim about an entry that does not exist, which is a real failure) — the asymmetry is deliberate, not a bug. ### Sending mail between sessions Agent sessions running on the same machine can leave each other short notes, scoped to the current repository, with `zirv ctx send` and `zirv ctx inbox`: ```bash zirv ctx send --message "the webhook route moved to /v2/webhook" zirv ctx inbox ``` `zirv ctx status` reports how many are waiting (`mail: N unread`). A mail message is free-form text written by whichever agent session sent it, not an operator instruction — see [Trust boundary](#trust-boundary) for how it's capped and labeled the same way the other untrusted surfaces are. Add `--to-session ` to address one specific live session instead of every session an agent has: `zirv ctx send --to-session abcd1234 --message "..."` resolves `abcd1234` (a short id, or a unique prefix of one — see [Session registry and nudging](#session-registry-and-nudging) below) against the live registry and stores the full resolved id, so the message keeps finding its target even if the registry record itself is gone by the time it is read. A session-addressed message is only ever delivered into a **headless** session's own launch prompt (`exec`/`loop`, when `[mail] enabled = true`); an **interactive** session (`chat`/`wrap`) only ever gets a one-line unread-count advisory on its status bar or event channel, split as broadcast+direct once something is addressed to it specifically — never the message body itself, the same "advisory, not authority" rule `zirv ctx nudge` follows below. There is no environment variable for `--to-session`: unlike the config knobs elsewhere in this document, session addressing is a per-invocation argument, not something an operator or a repo would want to pin as a default. A body over `[mail] max_message_bytes`/`max_delivered_bytes` is still stored, truncated, rather than failing the send — but the cut text is no longer just lost. The full original body is written to a sidecar file under the mailbox's own `full/` subdirectory (capped at 1 MiB of its own, with a trailing `[truncated]` marker if even that is exceeded), and the stored message's own `[truncated]` marker becomes `[truncated; full body: ]`, naming that sidecar. `full/` is a dedicated directory nothing else ever moves a message into or out of — unlike the message itself, a sidecar is never relocated into `read/` on consume or into a dead-letter directory once its TTL expires, so a marker's path always stays valid for as long as the sidecar exists. It is pruned to the same `[mail] keep` as the mailbox itself, in its own pass, so it does not accumulate forever either. A delegated worker's own final report is persisted before any report-back mail is sent, at `/logs/delegation-results/.json` (capped at 1 MiB, with a `report_truncated` flag when cut) — whether or not the delegation declared a `--result-schema`/`--result-kind` contract. The report-back mail for a contract-declared delegation names that file as a trailing `full report: ` line, and `zirv ctx agent --json`'s own receipt carries it as `result_path` (see [Delegation receipt](#delegation-receipt---json) above). ### Session registry and nudging Every supervised session (`wrap`, `exec`, `loop`, `chat`) registers itself under the state dir at `/sessions/.json` for as long as it is alive — best effort, released when the supervisor exits, and swept automatically the moment `zirv ctx status` (or anything else that reads the registry) notices its process is gone. `zirv ctx status` reports it under `sessions:`, one line per record: ``` sessions: abcdef12 claude exec pid 48213 3m live -work-my-repo 9a8b7c6d claude wrap pid 19042 40m stale -work-my-repo 1f2e3d4c claude wrap pid 51120 5m unreachable -work-other-repo ``` `unreachable` means the process is running but bound no turn-signal socket (`--no-supervise`, or the socket failed to bind), so it never checks for wake-ups: `zirv ctx nudge` refuses such a target and says so, while `zirv ctx send` still leaves a message for its next run. `` is the same eight-character id `--to-session` and `zirv ctx nudge` resolve a prefix against. A socket left behind by an older zirv binary that predates the registry (`s/*.sock` with no matching JSON record) still shows up, labeled `(no record)`, so a mixed-version machine never silently drops a live session from the listing. `zirv ctx nudge --message ` wakes a live session early instead of waiting for it to notice on its own: ```bash zirv ctx nudge abcd --message "please check the new failing test" ``` A nudge prefix must be at least four characters (or a session's whole short id) — unlike `--to-session`, which only addresses a message, a nudge wakes and can restart what it resolves to, and on a machine running one session a single mistyped character is still "unique". A shorter prefix is refused and the live sessions are named back to you. The message itself is ordinary, durable mail (visible in `zirv ctx inbox` even if the wake-up is missed), so the two pieces are decoupled on purpose: a nudge is a wake-up signal plus a payload, stored separately, and losing the wake-up never loses the message. For a headless session (`exec`), a nudge costs the in-flight turn — the session is stopped and relaunched with a handoff distilled from the transcript so far, the same recovery path a rot restart uses, just triggered by an operator instead of the rot engine. That restart is bounded by `[supervise] max_nudges` (default 3, `ZIRV_CTX_MAX_NUDGES`) so a session cannot be interrupted indefinitely; past the cap a nudge's message is still queued as mail but the session runs on untouched. The cap counts *consecutive* nudges — it resets as soon as the session reports a turn of its own, so a long-running session that keeps making progress can keep being steered. For an interactive session (`wrap` or `chat`), a nudge is advisory only: it never restarts or types anything into the agent, and it never receives the message body — it just surfaces on the status bar and event channel that a nudge arrived, pointing at `zirv ctx inbox`. `zirv ctx nudge` says so at send time too when the target it resolved is interactive, so an advisory delivery never looks like a nudge that silently did nothing. Either way, latency is bounded by `[supervise] poll_ms` (default 2000ms) — the interval a supervisor's own tick already runs on, since a nudge just claims a marker file that same tick checks for. ### Banner, status bar and events A `zirv ctx chat` session (bare `zirv` included) with a real, large-enough terminal attached also gets a bit of chrome: - a one-time **launch banner** naming the resolved harness, the rule that chose it, and the session id; - a reserved **one-row status bar** pinned to the bottom of the terminal; - an **event channel** on stderr, one line per notable event, in the shape `[HH:MM:SS] zirv ▸ `. All three degrade together and only in one direction: `--simple`, `--no-supervise`, a terminal narrower than 40 columns or shorter than 8 rows, or a non-terminal stdout turns every piece off, and nothing here ever upgrades a session mid-run. Turn just the event channel off with `--quiet`, the `ZIRV_CTX_QUIET` environment variable, or `[chrome] events = false` in `ctx.toml`; `[chrome] banner` and `[chrome] bar` switch the other two off the same way. See [.settings.toml](#settingstoml) below for enabling or disabling the harnesses themselves (claude, codex) — a separate file from `[chrome]`. ## Features Every capability below is derived from the command surface this binary actually ships (`zirv commands --json`), grouped by area; each bullet links to the section that documents it in depth. ### Script runner - **Script formats and layout** — YAML, JSON, or TOML scripts live in `.zirv/commands/` (or the global `~/.zirv/commands/`) with name/description metadata, extendable to new formats. See [Directory Structure](#directory-structure) and [Schema Examples](#schema-examples). - **Parameters** — required, positional `${var}` parameters, plus trailing optional ones (`greeting?`) that resolve to an empty string when omitted. See [Passing Parameters](#passing-parameters) and [Optional Parameters](#optional-parameters). - **Capture output** — `capture: var_name` on any step grabs its stdout into `${var_name}` for later substitution. See [Capture Output](#capture-output). - **Failure hooks** — a `fallback` sub-chain runs once on step failure (the original command is never retried); `proceed_on_failure` separately controls whether the script continues. See [Failure Hooks](#failure-hooks). - **Flexible per-step options** — `interactive` mode, `operating_system` filters, `proceed_on_failure`, `delay_ms`, and `secrets`. See [Schema Examples](#schema-examples). - **Dry run** — `--dry-run` prints every `${...}`-substituted step instead of running it. See [Dry Run](#dry-run). - **Chaining scripts** — one script calls another as an ordinary command (`command: zirv build`). See [Chaining Scripts](#chaining-scripts). - **Concurrent shells** — nested command lists open one terminal window per group, with a built-in `cd` that updates the working directory within a window. See [Concurrent Shells](#concurrent-shells). - **Agent steps** — an `agent`/`prompt` step runs a supervised AI-agent task in place of a shell command, under the same pacing and rot detection `zirv ctx exec` gives a hand-written invocation. See [Agent Steps](#agent-steps). - **Shortcuts** — short aliases for local or global scripts. See [Shortcuts](#shortcuts). - **Reserved command names** — built-in verbs can never be shadowed by a script or shortcut; a collision is flagged rather than silently swallowed. See [Reserved Command Names](#reserved-command-names). - **Helpful errors** — a mistyped script or shortcut name gets up to 3 "did you mean" suggestions by edit distance instead of a bare failure. See [Running Scripts](#running-scripts). - **Cross-platform** — Windows, macOS, and Linux. See [Installation](#installation). ### Harness supervision (`zirv ctx`) - **MCP bridge** — `mcp` (`zirv ctx mcp serve`) exposes repository-scoped session, memory, workflow, artifact, worker-result, and inbox reads to wrapped MCP hosts. `zirv ctx mcp doctor` checks a real stdio connection. See [MCP bridge](#mcp-bridge). - **Harness adapters** — one adapter per supported harness: `claude`, `codex`, `gemini`, `opencode`, `pi`, `copilot`, `droid`, `qwen`, `grok`, `kimi`, `cursor-agent`, `goose`, and `muse`, each enabled or disabled per repo in `.zirv/.settings.toml`. See [.settings.toml](#settingstoml). For which models zirv recognises on each harness, see [Supported harnesses and models](#supported-harnesses-and-models). - **Dashboard** — several supervised sessions in one terminal, with panes for delegated workers, worktree groups, and kill/nudge/send from the keyboard. See [The dashboard: multiple sessions in one terminal](#the-dashboard-multiple-sessions-in-one-terminal). - **Persistent runtime (experimental)** — `session` runs a local service that owns the PTYs, so closing or crashing the client leaves the agents running: `serve` starts it, `list` shows what it holds, `attach` and `detach` connect and disconnect a terminal, and `stop` is the separate verb that actually ends something. Off by default and operator-only. See [Persistent runtime (`zirv session`)](#persistent-runtime-zirv-session). - **Nested sessions are refused** — a supervisor started inside another supervised session stops instead of sharing its outer session's registry and turn signals. See [Nested sessions are refused](#nested-sessions-are-refused). - **Rot detection** — a pure scoring engine turns transcript events into advise/compact/restart verdicts before context rot ruins a session. See [Signals and verdicts](#signals-and-verdicts). - **Usage pacing and cross-harness fallback** — bounded pauses against the usage window, elastic scheduling across seats, and a handover to the other harness when one is exhausted. See [Usage pacing](#usage-pacing) and [Cross-harness fallback and handover](#cross-harness-fallback-and-handover). - **Untrusted repository configuration** — repo-owned `.zirv/` surfaces may only narrow what zirv does; widening keys are operator-only. See [Trust boundary](#trust-boundary). - **Supervised exit codes** — headless runs map every supervision outcome to a documented exit code. See [Exit codes for supervised runs](#exit-codes-for-supervised-runs). - **Banner, status bar and events** — a one-line banner and status bar in interactive sessions, with events surfaced as they happen. See [Banner, status bar and events](#banner-status-bar-and-events). - **Session types** — `ctx` supervises `wrap` (an interactive TUI through a PTY), `exec` (one supervised headless run), and `loop` (a fresh headless session per cycle); `chat` starts an interactive orchestrator session (also the top-level `zirv chat` alias, and bare `zirv`), and `agent` delegates one task to a supervised worker on another enabled harness (also `zirv agent`). See [Verbs](#verbs) and [Just Run `zirv`](#just-run-zirv). - **Experimental: `native`** — a thin, case-insensitive top-level alias (`zirv native`) for `zirv chat --runtime native`, reserved so a script or shortcut can never shadow it. Shown only in `zirv help`'s separately labelled "Experimental / work in progress" section, never beside the stable commands; `zirv commands --json` reports it with `"stability": "experimental"` and `"runtime": "native"`. See [The native conversation pane](#the-native-conversation-pane). - **Handoffs and recovery** — `score` rot-scores a transcript, `handoff` distills one, `resume` starts a clean session with the latest handoff injected, `handover` swaps the orchestrator seat's model or harness in place mid-session, and `ask` distills a read-only answer to an operator's question from a LIVE worker's own transcript without ever sending it input or touching its transcript or registry record. See [Verbs](#verbs) and [Cross-harness fallback and handover](#cross-harness-fallback-and-handover). - **Status and attention** — `status`, `explain-status`, and `wait` report or block on a session's composed attention projection; `snapshot` prints a redacted, capped diagnostic summary. See [Verbs](#verbs) and [Signals and verdicts](#signals-and-verdicts). - **Mail and nudges** — `send`/`inbox` leave and read short notes between live sessions on this machine, and `nudge` wakes one early with a message; `kill` terminates a registered session outright. See [Sending mail between sessions](#sending-mail-between-sessions) and [Session registry and nudging](#session-registry-and-nudging). - **Memory bank** — `remember`/`recall`/`forget` read and write this repo's cross-session memory bank from inside a session; the standalone `zirv memory` (`memory`) surface (`init`, `status`, `list`, `recall`, `remember`, `forget`, `verify`, plus `promote`/`rollback`/`optimize`) manages it without starting one; `learn` promotes a recurring fail-then-fix command correction from recent transcripts into one private memory entry. See [`zirv memory`](#zirv-memory), [Memory bank](#memory-bank), and [Verbs](#verbs). - **Delegation controls** — `group`, `objective`, `spend`, `savings`, `worktree`, `task`, and `swarm` bound, account for, and reclaim delegated work; `permissions` (`audit`/`compile`/`propose`) and `safety` (`check`/`list`/`explain`) audit and enforce zirv's harness-neutral command-safety policy; `close` ends a group or objective early. See [Permission auditing and safe-list proposals](#permission-auditing-and-safe-list-proposals-issue-178), [Command safety policy](#command-safety-policy-issue-83), and [Verbs](#verbs). - **Recall, measurement, and output** — `search` ranks past transcripts/handoffs/artifacts/mail against a query; `measure` and `discover` report proportionality and uncompacted-tool-result metrics; `run`/`output` execute a command directly and store its full output while printing a compact, reversible summary; `compile` prints or measures the composed session prompt. See [Verbs](#verbs). - **Configuration and instruction hygiene** — `config` shows or edits the operator's `~/.zirv/ctx.toml`, and `config migrate`/`--downgrade` versions that file with a backup and a documented way back; `provider` (`init`/`list`/`check`/ `credential set`) configures and inspects opt-in native provider routes, accounts and credentials; `context` (`sync`/`lint`/`status`) manages the canonical instruction-file layer; `optimize` reports redundancy, contradictions, and dead references across every configuration surface; `usage` reports usage-window state or tees the statusline. See [Reviewing your instruction files](#reviewing-your-instruction-files) and [Environment variables worth knowing](#environment-variables-worth-knowing). - **Configured capabilities** — `capabilities` reports every non-shell integration a native session can use — MCP servers, web search/fetch, browser, language diagnostics, artifact and frontend rendering — as `available`, `unavailable` or `unverified`, naming the missing binary, credential or config key for anything absent. `--probe` contacts each configured MCP server to verify it; `--require` gates a script on the same admission rule the workflow engine applies. See [Native configured capabilities](#native-configured-capabilities). - **Native readiness** — `doctor` diagnoses whether this machine can run a native session: per role, which backend an unflagged session gets and which authority decided it, which route it would spend, and every problem sorted into exactly one of `missing-auth-material`, `inaccessible-model`, `missing-tool`, `unsupported-isolation`, `service-failure` or `upstream-entitlement` — so a missing native adapter is never dismissed as an entitlement problem. Writes nothing; `--live` additionally contacts each provider's model-list endpoint. Redacted like `snapshot`, so the output is safe to paste into a bug report. See [Native setup, diagnosis and rollback](#native-setup-diagnosis-and-rollback). - **Local runtime protocol** — `api` (`schema`/`serve`/`call`) publishes zirv's versioned local control surface: an owner-only unix socket or Windows named pipe carrying NDJSON requests, replies and event subscriptions, with a generated schema and frozen wire fixtures. See [Runtime protocol v1](#runtime-protocol-v1-zirv-ctx-api). - **Hooks** — `hook` wires zirv into Claude Code's and Codex's own lifecycle events (stop, prompt, pre-compact, pretool/posttool, permission, notify, session-start), audits recorded decisions, and checks or heals the installed hook entries against their baseline. See [Hook registration (Claude Code)](#hook-registration-claude-code). ### Development workflow commands - **Skills** — `skill` inspects model-agnostic engineering skills (`list`, `show `) layered from built-in, operator-global, and repository sources. See [Development Workflows](#development-workflows). - **Workflow lifecycle** — `workflow` runs the durable `intent → spec → plan → implement → test → review → verify → deploy` lifecycle: `list` built-in definitions, `show` one, `classify` a task without starting, `start` and persist one, `status` an instance, `resume` a persisted one, `reclassify` its methodology overlay, and `context` prints the current step's resolved skill context. See [The full verb set](#the-full-verb-set) and [Lifecycle and artifacts](#lifecycle-and-artifacts). - **Gating and progress** — `approve` a gated step, `advance` records a step result and transitions the state machine, and `close` ends a workflow that will not reach `Completed`. See [Deploy tiers](#deploy-tiers) and [Workflow adoption](#workflow-adoption). - **Artifacts and agent seats** — `artifacts` inspects committed work-product artifacts and their acceptance state, and `agents` (`list`/`show`/`dispatch`) inspects provider-neutral workflow seats and trust provenance; `dispatch --runtime native` runs a read-only seat on zirv's own runtime with no coding harness installed. See [Agent registry](#agent-registry). - **Team composition** — `team` (`plan`/`show`/`brief`) compiles the smallest capable team for a request from the agent/skill registries, persists it on a workflow, and briefs one compiled seat with only its own manifest instructions and attached skills. See [Team composition](#team-composition). - **Review** — `review` (`package`/`run`/`add`/`dispose`/`list`/ `ingest-pr-comments`) builds compact review packages and persists finding dispositions; `run --runtime native` runs the reviewer seat natively. - **Maintenance and telemetry** — `maintain` (`scan`) runs deterministic operator-configured maintenance detectors, and `stats` aggregates privacy-conscious local workflow telemetry. See [Maintain loop](#maintain-loop). - **Frontend quality** — `frontend` derives a design profile and drives autonomous frontend work end to end (`profile`, `capabilities`, `check`, `render`, `review`, `benchmark`). See [Frontend quality](#frontend-quality). ### Verification - **Repository-aware checks** — `test` maps changed paths to checks (`changed`), runs every eligible check (`all`), or records an operator-owned baseline of already-failing tests (`baseline`) so a pre-existing failure never blocks a workflow gate. See [The full verb set](#the-full-verb-set). - **Final verification** — `verify` runs the full check suite plus zirv's own built-in self-check registry, reusing fresh `test` evidence when nothing has changed since. See [The full verb set](#the-full-verb-set). - **Repository check configuration** — optional, schema-versioned `.zirv/verify.toml` declares check id/kind/command/path patterns/phase eligibility/timeout; without it, Cargo commands and `npm run` scripts are discovered from the manifests present. See [Frontend quality](#frontend-quality). ### Housekeeping - **Guided setup** — `setup` migrates an existing Claude Code/Codex repo to Zirv (`apply`, `status`, `profile`), and `reset`/`restore` factory-reset or restore AI-specific settings separately from the rest of Zirv (`setup reset`, `setup restore`). See [AI setup and harness migration](#ai-setup-and-harness-migration). - **Self-update** — `update` (`--version `) installs the latest or a specified zirv release. See [Upgrading](#upgrading). - **Bug and feature reports** — `report` (`bug`/`feature`) files a Zirv issue on GitHub, optionally attaching a redacted `snapshot`. - **Workflow artifacts** — `artifact` registers and inspects workflow artifacts (`list`, `present`, `render`, `show`); presentation prefers an adapter's native mechanism and falls back to a static file. See [Frontend quality](#frontend-quality). - **Bundled orientation** — `skill` (`--json`) prints the bundled operator orientation skill for this binary (also `zirv --skill`). - **Command inventory** — `commands` (`--json`) lists every command this binary accepts, generated straight from its own clap model. - **Project bootstrap** — `init` creates a `.zirv/` directory here, and `create` interactively (or flag-driven) writes a new script into `.zirv/commands/`. See [Initialize a Project](#initialize-a-project) and [Creating a New Script](#creating-a-new-script). - **Help and version** — `help` lists every available script and shortcut, local and global, and `version` prints the installed version. See [Usage](#usage). --- ## Installation Pick your OS below. Each has one copy-paste command. ### Windows ```bash choco install zirv ``` ### macOS ```bash brew tap glubiz/homebrew-tap brew install zirv ``` > If Homebrew reports the tap as untrusted, run `brew trust glubiz/tap` and retry. The published binary is universal (Intel and Apple Silicon), so this works on either Mac. ### Linux Recommended — install script: ```bash curl -sSfL https://raw.githubusercontent.com/Glubiz/zirv-cli/main/install.sh | sh ``` To install a specific version: ```bash curl -sSfL https://raw.githubusercontent.com/Glubiz/zirv-cli/main/install.sh | sh -s -- ``` Alternative — Homebrew on Linux, via the same tap: ```bash brew tap glubiz/homebrew-tap brew install zirv ``` > If Homebrew reports the tap as untrusted, run `brew trust glubiz/tap` and retry. The prebuilt Linux release is x86_64-only. On other architectures (aarch64, armv7, ...) the install script fails fast with a pointer to the source build; the Homebrew formula does not guard the architecture, so skip it there and build from source instead (see below). Releases up to 2.39.0 additionally require glibc 2.39+; from 2.39.1 the Linux binary is fully static. ### From source (any platform/arch) ```bash cargo install --git https://github.com/Glubiz/zirv-cli ``` Works today without a crates.io publish, and is the only supported path on architectures the release pipeline doesn't build for (e.g. Linux aarch64). ### Precompiled Binaries Download the latest release from the [GitHub Releases]: https://github.com/Glubiz/zirv-cli/releases Assets per version: `zirv--linux.tar.gz` (x86_64), `zirv--macos.tar.gz` (universal x86_64+arm64), `zirv--windows.exe`. ## Upgrading ### Built-in (any platform) ```bash zirv update zirv update --version ``` A Homebrew or Chocolatey installation keeps working after a built-in update, but the package manager's recorded version lags until its own upgrade command runs. ### Homebrew (macOS & Linux) ```bash brew upgrade zirv ``` ### Chocolatey (Windows) ```bash choco upgrade zirv ``` ### Install Script (Linux) Re-run the install script to get the latest version: ```bash curl -sSfL https://raw.githubusercontent.com/Glubiz/zirv-cli/main/install.sh | sh ``` ### From source ```bash cargo install --git https://github.com/Glubiz/zirv-cli --force ``` ## Usage `zirv help` (also `zirv h`, `zirv --help`, `zirv -h`) lists every available script and shortcut, local and global. ### Initialize a Project Run: ```bash zirv init ``` Creates a `.zirv/` directory, its `.zirv/commands/` subdirectory (where you will define your scripts, as of zirv 3.0), and a default `.shortcuts.yaml`. The `.zirv/` directory is created in the current working directory or in the HOME directory depending on the commandline interactions. ### Creating a New Script ```bash zirv create ``` Interactively asks for the script name, an optional shortcut key, and whether to create it locally or in the global `~/.zirv` folder, then writes a template script into `.zirv/commands/` (or `~/.zirv/commands/`), plus a shortcut entry in `.zirv/.shortcuts.yaml` if one was given. To script the creation (e.g. in CI or setup scripts), pass any of the three answers as flags to skip the corresponding prompt; passing all three skips every prompt: ```bash zirv create --name build --shortcut b --global false ``` - `--name ` — the script name (file is written as `.yaml`). - `--shortcut ` — a shortcut key, or an empty string for "no shortcut". - `--global` — create in `~/.zirv` instead of the current directory. Bare `--global` means true; pass `--global false` to answer "no" without a prompt. If the name or shortcut collides with a [reserved command name](#reserved-command-names), zirv warns and asks for confirmation before creating an unreachable script; in non-interactive mode (all three flags given) a collision is an error instead, since there is no prompt to fall back on. ### Running Scripts Place your script files in `.zirv/commands/` (e.g., `build.yaml`): ```yaml name: Build description: Build the application. commands: - command: cargo build --release options: proceed_on_failure: false - command: cargo test options: proceed_on_failure: false ``` Execute the script with: ```bash zirv build ``` If the name doesn't match any script or shortcut (checked locally in `.zirv/commands/`, then globally in `~/.zirv/commands/`), zirv suggests up to 3 close matches by edit distance and points you to `zirv help`. If a script was left at the `.zirv` root instead of moved into `commands/` (the pre-3.0 layout), the error names it and says where it needs to move — there is no fallback lookup at the old location: ``` error: No script or shortcut found for 'buld'. Did you mean: build? Run `zirv help` to see available scripts and shortcuts. ``` ### Passing Parameters If a script declares parameters; ```yaml name: Commit Changes params: - commit_message commands: - command: git add . - command: git commit -m "${commit_message}" - command: git push origin ``` Run with: ```bash zirv commit "Your commit message here" ``` ### Optional Parameters A parameter name ending in `?` is optional and resolves to an empty string when omitted. Optional parameters must be declared after all required ones: ```yaml name: Greet params: - name - greeting? commands: - command: echo "${greeting} ${name}" ``` ```bash zirv greet Alice # greeting = "" -> prints " Alice" zirv greet Alice "Welcome" # greeting = "Welcome" -> prints "Welcome Alice" ``` Declaring an optional parameter before a required one, giving fewer arguments than there are required parameters, giving more than the total number of declared parameters, or reusing a parameter name (with or without the trailing `?`) are all rejected with an error. ### Capture Output To capture the output of a command, use the `capture` option: ```yaml name: Capture Test commands: - command: "echo hello" capture: greeting options: proceed_on_failure: false - command: "echo Got: ${greeting}" ``` First step stores `hello` in the variable `${greeting}`, which is then used in the second step to print `Got: hello`. ### Failure Hooks Declare a failure hook for a command using `fallback`: ```yaml name: OnFailure Demo commands: - command: "sh -c 'exit 1'" options: proceed_on_failure: true # don't stop the script once fallback succeeds fallback: - command: "echo 'Fallback action'" ``` If the command fails, every `fallback` command runs, in order, once — **the original command is never retried**. If any fallback command itself fails, the step fails immediately with an error naming both the original and the failing fallback command. Otherwise, whether the step's own failure stops the script is controlled separately by `proceed_on_failure`: `true` continues to the next step, `false` (the default) stops the script with an error, even though every fallback succeeded. ### Dry Run Pass `--dry-run` to preview a script without running anything: ```bash zirv build --dry-run ``` Each step is printed with its `${...}` parameters substituted, instead of being executed, so you can check what a script would do first. ### Chaining Scripts You can chain scripts by calling one script from another. For example, if you have a script `build.yaml` and want to call it from `deploy.yaml`: ```yaml name: Deploy description: Deploy the application. commands: - command: zirv test options: proceed_on_failure: false - command: zirv build options: proceed_on_failure: false ``` Run the `deploy` script with: ```bash zirv deploy ``` ### Concurrent Shells You can open multiple terminals at once by nesting lists. For example: ```yaml name: Parallel Commands commands: - - command: "echo 'Running Task A'" - command: "echo 'Running Task B'" - - command: "echo 'Running Task 1'" - command: "echo 'Running Task 2'" ``` Each nested list spawns its own shell window. Every window executes the commands listed in that group and stays open until they finish. This needs a desktop/GUI session: macOS uses `osascript` to drive Terminal, Windows opens a new `cmd` window, and Linux tries `gnome-terminal`, then `x-terminal-emulator`, then `xterm`. Over a headless or SSH-only connection — or on Linux specifically, whenever neither `DISPLAY` nor `WAYLAND_DISPLAY` is set — zirv returns a clear error naming what it tried, instead of failing cryptically or hanging. The built-in `cd` command updates the working directory for any following commands in the same window, allowing scripts like: ```yaml commands: - - command: "cd backend" - command: "cargo run" ``` ### Agent Steps A command step can run a supervised AI-agent task instead of a shell command, using `agent` and `prompt` in place of `command`: ```yaml commands: - command: cargo test - agent: claude prompt: "Fix the failing tests in ${dir}" # optional: flags: ["--model", "sonnet"] - command: cargo test ``` `runtime` selects which machinery runs the step: `harness` (the default, and what every existing script keeps doing) or `native` — zirv conducts the conversation itself over a direct provider route, with no coding harness installed: ```yaml commands: - agent: fast-route # a [route] name, not an adapter name runtime: native prompt: "Summarise the failing checks in ${dir}" ``` Under `runtime: native` the `agent` value names a provider route from `~/.zirv/native.toml` (the reserved value `native` means "use the `[roles]` entry for the worker role"), and `flags` are refused rather than silently ignored — they exist to reach a vendor CLI and there is none. An unrecognised `runtime` fails at load time, so `--dry-run` and the real run reject the same script. Everything else — `${var}` substitution, secrets, `operating_system`, `proceed_on_failure`, `delay_ms`, `fallback` and the exit-code contract — is identical on both runtimes. `prompt` gets the same `${var}` substitution as `command`, including the unresolved-placeholder error if a variable is missing. `flags` are passed straight through to the agent CLI. `operating_system`, `proceed_on_failure`, `delay_ms` and `fallback` work the same as they do for a regular command; `capture` and `interactive` are not supported and fail the step if set. The step runs in-process through the same supervision `zirv ctx exec` uses: pacing against your usage windows, rot detection, and automatic restart with a distilled handoff if the session rots. A non-zero outcome fails the step like any other command. Only Claude Code is supported today (see [Context Management](#context-management-zirv-ctx) below); naming any other agent fails with that adapter's own error. ## Configuration ### Directory Structure The `.zirv/` directory contains zirv's configuration; your scripts live in its `commands/` subdirectory (zirv 3.0). The structure is as follows: ``` .zirv/ ├── .shortcuts.yaml ├── ctx.toml ├── commands/ │ └── ...your script files ``` ### Schema Examples Supported schemas are YAML, JSON, and TOML. Below are examples of each: #### YAML Example ```yaml name: Example Config description: An example script. params: - user commands: - command: "echo Welcome, ${user}" capture: welcome_msg options: interactive: false - command: echo ${welcome_msg} description: Prints greeting options: interactive: true operating_system: linux proceed_on_failure: false delay_ms: 2000 fallback: - command: "echo 'Attempting fallback...'" secrets: - name: api_key env_var: API_KEY ``` #### JSON Example ```json { "name": "Example Config", "description": "An example script.", "params": ["param1"], "commands": [ { "command": "echo Welcome, ${user}", "capture": "welcome_msg", "options": { "interactive": false } }, { "command": "echo ${welcome_msg}", "description": "Prints greeting", "options": { "interactive": true, "operating_system": "linux", "proceed_on_failure": false, "delay_ms": 2000, "fallback": [ { "command": "echo 'Attempting fallback...'" } ] } } ], "secrets": [ { "name": "api_key", "env_var": "API_KEY" } ] } ``` #### TOML Example ```toml name = "Example Config" description = "An example script." params = ["param1"] [[commands]] command = "echo Welcome, ${user}" capture = "welcome_msg" options.interactive = false [[commands]] command = "echo Token is ${token}" options.interactive = true options.operating_system = "linux" options.proceed_on_failure = false options.delay_ms = 2000 [[commands.options.fallback]] command = "echo 'Attempting fallback...'" [[secrets]] name = "api" env_var = "API_KEY" ``` ## Shortcuts Shortcuts are defined in `.shortcuts.yaml` and allow you to create aliases for your scripts. For example: ```yaml shortcuts: b: build.yaml t: test.yaml cm: commit.yaml ``` Run zirv b instead of zirv build.yaml. This will execute the `build.yaml` script. A shortcut key that collides with a [reserved command name](#reserved-command-names) (for example `c`, already `create`'s alias) can never be reached; `zirv help` marks it as shadowed in the listing. ## Reserved Command Names `help`, `version`, `init`, `create`, `ctx`, `memory`, `context`, `setup`, `report`, `chat`, `agent`, `skill`, `workflow`, `test`, `verify`, `artifact`, `frontend`, `commands`, `update`, `session`, `native`, and their short aliases `h`, `v`, `i`, `c`, are handled as built-in commands before zirv ever looks in `.zirv/`. The comparison is case-insensitive (`Chat`/`CHAT` collide just as much as `chat`, matching how NTFS/APFS resolve script filenames), so a differently-cased script or shortcut is caught too, even though only the exact lowercase spelling is literally intercepted as a routing alias. A script file or shortcut key using one of these names (in any case) can never be invoked: - `zirv help` lists it but marks it `(shadowed by a built-in command, unreachable)`. - `zirv create` warns about the collision and asks for confirmation before creating it anyway; in non-interactive mode (see [Creating a New Script](#creating-a-new-script)) the collision is an error instead. ## Development Workflows Zirv owns the high-level development lifecycle outside model conversation memory. Only the current phase's selected skill instructions are injected; completed phases remain durable private state and are not repeated after a session restart or compaction. ```bash zirv skill list zirv skill show systematic-debugging --agent codex zirv workflow classify --task "fix authentication race" zirv workflow start bugfix --task "fix authentication race" --agent codex zirv workflow start feature --task "use only shipped methodology" --built-in-only zirv workflow status zirv frontend profile zirv frontend capabilities --agent claude zirv frontend check zirv frontend render zirv frontend review --agent codex zirv frontend benchmark zirv test changed zirv verify zirv workflow stats ``` Built-in workflows cover `feature`, `bugfix`, `refactor`, `spike`, and `review`. Deterministic intent/complexity/risk classification selects proportional design, approval, test, and review depth; sensitive auth/security and database/schema changes cannot be downgraded below High risk. ### The full verb set ```bash zirv workflow list [--json] [--built-in-only] [--repo ] # registry ids: layer/version/hash/domains zirv workflow show feature [--json] [--built-in-only] [--repo ] # one definition's steps zirv workflow classify --task "..." # classify without starting zirv workflow start feature --task "..." [--agent claude] [--built-in-only] [--brainstorm|--no-brainstorm] [--branch ] zirv workflow status [id] # one instance, or the active one; shows brainstorm: on|off, per-step wall-clock, and pinned definition/drift zirv workflow resume # restore as the active workflow zirv workflow context [id] # the current step's resolved skill context zirv workflow artifacts [--json] # committed work-product state zirv workflow agents list|show |dispatch --adapter --prompt zirv workflow team plan "" [--workflow |active] [--dry-run] [--seat ] [--json] zirv workflow team show [--workflow |active] [--json] zirv workflow team brief [--json] # Agent-tool-ready brief for one compiled seat zirv workflow approve # approve the current gated step zirv workflow advance --outcome success|failure zirv workflow review package | run --agent | add | ... zirv workflow maintain scan [--repo ] [--json] zirv workflow stats # local bounded telemetry ``` ### Workflow definitions v2 (issue #542) `zirv workflow list`/`show`/`start` resolve any id against a layered, validated **registry** of `WorkflowDefinitionV2` packs instead of a fixed Rust enum: 1. **Built-ins.** Versioned Zirv packs compiled into the binary (`src/commands/workflow/packs/*.toml`) -- today `feature`, `bugfix`, `refactor`, `spike`, and `review`, reproducing the same step ids, phases, skills, conditions, approvals, and artifacts the pre-#542 engine used. 2. **Operator-global** (`~/.zirv/workflows/*.{toml,yaml,yml}`). Trusted: may replace a built-in id, but only when the file itself sets `override = true`; otherwise a colliding id is ignored with a warning, never silently dropped or silently active. 3. **Repository** (`/.zirv/workflows/*.{toml,yaml,yml}`, gated by `workflow.repo_workflows_enabled`, default `false`). Untrusted: may only ADD a non-colliding id, and additionally can never widen authority beyond what some built-in pack already exercises -- `effects = "external"`, a `repo.write`/`shell.exec`/`network.access`/`agent.spawn` capability no built-in step ever declares, or dropping an approval/validation/ independent-review gate category a same-domain built-in establishes are each refused (dropped with a warning, not a hard failure of the rest of the layer). Symlinked directories/files, path escapes, and oversized (>32 KB) manifests are hard refusals, mirroring `.zirv/skills/`. A definition's fields: stable `id`/`version`/`title`/`description`, free-form `domains` tags and example `triggers`, typed `inputs`/`outputs`, a DAG of `steps` (each with `id`, `title`, `phase`, `skills`, an optional `agent_role`, `capabilities`, `depends_on`, an optional `parallel_group`, `condition`, `approval`, `artifact`, `max_attempts`, `effect`, a human-readable `reason`, and the domain-variant pair `domains`/`overrides_step`), `gates` (approval/ validation/independent-review step-id sets), `limits`, a `failure` policy, a top-level `effects` ceiling, an optional `idempotency` note, and a `completion` contract. Validation (id shape, uniqueness, cycle detection, unreachable-step detection, unknown skill/dependency/gate-reference detection, and the 32 KB size cap) runs before a pack is ever registered; `zirv workflow list`/`show --json` expose the resolved shape and each pack's stable content hash. `zirv workflow start ` executes ANY registry id through the same v2 materialization path (issue #542 chunk 3a) -- not just the five built-in kinds; a step whose `agent_role` names nothing in the (registry-aware) `AgentRegistry` fails the start before any state is written. `materialize_from_definition` prunes steps by `condition` (unchanged semantics), resolves each surviving step's data, and orders the result by `depends_on` with a stable topological sort (ties broken by declaration order); `parallel_group` is carried onto the materialized step as informational metadata for a future concurrent scheduler -- the state machine itself is still the single `current_step` sequence it always was, so two steps sharing a `parallel_group` tag still execute one after the other today. `effect` (`none`/`repository`/`external`) is likewise carried onto the materialized step for downstream effect-aware tooling. **Domain variants (frontend, for now).** A step whose data should differ by domain -- today only `"frontend"` -- is authored as a SEPARATE `[[steps]]` entry with `domains = ["frontend"]` and `overrides_step = ""`, supplying that id's `skills`/`agent_role`/`capabilities`/`approval`/ `artifact`/`max_attempts`/`effect` when the classified profile matches. A variant step is a data donor, not its own DAG node: it must declare no `depends_on` of its own, and the materialized step keeps the CANONICAL id, phase, `depends_on`, `parallel_group` and `condition` from the step it overrides regardless of which variant supplied its data -- switching profile (`zirv workflow reclassify --profile ...`) never changes a step's id. This replaces the old hardcoded `WorkflowProfile`-keyed Rust match table: a new pack adds a `domains`/`overrides_step` entry instead of an engine.rs change. **Brainstorm and deploy-tier tags.** These two overlays are NOT pack fields at all -- they are engine transforms keyed purely on `WorkflowPhase`, so any pack's steps opt in automatically by using the same phase as a built-in: a step with `phase = "intent"` gets the brainstorm/`--no-brainstorm` skill swap (`brainstorm` vs `write-intent`); a step with `phase = "deploy"` gets its `approval` set to `deploy_tier >= staging`, and if the deploy tier is `production` and no step anywhere has `phase = "review"`, a synthetic independent-review step is inserted immediately before the first `phase = "verify"` step as a production-readiness floor. A workflow started from a registry id pins that pack's `id`/`version`/hash (and, for a non-built-in pack, the full definition inline) on the running `WorkflowState`, so update, resume, and orchestrator rollover cannot change the run's meaning silently; `zirv workflow status` prints the pin and a `definition drifted from registry` note when the registry's current copy no longer hashes the same. ### Built-in packs (issue #542 chunks 4-5) | id | group | effects | output | |---|---|---|---| | `feature` | software | repository | `change` | | `bugfix` | software | repository | `change` | | `refactor` | software | repository | `change` | | `spike` | software | repository | `findings` | | `review` | software | none | `disposition` | | `adaptive-work` | (generic fallback) | none | `result` | | `pm-requirements` | pm | none | `brief` | | `pm-status-report` | pm | none | `update` | | `pm-backlog-triage` | pm | none | `backlog-update` | | `pm-cycle-planning` | pm | none | `committed-plan` | | `pm-risk-review` | pm | none | `risk-register` | | `pm-retrospective` | pm | none | `retro-summary` | | `data-question-to-report` | data | none | `report` | | `data-quality-investigation` | data | none | `findings` | | `data-anomaly-investigation` | data | none | `findings` | | `data-recurring-kpi-review` | data | none | `kpi-summary` | | `architecture-decision-record` | architecture | none | `adr` | | `architecture-design-review` | architecture | none | `disposition` | | `architecture-discovery` | architecture | none | `discovery-report` | | `architecture-migration-roadmap` | architecture | none | `roadmap` | | `architecture-threat-scale-cost-review` | architecture | none | `assessment` | | `sre-incident-triage` | sre | none | `triage-report` | | `sre-deploy-or-rollback` | sre | external | `decision-receipt` | | `sre-postmortem` | sre | none | `postmortem` | | `sre-capacity-reliability-review` | sre | none | `review-summary` | | `devops-ci-cd-change` | devops | repository | `change` | | `devops-infrastructure-change` | devops | external | `change-receipt` | | `dependency-upgrade` | software | repository | `change` | | `security-remediation` | software, security | repository | `change` | | `schema-data-migration` | software | repository | `change` | | `performance-investigation` | software | repository | `change` | | `documentation-runbook-change` | software | repository | `change` | Every pack's `effects` is `none` or `repository`, except `devops-infrastructure-change` and `sre-deploy-or-rollback` (issue #542 chunk 5), which declare `external` as their ceiling for the day #539 ships typed cloud/deployment-ops tools -- neither one actually mutates anything outside this checkout TODAY: every step stays `effect = "none"`, the workflow starts read-only, and every mutating step sits behind an explicit operator approval whose `reason` names the missing integration rather than acting through raw/untyped access. A pack that would otherwise need an integration (Kibana logs for `sre-incident-triage`'s diagnosis, a Linear/Jira ticket for `pm-requirements`/`pm-backlog-triage`/`pm-retrospective`, a live metrics/BI pull for `data-recurring-kpi-review`/ `sre-capacity-reliability-review`) says so in the relevant step's `reason` and stops at an explicit approval gate instead of guessing. The repository trust layer (above) refuses `effects = "external"` on ANY repository-provided pack unconditionally, regardless of what a built-in declares -- only zirv's own versioned built-ins may ever reach for it. **How a workflow is chosen.** `zirv workflow start` without an id runs `selection::select_definition` deterministically against the resolved classification and `--task` text -- no model call. A pack scores by matching `--task` text against its `triggers` (3 points each), a `domains` tag appearing in the task text (2 points), and the classified work-domain aligning with a `domains` entry (1 point); scores below a floor are dropped, and the highest-scoring survivor wins, ties broken toward fewer external effects and then alphabetically by id -- both the score and the tie-break are recorded in `Selection::reasons`/`alternatives` and printed alongside the started workflow (`--json` adds a `selection` object; `workflow classify --json` adds the same field as a preview with no side effect). A legacy intent (`Feature`/`BugFix`/`Refactor`/`Spike`/`Review`) that classification already produces with high confidence selects that kind's pack outright, skipping scoring entirely, so the five pre-#542 workflows keep their exact historical selection behavior. Nothing above the floor selects `adaptive-work`, a small five-step (understand/plan/execute/validate/present) fallback pack with no domain/trigger tags of its own (so it never competes for another pack's task) that prunes itself down to three steps (understand/execute/present) for a trivial task. An explicit id (`zirv workflow start --task ...`, or the native `/workflow ` slash command) always wins outright with no selection performed at all, and that choice survives `resume`/`reclassify` as the pinned `definition` on the running state. A bounded model tie-break for a close, ambiguous score is deferred -- the deterministic algorithm's every decision is already explainable from `reasons` alone, which a model call would only obscure for the near-certain-floor cases it would actually apply to; see the design note for the full reasoning. ### Lifecycle and artifacts A workflow instance moves through `intent → spec → plan → implement → test → review → verify → deploy`. Ceremony is proportional to classification: a trivial bugfix skips straight to the debug/test/verify spine, while substantial or high-risk feature/refactor work gains intent, plan (and, for substantial/high-risk work, spec) artifact gates plus an approval-gated design step. Frontend work overlays the same engine rather than running a separate one, selected automatically from task language and changed frontend paths. Artifact steps write fixed templates to `.zirv/work//` (`intent.md`, `spec.md`, `plan.md`, ...) committed to the repository. `zirv workflow approve` refuses an untouched template, then pins the accepted file's SHA-256 digest and timestamp in private state; a later step folds only accepted, hash-matching artifacts into prompt context. Editing or deleting an accepted file after the fact reopens its acceptance gate and invalidates later completed steps, so implementation can never silently proceed against a plan that changed underneath it — `zirv workflow artifacts ` shows pending/accepted/drifted state directly. Classification is re-measured (never downgraded) whenever a workflow advances into a review or verify step, so a review/verify gate the initial `workflow start` measurement missed (an empty tree, before any code existed) still gets added once the real change exists. ### Linked worktrees A workflow started in a repository's main checkout can be found from, and gated against evidence in, a `git worktree add`-linked sibling of it (and vice versa) -- but workflow state, and evidence, are never merged into one shared identity: every piece of per-repository state (workflow state, the active-workflow pointer, verification reports, crash witnesses, handoffs, telemetry, test baselines, mail, ...) stays keyed by the LITERAL checkout a session, process, or `zirv test changed` run actually used. Two independent lookup/relatedness rules make cross-worktree orchestration work without that: - **Finding a workflow by id or by "the active one".** `zirv workflow status|advance|review package --repo ` looks in ``'s own state directory first, then in each of its sibling checkouts (`git worktree list`) for that id -- an explicit id is never ambiguous, so this is safe to widen to every sibling. Bare `zirv workflow status` (no id) is different: it reads ``'s own active-workflow pointer first, and if `` has none, falls back ONLY to the MAIN checkout's own pointer -- never an arbitrary other sibling. This is what lets a worker worktree with no workflow of its own inherit the orchestrator's, while two workers each running their own `zirv workflow start` in their own worktrees never collide or clobber one another's active pointer. - **The `Test`/`Verify` evidence gate's relatedness check.** A workflow records the branch it gates (`WorkflowState.branch`: `--branch ` at `start`, or the checkout's own current branch when not given), and every `zirv test changed`/`zirv verify` run records the branch it was produced on. `zirv test changed` always writes its evidence under the literal checkout it ran in, so concurrent runs in sibling worktrees never clobber each other's evidence. The gate widens only its read: if the checkout it is evaluated from has no fresh, passing evidence of its own, it also checks every sibling checkout's own evidence against that sibling's own tree -- but ONLY accepts a sibling whose recorded branch matches the workflow's own recorded branch exactly. A sibling with fresh, passing evidence on a *different* branch never opens this gate, no matter how fresh or passing. Together: start the workflow in the main checkout (`--branch worker/feature-x` if that main checkout is not itself on the worker's branch), have the worker implement and run `zirv test changed` in `/.claude/worktrees/` (checked out on `worker/feature-x`), then `zirv workflow advance --outcome success` from the main checkout (or `--repo /.claude/worktrees/`, either finds the same workflow) -- the gate accepts the worktree's evidence because its recorded branch matches, not merely because it happens to be fresh and passing for someone. `zirv workflow classify`/`start --branch ` diffs that branch against its own base as refs (`git diff `), not `--repo`'s working tree -- necessary because the checkout given as `--repo` need not have `` checked out at all. Without `--branch`, classification is unchanged: `git diff --numstat ` already measures whichever repository it is given, so pointed at a worktree (`--repo` or plain cwd) it already saw that worktree's own branch diff (uncommitted edits included) against its base. The one unsupported edge case is a main checkout whose `.git` was relocated with `git init --separate-git-dir=...`; its worktree siblings are not resolved to it as "the main checkout" for the active-pointer fallback above. ### Deploy tiers `[workflow.deploy] tier = "development" | "staging" | "production"` in `~/.zirv/ctx.toml` (`ZIRV_CTX_WORKFLOW_DEPLOY_TIER` for the final override) is operator-only; a repository may set only `workflow.deploy.minimum_tier`, and a running workflow's resolved tier only ever ratchets upward: | Tier | Deploy step | |---|---| | `development` | Auto-advances once test/review/verify evidence passes | | `staging` | Requires an explicit `zirv workflow approve` on the deploy step | | `production` | Requires approval, plus at least one fresh independent `reviewer`-seat run and fresh final `zirv verify` evidence; an open finding or stale evidence blocks it outright | ### Workflow adoption `[workflow] adoption = "off" | "advise" | "nudge" | "enforce"` in `~/.zirv/ctx.toml` (`ZIRV_CTX_WORKFLOW_ADOPTION` for the final override) is operator-only and detects a session that has done "substantial" edit work -- at least 5 edit-like tool calls, or at least 1 edit-like call over 12 turns -- with no active `zirv workflow`: | Level | Behavior | |---|---| | `off` | No detection, no nudge, no gate. | | `advise` | A one-time nudge rides the Stop hook's `systemMessage` once substantial work is detected. | | `nudge` (default) | The same nudge, repeated every 5 turns while the session stays substantial with no active workflow, and also surfaced on the next prompt (`UserPromptSubmit`) if a workflow still has not started. | | `enforce` | The `nudge` behavior, plus `zirv agent` (`ctx::agent::run_with`) refuses to dispatch for a session recorded as substantial with no active workflow, until one is started. | ### Agent registry Workflow seats are provider-neutral data, not harness-specific plugins: a `WorkflowStep.agent` addresses one by id. The prebuilt roster has twelve built-in manifests, each mapped to the closed native team role (`ctx::team::TeamRole`) whose authority it holds — a manifest's `role` field is a display label, never an authorization grant: | id | team role | write posture | deliverable | |---|---|---|---| | `implementer` | implementer | writable | bounded implementation unit + evidence | | `reviewer` | reviewer | read-only | independent structured findings | | `doc-keeper` | implementer | writable | synchronized documentation | | `security-scanner` | reviewer | read-only | security/trust-boundary findings | | `explorer` | researcher | read-only | bounded investigation findings | | `researcher` | researcher | read-only | sourced external/repo research | | `planner` | planner | read-only | dependency-ordered task breakdown | | `architect` | planner | read-only | ADR-quality decision record | | `debugger` | implementer | writable | reproduction test + root-cause note | | `tester` | tester | read-only | independent test results + triage | | `data-analyst` | researcher | read-only | reproducible data/query analysis | | `devops-sre` | implementer | writable | CI/CD, infrastructure, deployment change | `reviewer`/`security-scanner`/`explorer` are pinned read-only by their own adapter. `~/.zirv/agents/*` (operator-global) may replace a built-in seat; `.zirv/agents/*` (repository) is disabled unless the operator sets `workflow.repo_agents_enabled`, and even then may only add non-colliding ids — a repository manifest can never rewrite `reviewer` or grant itself capabilities it does not already have. An operator/repository manifest may omit `team_role`; it is then derived from `read_only` alone (read-only → `researcher`, writable → `implementer`), so a manifest written before this field existed keeps loading unchanged. A manifest's `skills` list attaches existing `zirv skill` ids to a seat by reference (never by copying instruction text); an unknown or version-mismatched reference is refused by `AgentRegistry::validate_against`. `zirv workflow agents list|show` inspects the resolved registry and provenance; `zirv workflow agents dispatch --adapter --prompt ` launches that seat directly. ### Team composition `zirv workflow team plan "" [--workflow |active] [--dry-run] [--seat ] [--json]` compiles the smallest capable team for a request: it classifies the objective exactly like `zirv workflow classify`, derives the minimal execution profile (`intent`/`complexity`/`risk` → `Direct`/`Bounded`/`Orchestrated`, plus which independent-review/test/security gates apply), and deterministically selects seats from the agent roster above. Unless `--dry-run`, the compiled `TeamPlan` is stored on the active (or `--workflow`-named) workflow; with no active workflow it prints without storing and says so. `zirv workflow team show [--workflow |active] [--json]` re-prints the stored plan, and `zirv workflow team brief [--json]` prints an Agent-tool-ready brief for one compiled seat — the seat manifest's own instructions plus the bodies of only the skills that seat's manifest references, never the whole skill catalogue. `--seat ` bypasses the proportional rules for one explicit seat, but still passes the same capability/team-role/route checks a compiled seat does — explicit selection never bypasses policy. **How a team is chosen.** Mechanical/trivial work (`Direct` execution) spawns nobody. Bounded work gets one implementer (or the matching domain specialist); an independent reviewer joins only when the validation profile requires it. A bug fix gets a `debugger` (reproduction test + root-cause note) then an `implementer` scoped to that root cause. Substantial features add a `planner`; architectural ones also add an `architect`; implementers split one per claim-boundary group (by TOP-LEVEL path from the classification's own changed-path list when it has one, capped by the profile's fan-out limit — a count-based bucket only when it does not). Security/data/docs/dev-ops signals in the request or the diff add the matching specialist with a concrete deliverable — never a duplicate of a gate already covered (a security signal is satisfied by the same independent `security-scanner` gate risk alone would require, not a second seat). Independent review/test/security seats never share a writer's claim or worktree and depend only on the writers having finished, never on reading their transcript. An unknown manifest or a team role with no eligible route is never invented into a plan: the seat is omitted with a stated reason, and the plan still compiles. Fan-out (`Bounded`: 2, `Orchestrated`: 6) and dependency-depth limits are enforced before the plan is returned — a plan that would exceed them is refused outright, not silently truncated. The team compiler never spawns every role; see `docs/design/2026-09-15-native-team-composition.md` for the full rule set and what is deferred to #537's full execution profile. A native coordinator or sub-orchestrator seat compiles the same plan itself with the `team_plan` tool, and the native pane's `/agents`, `/agent` and `/team` slash commands — see "The native meta-orchestrator" below. ### Maintain loop `zirv workflow maintain scan` is an invoked scanner, not a daemon: it runs every operator-configured deterministic detector once, reading detector commands only from `[workflow.maintain.detectors.]` in the operator's own `~/.zirv/ctx.toml` (repository config cannot define one). Each detector is a bounded command judged by exit code/timeout or a stdout line-count threshold; zirv retains only exit code, timeout state, and line/byte counts, never detector command or output bodies. A breach parks one bounded incident workflow at its Intent acceptance gate (`.zirv/work//intent.md`, committed with detector metadata) and, when operator-only `[report] repository = "owner/repo"` is configured, auto-files a title-deduplicated GitHub issue. A clean scan clears the incident marker, so a later recurrence opens fresh. ### Frontend quality Frontend tasks are selected automatically from task and path evidence. Zirv derives a repository-specific design profile, classifies each surface as persuade/operate/read/experience, and requires a product-grounded design thesis, signature, justified risk, system, complete user journey, and resilient state matrix before implementation. A built-in craft floor plus phase skills drive the work; a 44-rule offline detector checks deterministic accessibility, UX, responsive, content, motion, internationalization, media, performance, and anti-slop hazards. Zirv starts and cleans up the discovered dev server, captures narrow/intermediate/wide screenshots, and requires a fresh AI review with 13 explicit UI/UX scores, each at least 4/5. The score is produced by an isolated, read-only Zirv reviewer rather than accepted from CLI arguments. `zirv frontend capabilities --agent [--json]` reports the same provider-neutral skill/provenance contract and logical capability matrix `zirv skill show --agent` reports elsewhere. There is no frontend init command or questionnaire: the active agent owns routine design, rendering, and review decisions. Missing, stale, truncated, unavailable, weak, or failed evidence cannot advance frontend test, review, or verify gates. Detector waivers are schema-versioned TOML in `.zirv/frontend-waivers.toml` or the operator-owned `~/.zirv/frontend-waivers.toml`. Every waiver names a rule, an exact path or `/**` prefix, an optional evidence value, and a reason. Repository waivers are advisory only: they can disposition advisory craft findings, but only the operator-owned file can waive a blocking accessibility finding. [Glubiz/zirv-generic-frontend](https://github.com/Glubiz/zirv-generic-frontend) is the reference frontend template built against this contract — it is also the source behind [cli.zirv.io](https://cli.zirv.io), the site rendering this README. Optional repository checks live in `.zirv/verify.toml`. Custom skills may be shared under `.zirv/skills/` or kept operator-global under `~/.zirv/skills/`; `zirv skill list/show` reports the winning source and `--built-in-only` disables custom layers. The same flag on `workflow start` persists that choice across resume and prompt composition. Repository skills are untrusted: they can request logical capabilities but never grant themselves filesystem, shell, network, or other permissions. Use `zirv workflow review package ` for a compact diff/test review input, `zirv artifact render ` for stable static artifact references, and `zirv workflow stats` for local bounded telemetry. Review results are persisted from a strict bounded JSON contract and fix/re-review stops after three rounds. Interactive artifact fallback obeys canonical policy (`ask` needs `--approve`), and supervised Claude/Codex workflows attribute available transcript token deltas automatically. Telemetry excludes prompts, source code, diffs, command output, and model responses by construction. ## Context Management (zirv ctx) `zirv ctx` watches Claude Code sessions for context rot and intervenes before quality drops: it advises, compacts early, or restarts the session with a distilled handoff. Scoring is deterministic, and every decision is logged. **Agent support.** Claude Code and Codex are both supported for supervised sessions, but not to the same depth. Claude Code gets the full feature set: event parsing, a rot score, turn signals and an injected system prompt. Codex launches and supervises fine -- `--agent codex` succeeds both when `codex` resolves to a real binary and when nothing named `codex` is installed at all (that case is left to fail at spawn time with the OS's own "not found"), the same contract `--agent claude` gives claude -- but with an honestly degraded surface, because the pieces below were never verified against an authenticated CLI: - No event parsing, so no rot score and no structural context for codex sessions (`parse_events`/`structural_context` stay empty). - No usage source: a codex session's usage reads `openai: no usage source` rather than a real reading. - Lifecycle hooks are available and `zirv setup` registers them, but event parsing is still absent, so Codex cannot yet produce a meaningful rot score or structural context from its rollout. - Direct launches receive Zirv's composed prompt through Codex's official per-run `developer_instructions` override. Windows command/PowerShell shims stay fail-closed because inline repository text would be reparsed by a shell. That direct path covers interactive orchestrators and headless workers. Shell-shim launches retain the task-prompt fallback for mail and worker instructions but intentionally withhold repository-authored system layers. Full event support is tracked in [issue #11](https://github.com/Glubiz/zirv-cli/issues/11). **Platform support.** Supervision is unix only. `wrap` and `exec` need unix domain sockets for turn signals, and `wrap` additionally needs raw terminal mode and the terminal's window size. On Windows those degrade rather than fail: `exec` falls back to polling the transcript, and `wrap` runs as pure passthrough with the inner terminal pinned at 80x24. Everything else, including `score`, `handoff` and `status`, works on all three platforms. ### Verbs | Command | What it does | |---|---| | `zirv ctx score --transcript ` | Rot-scores a transcript and prints JSON | | `zirv ctx loop --prompt ` | Runs a fresh headless session per cycle, so the orchestrator cannot rot | | `zirv ctx exec -- ` | Supervises one headless run: kill, distill, restart | | `zirv ctx wrap -- claude` | Supervises an interactive TUI through a PTY | | `zirv ctx handoff --transcript ` | Distills a handoff and stores it | | `zirv ctx resume` | Starts a clean session with the latest handoff injected | | `zirv ctx hook ` | Agent hook entrypoints; `install ` wires zirv's own guard/compaction hooks into a non-claude agent's native hooks file (copilot, droid, gemini) | | `zirv ctx status [--json] [--agents]` | Shows supervised sessions, the resolved chat agent, unread mail, recent decisions, handoffs, and (issue #358) a cross-harness capacity/pool section; `--json` emits the pool view plus the orchestrator seat as structured JSON; `--agents` (issue #490) emits the native dashboard's own agent/task overview, usage-and-health provenance strip and a `limitations` list, built from the identical reducers the TUI renders through | | `zirv ctx mcp serve [--stdio] [--repo ] [--session ]` | Serves seven read-only MCP tools for one repository/worktree; optionally binds inbox reads to a registered session. See [MCP bridge](#mcp-bridge) | | `zirv ctx mcp doctor [--repo ] [--session ] [--timeout-seconds <1..60>]` | Launches this executable as a stdio server, checks tool discovery and a snapshot call, and prints JSON; default deadline 10 seconds | | `zirv ctx usage` | Shows usage-window state, or `usage tee` to collect it from the statusline | | `zirv ctx optimize` | Reports redundancy, contradictions and dead references in the files that steer your sessions | | `zirv ctx provider init\|list\|check\|credential set` | Coming soon; native provider setup is unavailable in this release | | `zirv ctx chat [--pin-harness]` | Starts an interactive orchestrator session on the resolved adapter (also `zirv chat`, or bare `zirv`; see [Just Run `zirv`](#just-run-zirv)). `--pin-harness` (same as `ZIRV_CTX_SEAT_PIN=1`) opts this session's orchestrator seat out of automatic rollover (issue #358) — a manual `zirv ctx handover` still works on a pinned seat. `--runtime native` reports coming soon and refuses to start — see [The native conversation pane](#the-native-conversation-pane) | | `zirv ctx agent ` | Delegates one task to a supervised worker on another enabled harness -- a dashboard pane when one is live, otherwise inline in this terminal; `--runtime native` reports coming soon and refuses to start (also `zirv agent`) | | `zirv ctx send [--to-session ]` / `zirv ctx inbox` | Leaves or reads short notes between agent sessions on this machine, scoped to the repo, optionally addressed to one live session | | `zirv ctx nudge --message ` | Wakes a live supervised session early with a message, instead of waiting for it to poll | | `zirv ctx remember --key --text ` / `zirv ctx recall` / `zirv ctx forget ` | Reads and writes this repo's cross-session memory bank | | `zirv ctx handover [--agent ] [--model ] [--dry-run] [--force]` | Swaps the orchestrator seat's harness or model in place mid-session, carrying a handoff packet across the swap — see [Cross-harness fallback and handover](#cross-harness-fallback-and-handover) below | | `zirv ctx permissions audit\|compile\|propose` | Audits, compiles, or (operator opt-in) proposes command-permission approvals from recent transcripts — see [Permission auditing](#permission-auditing-and-safe-list-proposals-issue-178) below | | `zirv ctx api schema [--json]` / `zirv ctx api serve` / `zirv ctx api call ` | Prints the local runtime protocol v1 contract, binds its endpoint, or calls one method over it — see [Runtime protocol v1](#runtime-protocol-v1-zirv-ctx-api) below | | `zirv ctx capabilities [--probe] [--require ] [--json]` | Reports every configured integration (MCP, web search/fetch, browser, diagnostics, artifact and frontend rendering) as available, unavailable or unverified, with the diagnosis for anything missing — see [Native configured capabilities](#native-configured-capabilities) below | | `zirv ctx doctor [--role ] [--live] [--json]` | Diagnoses native readiness: the resolved backend and route per role, and every problem classified as missing auth material, inaccessible model, missing tool, unsupported isolation, service failure or upstream entitlement limit — see [Native setup, diagnosis and rollback](#native-setup-diagnosis-and-rollback) below | | `zirv ctx config migrate [--to harness\|native] [--downgrade] [--dry-run]` | Versions `~/.zirv/ctx.toml` with a backup and a documented way back; idempotent in both directions — see [Native setup, diagnosis and rollback](#native-setup-diagnosis-and-rollback) below | ### Runtime backends Every supervised session picks a `RuntimeKind`: `harness` (the default — an `AgentAdapter` spawns the real Claude Code/Codex/etc. binary through the `supervise::spawn_tapped` chokepoint) or `native` (zirv conducts the model/tool conversation itself over a direct provider route, with no vendor CLI in the loop, on the native-runtime roadmap, [issue #469](https://github.com/Glubiz/zirv-cli/issues/469)). Native mode is always explicit and opt-in: nothing detects it, and an unrecognised runtime name is an error rather than a silent fallback to `harness`. The session registry (`sessions::Record`), the orchestrator seat (`seat::Seat`), and the `.conversation` marker each persist which `RuntimeKind` they were started under, defaulting to `harness` so every record written by an older binary still parses. A resume matches agent, session id, **and** runtime before reusing a recorded conversation, so a session can never silently cross from a harness-backed conversation to a native one (or back). The in-process wire shapes a `RuntimeBackend` speaks — commands, events (`EventEnvelope`'s wire keys are `version, revision, session_id, generation, event`), replies, and a structured `ErrorCode` (`runtime::RuntimeError`'s `Unsupported`/`UnknownSession`/`Busy`/`StaleGeneration` map 1:1 to it; any other backend error maps to `ErrorCode::Backend`) — are versioned from day one as protocol v1 (`src/commands/ctx/runtime/protocol.rs`), with frozen example payloads under `tests/fixtures/runtime/v1/` so a later change can't silently break what v1 meant. No daemon or socket exists yet; everything is in-process. Native sessions use an authoritative schema-v1 SQLite/WAL journal at `/native-journal.sqlite`. It records acknowledged inputs, complete typed assistant blocks, exact tool calls, execution-state receipts, usage, task receipts, portable checkpoints, and monotonic sequence/generation IDs. Bounded streaming frames are transient until a completed-message barrier commits them; incomplete tool arguments never enter the executable event log. An execution interrupted after it starts becomes `outcome_unknown` and must be reconciled rather than blindly replayed. Provider continuation envelopes live outside portable replay and are readable only with the same route, provider, endpoint, account, protocol, vendor, and model identity. The journal projects committed facts into the existing `NormalizedEvent` scoring vocabulary, so the pure rot engine is unchanged. The storage/recovery contract is documented in [`docs/design/2026-09-11-native-conversation-journal.md`](docs/design/2026-09-11-native-conversation-journal.md). The journal does not replace the session registry, seat, task, mailbox, workflow, or policy stores. Those remain the single authorities for their own state and the journal records only their stable identifiers and receipts. Existing harness transcripts are untouched, and older Zirv builds simply ignore the additional private database. Native effects are admitted through one Zirv-owned execution broker before any tool implementation can touch the machine. The broker reloads the current canonical policy, fences the persisted native seat generation, resolves paths through symlinks/junctions, requires the writer permit for the exact linked worktree, protects Zirv/provider credential paths, and strips credential-bearing environment variables from subprocesses. Interactive approvals are signed and bound to the exact action arguments, canonical path targets, task/session/role/generation, resource claims, and current policy; they cannot be reused after any of those facts changes, and a headless ask is an explicit refusal. Arbitrary native processes require a verified OS isolation launcher. Linux uses bubblewrap when `bwrap` is installed, macOS uses the built-in Seatbelt launcher, and Windows requires Zirv's restricted-token/AppContainer helper; a Job Object is cleanup, not containment. A missing mechanism is reported as unsupported and never falls back to an unsandboxed spawn. Consequently Zirv does not advertise native coding support on a host until that host's enforcement probe passes. The contract and current platform evidence are in [`docs/design/2026-09-11-native-execution-enforcement.md`](docs/design/2026-09-11-native-execution-enforcement.md). The native coding tool service now exposes a closed, schema-described registry for file ranges, directory/glob/text search, atomic writes, exact-content patches, process start/poll/wait/input/termination, and stored output retrieval. JSON is fully decoded into a typed request before the N04 broker sees an action; unknown fields, incomplete payloads, empty handles, and oversized arguments are refused without executing anything. File writes require idempotency keys and existing files require SHA-256 preconditions. Patches additionally require exact occurrence counts. UTF-8 BOM, UTF-16 endianness, existing permissions, and consistent line endings survive an atomic replacement; binary and image reads report their media type and hash instead of corrupting bytes through a text decoder. Processes use explicit argv by default. Shell mode is a separate `shell_script` shape and receives conservative git/destructive effects. Requested network, outside-write, and git-metadata access can only add broker checks and sandbox bindings; omitting them leaves those effects unavailable. Non-interactive commands use pipes, interactive commands alone use PTY/ConPTY, and every live command has an opaque handle for bounded polling, waiting, input, and process-tree termination. Raw stdout/stderr streams once into the existing output store, so large and non-UTF-8 failures return a bounded summary plus `output_id` rather than filling model context. Process handles are intentionally machine-process-local and are not claimed to survive a runtime crash. N03 journal states make an interrupted effect `outcome_unknown`; receipts mark each tool `safe`, `reconcile`, or `never_after_start`, preventing blind replay of remote mutations. The exact contract is documented in [`docs/design/2026-09-11-native-coding-tools.md`](docs/design/2026-09-11-native-coding-tools.md). Native context is compiled independently from provider delivery. Zirv emits typed instruction and data messages with source/trust provenance, a stable methodology prefix, an output-token reservation, and explicit records for every included, truncated, referenced, or excluded source. Operator and Zirv methodology are instructions; repository prompts, canonical context, shared memory, and repository skills remain untrusted data and cannot grant authority. Only the active workflow step's skills and bounded relevant memory are selected. Required task, workflow, and evidence handles either fit or the compile fails—optional prose can never silently crowd them out. The native registry also exposes `memory_recall`, `memory_remember`, `memory_forget`, and zero-model `context_search`. They reuse the existing locked memory/search stores and opaque `output_read` evidence handles; shared memory writes still cross the repository policy and writer-lease boundary. No Claude/Codex prompt file, helper process, or `AgentAdapter` participates in native compilation. The contract is documented in [`docs/design/2026-09-12-native-context-compiler.md`](docs/design/2026-09-12-native-context-compiler.md). The first direct-model adapter speaks Anthropic's Messages API over raw HTTPS/SSE behind a provider-neutral transport contract. It sends only Zirv's compiled system/data messages and public tool schemas, reassembles interleaved text, tool-use, thinking signatures, and redacted-thinking blocks, and keeps all tool execution in Zirv. Exact model IDs and model-specific thinking/effort controls are checked locally; authentication, entitlement, model access, context limits, rate limits, overloads, timeouts, cancellation, refusals, and usage/cache classes remain typed. Opaque continuation material is replayed verbatim but excluded from diagnostics. Versioned fixtures and an ignored, credential-gated live contract test (`cargo test --ignored live_anthropic_messages_contract`, needing `ANTHROPIC_API_KEY` and `ZIRV_ANTHROPIC_LIVE_MODEL` — an exact, entitled model id) cover the provider boundary; N09 owns wiring this adapter into the durable agent loop. The transport contract is in [`docs/design/2026-09-12-native-anthropic-provider.md`](docs/design/2026-09-12-native-anthropic-provider.md). The second direct-model adapter speaks OpenAI's Responses API the same way -- raw HTTPS/SSE to `POST /v1/responses`, with no Codex binary, Codex SDK, or Codex App Server in the path. Zirv owns the conversation: every request sends `store: false` with the full local history, and `previous_response_id` is never used as durable state. Reasoning items keep their id and encrypted content verbatim for replay and are refused when they reach a provider that cannot carry them; function calls commit only from a completed output item whose arguments match the streamed ones, an incomplete response drops every function call and records why, and a stream without a terminal event is a typed error rather than a completion. Only a Platform API key is accepted: a ChatGPT/Codex subscription login is refused with an entitlement error instead of being billed as API usage. Controls are validated against exact API model ids -- a Codex harness model id is not assumed to be a Responses model. The routes are fixture-verified, with live validation available through an ignored, credential-gated test (`cargo test --ignored live_openai_responses_contract`, needing `OPENAI_API_KEY` and `ZIRV_OPENAI_LIVE_MODEL`); the contract is in [`docs/design/2026-09-12-native-openai-provider.md`](docs/design/2026-09-12-native-openai-provider.md). The third direct-model adapter speaks Google's Gemini `generateContent`/ `streamGenerateContent` API over the same raw HTTPS/SSE transport, behind two explicitly versioned protocol profiles: the Gemini Developer API (API key, `generativelanguage.googleapis.com`, `v1beta`, where thinking and function-calling controls live) and Vertex AI (an OAuth access-token credential plus an explicit project and location, addressed at `.../v1/projects/{project}/locations/{location}/publishers/google/models/ {model}`). Token acquisition for the Vertex profile is a separate credential-class concern from payload transformation. Parallel function calls and `thoughtSignature` continuation metadata -- including signature-only reasoning with no visible thought text -- are preserved verbatim as opaque data and rejected before transport if they carry another provider's shape. Safety blocks (`SAFETY`/`RECITATION`/`PROHIBITED_CONTENT`/...) commit as a typed refusal outcome, never prose; quota errors, invalid project/location, authentication, model access, transport and context-overflow failures map onto the same typed failure classes the other two providers use. A Gemini CLI OAuth login (`~/.gemini`) is refused outright, by path and by credential shape, with an actionable `Entitlement` failure -- the same posture as OpenAI's subscription refusal. The Vertex profile is declared but only just promoted from `Support::Planned` to `Support::Native` in N02's capability table; both routes are fixture-verified, with live validation of the Developer API profile available through an ignored, credential-gated test (`cargo test --ignored live_google_generative_ai_contract`, needing `GEMINI_API_KEY` and `ZIRV_GOOGLE_LIVE_MODEL`). The contract, and how to add or retire a protocol profile, is in [`docs/design/2026-09-13-native-google-provider.md`](docs/design/2026-09-13-native-google-provider.md). #### The native agent loop > Coming soon: native execution is unavailable in this release. `zirv ctx exec --runtime native` runs a whole session without a coding harness installed: ``` zirv ctx exec --runtime native --prompt "fix the failing test" zirv ctx exec --runtime native --route work-sonnet --role worker -- fix the failing test zirv ctx exec --runtime native --resume 9d2f… --prompt "now update the docs" ``` Flags: `--runtime harness|native` (default `configured` — the operator's own `~/.zirv/ctx.toml` `[runtime]` table decides; unconfigured resolves to `harness` — see [Choosing the default](#native-setup-diagnosis-and-rollback)), `--route ` (a `[route]` from the operator's native provider configuration; defaults to the `[roles]` entry for `--role`), `--role ` (default `worker`; selects that default route and the repository-write posture its tools run under), `--resume ` (continue a stored native session — see below; a resume needs no fresh prompt). `--budget-tokens`, `--max-tool-calls` and `--timeout-secs` apply as the loop's own ceilings (issue #637: `--budget-tokens` checkpoints once at `agent::BUDGET_SOFT_FRACTION` of the ceiling and stops at it, `status:"limit_reached"`, `limit:"tokens"`, the same exit code the harness path's own budget ceiling uses). The ceiling counts input + cache-creation + cache-read + output tokens, the same four classes the harness path's own `--budget-tokens` sums (harness parity); the final status's own `reconciliation.billable_tokens` counts only input + output, so the two figures differ whenever cache tokens are in play. `--agent`, `--transcript`, `--session-id` and `--max-restarts` are harness-runtime flags and are **refused** here, not ignored: a native session supervises no external process, has no transcript to score and nothing to restart. The command prints one structured JSON final status (`schema_version`, `status`, the actual route/provider/endpoint/account, the configured **and** served model, turn/request/tool counts, usage, evidence, and any incomplete or outcome-unknown tools) and exits on the same supervisor exit codes. `--view json|plain` (default `json`, contract unchanged) — `plain` additionally renders the finished session's transcript, after that same JSON status, through the dashboard native pane's own non-ratatui renderer (issue #480): streaming text/markdown, tool calls, diffs, test outcomes, artifact links and errors, readable in a piped log or a terminal too small for the dashboard, with no `ratatui` required. `--resume ` does the four things a continuation owes before it may run, in this order: it reads the stored session (a session this journal has never heard of is an error, never an invented one), checks it was started in **this** repository (issue #639 — the canonical repository root is recorded once at session start and never rewritten; `--resume` from any other root is refused, naming the recorded origin, before anything else below runs or mutates the journal at all — resume from the same repository is unaffected), converts every execution whose last durable state is `started` to `outcome_unknown` — an effect that began and never reported is **never** silently retried — and then advances the generation, which fences the previous one out of both the journal and the execution broker. Anything still holding the old generation (a half-dead process, a stale handle) is refused from that point on. Two operator-only flags make a whole native session runnable with no provider configured at all: `--provider fixture:` replays a deterministic provider script instead of calling a model, and `--fixture-tools ` supplies the tool receipts it runs against (without it every tool call reports a fixture failure rather than touching the machine). Both are command-line flags and nothing else — no configuration layer, least of all a repository's, can set them. This is the same fixture machinery the loop's own tests use, so `zirv ctx exec --runtime native --provider fixture:…` exercises the shipped code path end to end without a credential, a network call or an installed harness. Inside, an explicit session/turn/request/tool state machine drives the cycle. The assistant message and its complete tool calls are committed to the journal **before** any tool preflight, so a truncated argument stream can never become an effect. Independent (read-only) calls may be scheduled ahead of mutating ones, but results always go back in the provider's own declared order, keyed by call id. Every input is durably acknowledged the moment it is accepted and joins the conversation at an explicit delivery boundary — between requests in a turn, or between turns, never mid-stream or mid-tool; anything not yet delivered is reported as `queued_input` rather than dropped. An interrupt cancels the in-flight stream, every unstarted tool and the remaining turns, but never an effect already in progress: that becomes `outcome_unknown` and must be reconciled before any retry. Response retries (re-sending a request that committed nothing) are budgeted separately from tool-effect retries, and only a tool whose own contract says `safe` is ever re-run. A model's finish token cannot produce `completed` while an execution is incomplete, an outcome is unknown, an acknowledged input is undelivered, or a lifecycle gate objects. Every lifecycle decision the loop makes — before-tool admission, after-tool result disposition, prompt notes, stop, notification classification, owed verification — comes from `ctx::lifecycle`, the shared service `hook.rs` now translates its harness payloads into. The native path calls it directly: no hook process, no harness binary, no PATH probe. Deterministic fixture provider/tool scripts under `tests/fixtures/runtime/native/` prove loop correctness for both primary provider shapes without a paid call. The contract is in [`docs/design/2026-09-13-native-agent-loop.md`](docs/design/2026-09-13-native-agent-loop.md). #### The native conversation pane > Coming soon: native execution is unavailable in this release. The native conversation pane is **coming soon**. `zirv native`, including `zirv native --help`, only prints the coming-soon notice and exits without loading configuration, opening a dashboard, or starting a session. Native flags such as `zirv chat --runtime native` fail with the same explanation. `zirv commands --json` lists `zirv native` as an informational, non-mutating placeholder with no activation flags. `zirv chat` remains the existing harness. The following describes the implementation retained for a future release. `zirv chat` takes no `--route` or `--view` flag (those are `zirv ctx exec --runtime native`'s own) — the native pane always spends the `orchestrator` role's route (`[runtime.roles].orchestrator`, or `[roles].orchestrator` in `~/.zirv/native.toml` with no per-role runtime override). `--runtime native` refuses every wrapped-harness-only flag (`--agent`, `--simple`, `--resume`, `--pin-harness`, a trailing `extra` argv) rather than silently ignoring them, and refuses without an interactive terminal on both stdin and stdout. The transcript renders Claude Code-style: assistant text and tool calls as `⏺` bullets, each tool result as an indented `⎿` line ("(ctrl+r to expand)" while collapsed), user turns as `>` lines, and diffs with an old/new line-number gutter. While a turn runs, an activity line (spinner, a rotating verb, real elapsed time, a running token count, "esc to interrupt") appears at the tail of the transcript. The bottom status line shows the actual model, route, runtime and billing class, one of seven states (generating, executing, waiting, blocked, cancelled, failed, or completed with an unread result), an estimated "context left N%" (recorded usage against the route's declared context window — not the compaction budget's own accounting), the repo path and, when known, the checked-out git branch. **Composer key contract:** `Enter` submits (or, mid-turn, steers — written straight to the journal and picked up at the next turn boundary, the same mechanism `NativeLoop::queued_input` already re-polls between turns; a blocked/not-yet-ready submission is queued instead, and queued input is **never** treated as an approval answer). `Shift+Enter`/`Alt+Enter` insert a newline. `Up`/`Down` browse submit history only at the first/last line of the draft. `Esc` interrupts the current turn without quitting. `Ctrl+C` no longer interrupts by itself — one press arms a quit confirmation, and a second `Ctrl+C` within 2 seconds of the first quits; `Ctrl+Q` still quits immediately (persisting the draft first). `Ctrl+R` toggles the most recently rendered tool call's expanded state from either region. `Shift+Tab` cycles a composer mode label (`default`/`accept-edits`/`plan`) shown on the hint line — **decorative only**: no submit path reads it back to change approval or write behaviour yet. `Tab` swaps focus between the composer and the transcript, where `Up`/`Down`/`PageUp`/`PageDown`/`Home`/`End` scroll (scrolling up disengages auto-follow; it re-engages at the bottom) and `e`/`Enter` expands or collapses the most recent tool call. A `/`-prefixed submission is a pane-local command, never a turn: `/clear` drops the queued backlog, `/help` and `/status` report the key contract and the live session facts, `/compact` is an honest inert stub. `/agents` lists the resolved agent roster; `/agent ` compiles and shows an explicit one-seat plan (a dry-run preview through the same capability/team-role/route checks `--seat` uses, never persisted); `/team` shows the plan stored for this repository and `/team plan ` compiles and persists a new one — all three render through the identical functions the headless `zirv workflow agent list`/`team show|plan` commands print through, so the two surfaces cannot drift (issue #541 chunk C). `/workflows` lists the workflow registry and `/workflow ` shows that pack's definition (or, given trailing text as a task, starts it) and `/workflow status [id]` shows a running workflow's status -- all three rendered through the exact same `workflow::engine` writer functions the headless `--json`/text CLI uses (issue #542 chunk 3b), so the pane and `zirv workflow list`/`show`/`status` can never disagree about the same state. `@` file references (`resolve_file_refs`, containment-checked against the workdir) and a `!`-prefixed shell line are not wired into this loop yet. `--runtime native` is **not** a separate dashboard any more: the native conversation opens as the **first pane of the ordinary dashboard**, so one process holds wrapped and native panes together. A pane carries its kind (`dash::pane::PaneKind`); everything around it — the header, the sidebar roster, the spawn-request channel, the mail sweep, attention, the budget sweep, the footer spend and the restore roster — is the dashboard's own and applies to both kinds unchanged. Only four things differ by kind: the pane renders as a `vt100` grid or as the native frame, a keystroke reaches the pty writer or the native composer/router (a native control is never offered on a wrapped pane, and vice versa), mail is delivered as a visible pty injection or through the native **submit path** (so it is subject to the same rollover/generation guard the operator's own `Enter` is), and ending it sends a harness quit sequence or shuts the session down. `zirv agent --runtime native` from inside the dashboard opens a native **worker** pane the same way; such a request is refused, rather than accepted and silently unaccounted, when it asks for a work group or a token/time ceiling, which are accounted from a harness transcript a native session does not have. The restore roster records each pane's kind, and a native pane comes back through the same attachment decision a fresh one makes — so a dashboard restarting while the persistent runtime still holds the conversation re-attaches to it rather than opening a second supervisor over it. The view model (the reducer from the journal to a transcript, the composer, the renderers) is `dash::native_pane`, covered by deterministic tests with no terminal required; see [`docs/design/2026-09-13-native-pane.md`](docs/design/2026-09-13-native-pane.md). **The pane's own surfaces (issue #490).** Beside the conversation the pane draws an **agent & task overview** built from the coordinator graph, the delegation receipts and the seat records — role, the *actual* model and backend with its provenance, ownership and worktree, and one of running / blocked / approval-needed / done-unread / failed / queued / draining, each with its own glyph as well as its own colour. Rows are navigable with `Up`/`Down` and clickable; `Enter` opens that worker's **bounded manifest** — its diff, test and artifact evidence — and never its transcript, and `f` sends a bounded follow-up to it (queued automatically if that worker is itself blocked, and retried at its next idle boundary). Underneath sits the **usage and health provenance strip**: measured, estimated and unknown are labelled and never summed, a route that is excluded keeps its reason verbatim, and an unknown figure is never rendered as a zero. Compaction, rollover and reconnect appear as ordinary notices that scroll with the conversation, each announced once; drafts, selection, focus, scrollback and acknowledged input survive all three, and a submission is refused outright when the logical seat has moved on underneath the pane. An approval is rendered as a numbered dialog carrying the exact scope, answered only with `1`–`3`, the arrows, `Enter` or `Esc` — never from the composer, which queues while blocked. **Answering "Yes" is real for an in-process session:** its execution broker runs in interactive approval mode, raises a typed request carrying the exact tool, scope and actor, and BLOCKS that tool call until the dialog answers. `1` runs it once; `2` runs it and stops asking for that **exact** tool+scope for the rest of this session only (never persisted, never widened to a directory, and only offered when the scope can be stated exactly); `3` refuses the call with the operator's guidance, which is also committed as steering so the running loop picks it up. `Esc` is `3`. Interrupting the turn cancels whatever is blocked on the dialog: the call fails closed and a later answer releases nothing. A **headless** session is unchanged — it is shown the deny option only, rather than a "Yes" that would quietly fail — and repository-owned configuration can only narrow this, never grant it. `?` lists every binding, `Tab` moves focus, `Esc` interrupts (or closes a dialog first), and a double `Ctrl+C` quits. The composer itself is a bordered box with a `>` marker and a hint line naming what `Enter` does right now, and `/`, `@` and `!` open the slash-command list, the worktree-restricted file picker and a shell line that runs through the pane's own process tool under the same policy as any other tool call. **Where the conversation lives.** With `[session] persistent` on, something listening, a runtime that serves native conversations, and a live native seat for this repository, the pane **attaches** to that session over protocol v1 instead of opening its own — submit, steer, interrupt and the approval decision all go out as `session.send_input` / `session.interrupt` / `session.approve`, closing the window is a `session.detach` rather than a kill, and the status line reads `native · runtime`. Anything missing falls back to an in-process session, which is a working mode rather than a failure. An attached pane's model and route show as `–`: the runtime publishes no route identity for a session, and a guess would be worse than an honest placeholder. See [`docs/design/2026-09-13-native-ux.md`](docs/design/2026-09-13-native-ux.md) and the mock in [`docs/design/mocks/2026-09-13-native-pane.html`](docs/design/mocks/2026-09-13-native-pane.html). #### Native workers, shared ownership and delegation receipts > Coming soon: native execution is unavailable in this release. `zirv agent --runtime native` (equally `zirv ctx agent --runtime native`) delegates one task to a **native** worker -- no coding harness installed, no child process, no PTY: ``` zirv agent native "read src/main.rs and report the entry point" --runtime native zirv agent work-sonnet "run the failing test and report" --runtime native --task task-12 --json zirv agent claude "review this diff" --mode read-only # unchanged: the harness fork ``` Flags: `--runtime harness|native` (default `configured`, resolved the same way `zirv ctx exec`'s does — see [Choosing the default](#native-setup-diagnosis-and-rollback)) and `--route `, the same two flags and the same two values `zirv ctx exec` already takes. **Without `--runtime native` nothing changes** -- the harness delegation is reached by the same code, in the same order, with the same arguments. With it, the positional `` names the provider **route** rather than a harness (a native worker has no harness to name), and the reserved value `native` defers to the `[roles]` entry for `--role`; `--route` overrides it. `--max-restarts` and a trailing `-- ` passthrough are **refused**, not ignored, for the same reason `zirv ctx exec --runtime native` refuses them. Everything else about the delegation is identical, because it is literally the same code: `--workdir`/`--worktree`, `--task`, `--group`, `--mode`, `--path-scope`/`--no-network`/`--depth`, `--result-schema`/`--result-kind`, `--budget-tokens`/`--max-tool-calls` and `--json` all behave exactly as they do for a harness worker. A native worker takes the **same** ownership a legacy one does and is therefore mutually exclusive with it: | Exclusive claim | Mechanism | Effect | |---|---|---| | Task card | `zirv ctx task` claim (`task::claim_locked`) | one live claimant per card, whichever runtime asked | | Checkout write | writer permit (`permit::acquire_writer`) | one writer per tree; a native worker's permit is handed to its execution broker, which refuses any repository write not backed by a permit for that exact tree | | Provider tokens | per-provider reservation ledger | one machine-wide outstanding total, settled from the run's real usage | Each delegation gets a **stable handle** -- minted by zirv, independent of any provider conversation id a resume would change -- and a durable record at `/delegations//.json` holding the launch receipt (written *before* anything runs), the ownership taken, every attempt, and every delivery already published or consumed. Terminal outcomes are persisted first and notified second, over ordinary mail, carrying a **delivery identity** `::`. Mail is at-least-once: a consuming `zirv ctx inbox` drops an exact repeat of an identity it has already consumed (and still shows anything it cannot account for), so one completion is never acted on twice and never lost. A message that arrives while its target has an approval or other attention latch open is queued durably and retried at the next idle boundary -- never typed at the dialog (the same rule the dashboard's own pane sweep applies). A native session drives all of this with seven typed tools in its own registry -- `delegate`, `send`, `wait`, `result`, `follow_up`, `interrupt`, `close` -- each a validated argument shape in front of the *same* service method the CLI verb calls. `result` returns a bounded manifest (outcome, delivery identities, report reference, unknown tool outcomes, whether the summary was cut), never a transcript. `follow_up` is addressed to the delegation handle: directed mail while the worker is live, a journal resume for a finished native worker (the id it returns is what `zirv ctx exec --runtime native --resume` takes), otherwise an explicit replacement checkpoint that says it has none of the original's hidden context -- there is no "most recent session" fallback, and an unknown handle is an error. `close` releases the reservation and the write claim while preserving every receipt and every `outcome_unknown` effect. The contract is in [`docs/design/2026-09-13-native-workers.md`](docs/design/2026-09-13-native-workers.md). Trust boundary: every delegation tool crosses the native execution broker as an `ExecutionAction::Delegate`, so a native session cannot delegate around the seat fence and policy its other tools run behind; the delegation handle is validated as `[A-Za-z0-9_-]{1,128}` at the argument boundary, so provider output can never name a file outside its own repository's delegation directory; and a nested worker gets its own principal and a `delegation_depth` one hop shorter than its parent's, so it inherits none of the parent's session authority. `zirv verify --builtin`'s `ZCHK-RUNTIME-INVENTORY` check keeps [`docs/design/native-runtime-inventory.md`](docs/design/native-runtime-inventory.md) honest against the real command surface and source tree: every command verb and every model-calling call site in `src/` has a named implementation owner, checked on every run. Its sibling `ZCHK-NATIVE-PARITY` keeps [`docs/design/native-parity.md`](docs/design/native-parity.md) -- the release-blocking parity matrix -- honest in turn: every capability the inventory knows about has a row naming the native path, the legacy path, the provider/platform requirement, the evidence, and the *rung* that says how strong the claim is (`unit`, `integration`, `ci-matrix`, `live-validated`, `legacy-only`). A missing row, a cited test that exists nowhere in `src/`, a CI step that is not in `ci.yaml`, an uncommitted `docs/benchmarks/` pointer, a `live-validated` claim with no recording, an evidence-free row that is not named as a release blocker, or a `legacy-only` row that does not say why -- each fails the build, so no row can claim more than its evidence. What that record is worth, what is deliberately *not* verified, the pre-declared non-regression targets and the release decision (the default stays the harness; native is opt-in) are in [`docs/design/2026-09-14-native-release-evidence.md`](docs/design/2026-09-14-native-release-evidence.md). The architecture decision behind all of this is recorded in [`docs/design/2026-09-11-native-runtime-contracts.md`](docs/design/2026-09-11-native-runtime-contracts.md). ### Native workflows, verification and helper calls > Coming soon: native execution is unavailable in this release. Migrating the chat loop alone would leave hidden vendor-CLI dependencies in everything around it, so every zirv model call that is *not* the main conversation runs natively too: handoff distillation, `zirv ctx ask`, `zirv ctx optimize`'s judgment pass, the agent loop's objective judge, the memory harvest and consolidation, the independent code reviewer, the frontend visual reviewer and the built-in agent seats. They all go through one helper service (`ctx::helper`) rather than per-call-site provider code: one bounded conversation, a typed answer, typed failures. **There is no new configuration key.** A helper runs natively exactly when your own native provider configuration names a route for that helper's role — `distiller`, `ask`, `optimize` or `seat` under `[roles]` in `~/.zirv/native.toml` — and otherwise keeps its existing harness path unchanged. A native attempt that fails still falls back to the harness rather than failing the caller. The seats that are real delegated workers take `--runtime native` instead, so they reuse `zirv agent` end to end: ```bash zirv workflow review run --agent fast-route --runtime native zirv workflow agents dispatch reviewer --adapter fast-route --runtime native zirv workflow frontend review --runtime native ``` Read-only stays read-only by mechanism, not by prompt: a native helper and a `--mode read-only` native worker hold no writer permit at all, so the execution broker refuses every repository write, outside write, write-effect process and shared-scope knowledge write at effect time — and a headless session cannot approve its way past that. A *writable* seat is therefore refused by the native seat dispatcher rather than quietly downgraded; run it as a delegated worker (`zirv agent --runtime native --mode writing`), which takes a real permit. A native session drives the workflow itself with four typed tools — `workflow_status`, `workflow_context`, `workflow_advance`, `workflow_approve` — each a thin adaptor over the same `workflow::engine` function the CLI verb calls, over the same durable state. The two that mutate the workflow declare the write capability, so a read-only reviewer can read the workflow it is reviewing and cannot move it. Methodology and workflow adoption are automatic. The native context compiler puts the engineering standard, the role methodology, the model profile, your own and the repository's instruction files and the active workflow's current step into every request the session makes; nobody hand-seeds a prompt. Verification freshness is not advisory either: the engine's completion gate is re-read at every completion attempt, so a session that reaches the Test step after it started is gated on the evidence that exists then, and its "I am finished" token cannot outrank it. That gate is keyed by the workflow's own recorded branch, so a workflow started in the main checkout accepts its worker worktree's evidence for the same change set (see [Linked worktrees](#linked-worktrees)) and rejects a different one. The per-command parity table — every shipped command and helper, its native implementation and the test that pins it — is [`docs/design/native-parity.md`](docs/design/native-parity.md); the decisions behind this step are in [`docs/design/2026-09-13-native-workflows.md`](docs/design/2026-09-13-native-workflows.md). #### Instructions in native sessions > Coming soon: native execution is unavailable in this release. A native session's instruction layer (`SourceKind::NativeInstructions`, data, never a provider instruction message) is built the same [`ZIRV.md` precedence](#zirvmd-instruction-files) the wrapped harness reports, in a fixed order: operator-global `~/.zirv/ZIRV.md` first, then the unchanged repo `.zirv/system-prompt.md` layer, then the resolved `ZIRV.md`/`AGENTS.md`/ `CLAUDE.md`/`AGENT.md` winners for the repo root and the active scope's ancestor directory chain. **Scope rule**: only the repo root plus directories that contain a path this session has actually touched (a tool call's read/write/edit target) load their nested instruction files — a large monorepo's unrelated crates are never pulled in. A shadowed, duplicate or excluded chunk A decision still gets a provenance entry (`Excluded`, naming the exact reason) rather than vanishing, and the whole layer is capped by `context.instructions_max_bytes` (default 32 KiB) on top of the existing per-file cap. **Recompile on change**: at the start of a turn, the resolved instruction file list (paths and content hashes) is recomputed for the session's touched-path scope; when it differs from what shaped the current system/preamble — a new nested file entered scope, a file changed on disk, a file was removed — the whole standing context recompiles and replaces it before that turn is sent, without restarting the session. Recompiling only ever touches the instruction layer: tools, policy, permissions and the route are untouched. Every compile/recompile is journaled (`context_version` = the compiled context's stable-prefix hash, plus the per-source path/scope/trust/decision), and the native `/context` (alias `/instructions`) pane command surfaces the same facts live. ### Native teams and the coordinating seat > Coming soon: native execution is unavailable in this release. Zirv can run the coordinator itself. A native session seated as `--role coordinator` (or `sub-orchestrator`) plans the work, staffs it and delegates it across native *and* wrapped workers, over the same shared task cards, work groups, objective and workflow every other surface uses. Seven team roles are recognised — `coordinator`, `sub-orchestrator`, `researcher`, `planner`, `implementer`, `reviewer`, `tester` — and each one spends the route **you** configured for it under `[roles]` in `~/.zirv/native.toml`: ```toml [roles] coordinator = "sonnet-seat" implementer = "sonnet-seat" reviewer = "haiku-metered" tester = "haiku-metered" ``` A role with no entry has no native path at all: the refusal names the role and lists the roles that *are* configured, rather than quietly spending another role's route. Nothing is inferred, and nothing changes provider on its own — a route a model asks for is admitted only if it clears your policy and keeps the same billing posture you seated that role on. ```bash zirv ctx exec --runtime native --role coordinator --prompt "ship issue #123" ``` Eight typed tools give that seat the board: `task_create`, `task_claim`, `task_list`, `group_create`, `group_status`, `objective_status`, `team_status` and `team_plan`. Each is a thin adaptor over the same `zirv ctx task|group|objective`/`zirv workflow team` service the CLI verb calls, so there is one definition of "this card is claimed" and one of "this group is full". The four that change shared state need a writer permit; the four that read do not. **Who may do what comes from the seat, not from the request.** A reviewer or tester is read-only by identity, however the delegation was spelled; a coordinator that is itself read-only can still dispatch a writing implementer; a reviewer seat may not delegate at all. Delegation depth, task ownership, checkout ownership, group admission and the provider token ceiling are the same limits the rest of zirv already enforces — there is no second counter, and a delegation that fails any of them starts nothing and leaves no receipt behind. **A delegation is checked against its manifest and the team plan (issue #541 chunk C).** `delegate` accepts `manifest: ` (defaulting to the requesting role's own built-in manifest — `implementer` for `implementer`, `reviewer` for `reviewer`, and so on); an unknown manifest, or one whose own team role disagrees with the requested role, or one that may write for a role that is read-only by identity, is refused before any receipt exists. `team_plan` (coordinator/sub-orchestrator seats only) compiles the SAME proportional team `zirv workflow team plan` compiles — classification, execution profile, then `compile`/`compile_explicit` for `seat: ` — and stores it: the active workflow owns the plan when one exists for the repository, and the coordinator's own record does otherwise. Once a plan exists, a `delegate` call must name (as its `task`) an unfilled seat whose manifest and role match one in it, or it is refused with `not in the team plan; run team_plan again or pass override: true`; `override: true` bypasses the match but is honoured only for the coordinator seat, and is recorded on the launch receipt either way. Seats are marked filled the moment they are dispatched (the SAME graph node `task_create`/`team_status` already key by task id); a retry after a failure re-fills the identical seat rather than creating a second one, and two seats whose claimed paths overlap can never both be dispatched at once — a wrapped-harness worker seat goes through these exact checks too, whichever runtime the coordinator itself runs on. **A coordinator survives a restart.** Its task graph, decisions, evidence references and your standing constraints are durable. On restart it consumes every worker receipt published while it was away, exactly once, and never re-dispatches work that already settled. A worker's outcome is a bounded manifest and a reference to its result, never a replay of its transcript, and until the receipt is actually read `team_status` reports it as pending rather than assuming it: ```bash zirv ctx objective set "ship issue #123 without touching the release branch" zirv ctx objective close # stops further dispatch; work already running still reports ``` Setting an objective is how you steer — it redirects the coordinator and lifts a previous stop. Closing it stops further dispatch while leaving work already delegated answerable, so its results are still collected. `objective close` refuses (exit 1) unless a fresh, passing final verification for the repository already exists — completion is asserted only by a passing verification gate, never by prose. The decisions behind this step are in [`docs/design/2026-09-13-native-orchestrator.md`](docs/design/2026-09-13-native-orchestrator.md). ### Native configured capabilities > Coming soon: native execution is unavailable in this release. A native session inherits nothing from a coding harness, so the non-shell capabilities a workflow needs — MCP servers, a browser, web search, language diagnostics, artifact presentation — are **configured**, never assumed. Zirv does not claim a raw model API provides any of them. ```bash zirv ctx capabilities # the report, one row per integration zirv ctx capabilities --probe # also contact each configured MCP server zirv ctx capabilities --require browser # exit non-zero unless it would admit a step ``` Every row is exactly one of three states. `available` means zirv found the backend. `unavailable` means it did not, and the row names the missing binary, credential or config key. `unverified` means it is configured but has not been contacted this run — discovery reads configuration, `PATH` and the repository tree and contacts nothing, so a configured MCP server nobody spoke to is not evidence that it answers. `--probe` is what turns an unverified MCP row into a verified one. The workflow engine reads the same report and refuses to enter a step whose required integration is unavailable, quoting the diagnosis, instead of failing halfway through it. Zirv speaks MCP itself, over a local stdio server or a remote Streamable HTTP endpoint with a bearer credential from the same store the direct providers use. It negotiates the 2025-11-25 revision (refusing an unknown one rather than guessing), discovers tools and resources, calls them, cancels with `notifications/cancelled`, and reconnects with re-discovery. A reconnect that changed or removed a tool invalidates it: a call naming that tool is refused until its current schema is described again, so a stale call can never execute a different tool. Server descriptions and results are untrusted data — bounded, redacted, never executed, and streamed into the existing output store when large. A catalogue at or below `capabilities.max_inline_mcp_tools` is exposed as ordinary tools, namespaced `mcp____` so a server can never shadow a built-in name; a larger one is reachable only through a compact index (`mcp_list`), an on-demand schema (`mcp_describe`) and `mcp_call`, so a big toolset never enters every model request. MCP invocation crosses the same execution broker, canonical policy, tool receipts and output limits as every built-in tool, with the effects an operator declared for that server — never the server's own claim about itself. An MCP call is never blindly replayed, and a cancelled one is reported as an unknown outcome. Web results always carry the source URL they came from, and a row that cannot name one is dropped. Browser captures return the on-disk evidence path they actually wrote, and a capture that produced no readable file is an error rather than a success. A capability with no configured backend returns a typed unavailable result naming what is missing; there is no code path that returns an empty success. Outbound requests pass one interception seam (`EgressGuard`), which is where [issue #466](https://github.com/Glubiz/zirv-cli/issues/466)'s on-device obfuscation belongs rather than a second subsystem beside it. Configuration lives under `[capabilities]` in `~/.zirv/ctx.toml` and is off by default: ```toml [capabilities] enabled = true max_inline_mcp_tools = 24 # above this, only the index plus describe [capabilities.web] search_endpoint = "https://search.example/api?q={query}" search_credential = "env:SEARCH_TOKEN" # env:NAME, store: or file: fetch_enabled = true allow_hosts = ["docs.rs", ".rust-lang.org"] # empty means nothing is reachable [capabilities.browser] enabled = true # binary = "chromium" # discovered on PATH when unset [[capabilities.mcp]] name = "docs" enabled = true transport = { mode = "stdio", command = "mcp-docs", args = [] } effects = { network = true } # what this server's tools may do, per the operator [[capabilities.mcp]] name = "remote" enabled = true transport = { mode = "http", url = "https://mcp.example/rpc", credential = "env:MCP_TOKEN" } ``` The whole `[capabilities]` table is operator-only (see [Trust boundary](#trust-boundary)): every key names a command zirv spawns, an endpoint it authenticates to, a credential, or a browser it launches, so there is no narrowing half a repository checkout could legitimately set. The full contract is documented in [`docs/design/2026-09-13-native-capabilities.md`](docs/design/2026-09-13-native-capabilities.md). ### Runtime protocol v1 (`zirv ctx api`) The `RuntimeBackend` wire above is in-process. **Protocol v1** is the public, versioned surface around it: the one a durable subscriber, an alternate client or a future daemon is allowed to depend on. The CLI wrappers remain the normal automation interface — raw protocol access exists for clients zirv does not ship. ```bash zirv ctx api schema # the contract, for a human zirv ctx api schema --json # the same contract as a JSON Schema document zirv ctx api serve # bind the endpoint for a bounded time zirv ctx api call session.snapshot ``` Both schema renderings are generated from the binary's own types — the method table, the real enum variants, and a test that pins the published frame field lists against what serde actually writes — so the documentation cannot drift from the wire. **Transport.** A unix domain socket at `/s/api.sock` on unix, a named pipe on Windows, carrying NDJSON in both directions. The server writes one `hello` frame per connection before reading anything; a client intersects the capabilities that frame advertises with its own and disables locally whatever is missing, which is how an older client connects to a newer server and vice versa. Requests carry a caller-chosen `id`; replies echo it and carry the server `revision`; mutations accept an optional `idempotency_key`, and a retry with the same key returns the first attempt's result instead of repeating the work. Unknown fields are ignored everywhere, and every published enum vocabulary ends with an `unknown` fallback. **Methods** (v1 is deliberately narrow): `server.ping`, `server.capabilities`, `session.snapshot|list|get`, `session.start|stop`, `session.read|send_input`, `session.wait`, `session.report_status`, `session.attach|detach|takeover|resize|screen`, `session.interrupt|approve|task_result|history|journal`, `events.subscribe`. Mail, memory, work-group, workflow, layout and plugin methods are added only when a concrete client needs them. The five attachment methods need a server that owns terminals, so they sit behind their own `session.attach` capability; the five native methods need one that owns native conversations, so they sit behind `session.native`. A server without either does not advertise it, and a client disables that surface locally rather than calling it and being refused. **Native sessions on the protocol.** A native conversation has a journal instead of a pseudoterminal, so it reaches the same endpoint through the same methods with a different half of the surface. Input is `session.send_input`'s ordinary `submit`/`steer` modes — there is no second way to hand a session text. `session.interrupt` cancels the turn in flight and leaves the session alive (`session.stop` is still the only verb that ends one); `session.approve` decides one pending approval; `session.task_result` records a delegated task's outcome; `session.history` reads the conversation; `session.journal` pages its durable event stream by cursor. `session.screen`, `session.resize` and `mode=raw` are refused by name: there is no terminal to act on. `session.history` is the one method in v1 that publishes conversation text. It is capability-gated, seat-checked, and its tool entries carry a tool's name and never its arguments or results. Session facts, snapshots, lists and event frames still carry none of it. **Seats and durable retries.** Once any client is attached to a native session, every mutation must name a `client_id` holding the controller seat — omitting it is refused too, so an observer cannot mutate by leaving the field out. A session nobody has attached to is driven by whoever can reach the owner-only endpoint, which is the rule a headless `zirv ctx exec` needs. For a native session an `idempotency_key` becomes the journal's own message id, so a retry after a reconnect — or after the runtime restarted, which loses every in-memory cache — is deduplicated on disk and answered with `duplicate: true` and the original `message_id`; no second turn is queued. `session.journal` answers `gap: true` when a caller's cursor can no longer be continued from, which is the durable counterpart of the live revision gap rule. **Events and gaps.** The server-wide `revision` advances by exactly one per emitted event, so a subscriber that sees a revision other than `last + 1` has missed something and refreshes `session.snapshot` rather than drifting. Waits pin the session generation resolved at call time: a session replaced while a wait is running fails that wait with `stale_generation` instead of letting the replacement satisfy it. **What is shared and what is not.** Server state holds runtime facts only — which sessions exist, their stable ids, generations and lifecycle state, and the event log. Layout, colour, sidebar selection, mouse state and modals stay in whichever client draws them; no method can read or write any of it. Session ids are stable across panes, tabs, worktrees and client attachment: `surface` is the only axis a client change moves. **Security.** The endpoint path is always derived from the operator-owned state directory — there is no flag, config key or environment variable that points the server or a client at an arbitrary path, so a checkout cannot name or redirect it (see [Trust boundary](#trust-boundary)). On unix the socket lives in a 0700 directory, is chmod'ed 0600, and the server verifies the peer uid (`SO_PEERCRED`/`getpeereid`) against its own; on Windows the pipe is created with an explicit owner-only DACL, because the *default* named-pipe descriptor grants read access to Everyone and the anonymous account. Snapshots publish a redacted session shape by construction: no transcript path or body, no prompt, no mail body, no terminal history, no credential and no absolute repository path — only the sanitised repo slug. Mutations go through the same narrowing-only trust model as everything else. **What v1 is not.** The reference server (`zirv ctx api serve`, or the duration of one `zirv ctx api call`) runs in-process, owns no terminals and holds no processes: it serves every read method off the session registry, refuses `session.start|stop|send_input` with a structured `unsupported`, and does not advertise the attachment capability at all. The daemon that does own terminals is `zirv session serve` — the same protocol, the same endpoint, one extra capability. Frozen request/response/event fixtures under `tests/fixtures/protocol/v1/` are replayed against the server on every test run, so the wire cannot change by accident. The full contract and its trade-offs are recorded in [`docs/design/2026-09-12-runtime-protocol-v1.md`](docs/design/2026-09-12-runtime-protocol-v1.md). ### Persistent runtime (`zirv session`) **Experimental, operator-only, and off by default.** A local runtime service owns the PTY/ConPTY processes, their supervisors and their registry records, and every UI is a client of it. Closing the window, killing the client or losing the terminal then costs a repaint, not a session. ```bash zirv session serve # run the runtime (owns the terminals) zirv session list # what it holds, and who is attached zirv session attach [name|id] # attach this terminal (Ctrl+A d detaches) zirv session detach [name|id] # release clients; the agent keeps running zirv session stop [name|id] # end a session (this one asks first) zirv session stop --runtime # stop the service; sessions keep running ``` Turn it on in `~/.zirv/ctx.toml` (a repository cannot): ```toml [session] persistent = true # default false history = false # default false -- see the warning below scrollback_rows = 2000 # in memory, per session stale_after_secs = 120 # secondary staleness signal only ``` With `persistent = true` and a real terminal on both stdin and stdout, `zirv chat` attaches to this repository's runtime session (starting the runtime and the session if they are not there yet) instead of owning a PTY in its own process. `zirv chat --no-session`, a piped stdin and a redirected stdout all keep the previous behaviour exactly. **Clients.** Any number of observers may watch a session; at most one client holds the keyboard. A second controller is refused with `busy` rather than silently displacing the first — `zirv session attach --takeover` takes the seat on purpose, and every client sees the `controller_changed` event. `Ctrl+A d` detaches (`Ctrl+A Ctrl+A` sends a literal `Ctrl+A` to the agent), the same prefix the dashboard uses. **Detach is not stop.** `detach` moves an entry in the runtime's attachment table and touches nothing else: no confirmation, and the agent, its PTY, its supervisor and its registry record are all still there afterwards. `stop` puts the child through the existing termination ladder and asks first — `--yes` is required when stdin is not a terminal. **What actually survives, honestly:** | Tier | Event | Guarantee | |---|---|---| | 1 | A client detaches, crashes or closes | The original PTY and process keep running. Reattaching restores the current rendered screen and live input from the runtime's own in-memory parser. Nothing is relaunched. | | 2 | The runtime itself restarts | The processes are gone. Topology (which sessions, which directories, what size) is restored, and a session is **resumed** only when it carries a verified harness conversation reference; everything else is reported as not resumed. A restored session is a new session that records its predecessor — never the old identity revived. | | 3 | Rendered terminal history across a runtime restart | Off by default. `history = true` writes rendered terminal output — including anything an agent printed, such as API keys, tokens and file contents — to disk, and says so every time the runtime starts. | | 4 | Replacing the runtime binary under live sessions | Not supported. Stop the runtime, upgrade, start it again. | A detached session keeps every Zirv guarantee that reads the registry, because the runtime files the same registry record a dashboard pane does and holds it for the life of the session: usage pacing, budgets, rot scoring, mail addressing, writer permits and workflow policy all keep working with nobody watching, and the harness's turn signals keep being observed. **Mail is delivered while nobody is watching too** — the service types it into the session on its own heartbeat, through the dashboard's own sweep, instead of leaving it queued until a client attaches. **Native conversations live here too.** The same runtime owns native sessions (`runtime = "native"`), publishes them in the same session list and serves them over the same endpoint — one service with two hosts, not two daemons. A native conversation's durable state is its journal, so a runtime restart brings it back by reading it: every tool execution that was merely *started* becomes outcome-unknown and is named on startup rather than retried, the generation advances to fence out any straggler, and nothing is re-submitted. No process is ever described as having survived. **The dashboard is a client, not a second owner.** With the gate on and a runtime listening, `zirv dash` refuses to open a second terminal over a session the runtime already holds and points at `zirv session attach`; two supervisors on one conversation is what the runtime exists to prevent. With the gate off, or with nothing listening, the dashboard owns its own terminals exactly as before. **Identity.** Each namespace publishes a record under `/runtime/` with its owner, version, endpoint, creation time and last-client time. Staleness is decided by process **start identity**, not by pid alone: a live pid whose start time does not match the record is a recycled pid and the namespace is free, while a record nothing can verify is left alone rather than seized. Every service start mints a fresh instance id, so a crashed runtime's session identities can never be republished by its successor. The native integration — the two hosts, the seat rule, durable idempotency, journal cursors and restart reconciliation — is recorded in [`docs/design/2026-09-13-native-runtime-integration.md`](docs/design/2026-09-13-native-runtime-integration.md). See [`docs/design/2026-09-13-persistent-runtime.md`](docs/design/2026-09-13-persistent-runtime.md) for the design, the measurements and what is deferred. ### MCP bridge `zirv ctx mcp serve` lets an MCP host such as Codex or Claude Code read zirv state through structured tools. The host launches it as a local subprocess; stdin/stdout carry MCP JSON-RPC and diagnostics go to stderr. Stdio is the only transport (`--stdio` is optional). `--repo` fixes the authorized directory at startup; without it, the server uses its launch directory. Use an absolute path to the installed zirv executable and an explicit repository path in host configuration so a host's working directory cannot select the wrong project. | Tool | Arguments | Result | |---|---|---| | `session_snapshot` | `{}` | Up to 64 session registry records for this directory, requested policy, and memory gates. Liveness and host enforcement are explicitly unverified; stale records are preserved. | | `memory_search` | `query`, optional `limit` and `max_bytes` | Ranked private/global/shared facts with provenance and verification dates. Uses the existing retrieval engine and trusted-key precedence. | | `workflow_status` | `{}` | Active workflow, current step/skill, approval state, and up to 64 registered artifact IDs, newest first. An absent workflow stays absent. | | `artifact_read` | `id`, optional `offset` and `max_bytes` | A UTF-8 text page from an artifact registered with `zirv artifact render`. Returns `next_offset` for subsequent pages. | | `worker_status` | Optional `id`, `cursor`, `limit` | Repository-scoped delegation/report records with recorded phase, attempt, exit code where available, report outcome and truncation. Worker IDs sort lexicographically; follow `next_cursor`. | | `result_read` | `id`, optional `offset`, `revision`, `max_bytes` | A page of the persisted worker result JSON, including report text, structured result, validation errors and undeclared changes. Use an ID from `worker_status`. | | `inbox_read` | Optional `cursor`, `limit`, `max_bytes` | Unread message previews for the launch-bound recipient, with sender labels, truncation and `next_cursor`. Never consumes, acknowledges, claims, expires or retries delivery. | All successful responses contain `captured_at` (Unix seconds), `repository`, and `data`, with both an output schema and structured JSON. Tool failures return `isError: true` with an explanation. Each serialized structured result is capped at 32 KiB (the MCP text fallback repeats that JSON). Memory queries must be nonblank and at most 2048 bytes. Memory limits default to 6 entries / 2048 bytes, may be lowered, and cannot exceed 32 entries / 16384 bytes or the operator's configured retrieval budgets. Artifact pages default to 8192 bytes (range 4..8192); files must be regular UTF-8 text no larger than 1 MiB. Offsets are byte positions on UTF-8 boundaries. Registered files remain live files, so callers should restart pagination if they change between reads. Worker listings default to 16 records (maximum 64). `id` selects one exact worker and cannot be combined with `cursor`. Ordinary harness reports have no delegation phase or exit code unless a corresponding durable delegation exists; those fields remain null. Report outcomes such as `reported`, `validated` and `contract_failed` describe stored evidence, not process liveness or task correctness. Unreadable, oversized or unscoped reports are omitted from listings; an explicit `result_read` returns an error. Reports written before repository provenance was recorded require a matching scoped delegation reference, never just a filename. Result pages default to 8192 bytes (range 4..8192). The first page returns a SHA-256 `revision`; pass it with every nonzero `offset`. A changed report refuses continuation, so restart at offset zero. Pages reconstruct the stored JSON, including escaped report text. Stored files must be regular UTF-8 JSON at most 8 MiB (allowing JSON escaping of the existing 1 MiB report-body limit). Inbox pages default to six messages (maximum 32), oldest first. `max_bytes` controls each body preview, default 1024, range 4..4096. Current `mail.max_message_bytes` and the aggregate `mail.max_delivered_bytes` also apply; `body_truncated` and `body_bytes` describe omitted text. Use the existing inbox CLI/full-body references when a preview is insufficient. Cursors are exclusive and remain usable when earlier messages are consumed; concurrent arrivals can require another scan from the beginning. `mail.enabled = false` returns an explicitly disabled, empty inbox. Bind directed inbox reads using `serve --session ` or by forwarding the supervisor's `ZIRV_CTX_SESSION` to the MCP subprocess. The server resolves that identity once against the session registry, requires its canonical repository to match `--repo`, and derives the agent and mailbox from the record. Unknown, ambiguous or foreign bindings refuse startup. Tool arguments cannot supply or change the recipient. Without a binding, only undirected mail addressed to `any` in this repository is visible. With a binding, existing directed delivery envelopes can locate that recipient's mail across mailbox directories; unrelated broadcast mail stays repository-scoped. **New Claude Code and Codex sessions launched through Zirv register this bridge automatically.** This includes chat/wrap, dashboard panes, headless workers, loop cycles and their supervised restarts. No `.mcp.json` or manual server entry is needed. The launch pins the current Zirv executable, canonical repository, operator state directory and stable inbox address. Install the updated Zirv binary and start a new session to discover its tools; an already-running host keeps its existing server process until restarted. Zirv supplies a launch-only `zirv` server entry using Claude's `--mcp-config` or Codex's `-c` overrides. Other named servers remain configured. The `zirv` name is reserved for this automatic entry during the launch. No project or user host settings are edited. On Windows, Zirv writes Claude's generated JSON under the private state directory's `mcp-launch/` to keep JSON off shell-shim command lines; users do not create or maintain that file. Claude launches also pre-approve the bridge's seven named read-only tools so headless `dontAsk` sessions can use them. Native deny/ask rules and the server's current policy gates still apply; other servers receive no added permissions. Set `ZIRV_CTX_MCP_AUTOREGISTER=0` (also `false` or `off`) to opt out. Claude's explicit `--strict-mcp-config` is respected and suppresses automatic registration. `wrap --no-supervise` remains passthrough. Unsupported hosts and sessions launched directly outside Zirv retain their own registration setup. An unavailable automatic configuration prints a warning and leaves the host launch available. For a **direct Codex launch outside Zirv**, add a server entry to the operator's `~/.codex/config.toml`, using the real absolute paths: ```toml [mcp_servers.zirv] command = "/absolute/path/to/zirv" args = ["ctx", "mcp", "serve", "--repo", "/absolute/path/to/project"] ``` For a **direct Claude Code launch outside Zirv**, pass this JSON through `--mcp-config` (an inline JSON string or an operator-owned file): ```json { "mcpServers": { "zirv": { "command": "/absolute/path/to/zirv", "args": ["ctx", "mcp", "serve", "--repo", "/absolute/path/to/project"] } } } ``` On Windows, use the installed `zirv.exe` path; forward slashes work in JSON and TOML paths. Preserve the host's existing server entries. If the host must use a custom zirv state directory or operator environment override, forward that setting through its MCP server environment configuration. See the official [Codex MCP](https://developers.openai.com/codex/mcp) and [Claude Code MCP](https://code.claude.com/docs/en/mcp) configuration references. The server's seven tools neither consume mail nor write memory, run commands, launch workers, or advance workflows. Existing hooks, CLI checkpoints and supervisor recovery continue independently. Reading a registered report does not certify that its claims are correct. Mail mutations, dispatch, and incoming event delivery are follow-on work tracked in [issue #658](https://github.com/Glubiz/zirv-cli/issues/658). Check the local server connection: ```bash zirv ctx mcp doctor --repo /absolute/path/to/project ``` The diagnostic launches the current executable, negotiates MCP, validates all seven tool definitions and invokes `session_snapshot`. It prints JSON on success and exits nonzero on a denied call, protocol error or timeout. It inherits the same state/configuration environment and optional session binding as `serve`, and terminates and reaps its subprocess on all paths. This verifies the local server connection; it does not inspect or certify host registration. No host settings, workflows, mail or memory are changed. MCP reads use the current repository state buckets and never migrate legacy buckets. If an older installation's state has not been adopted yet, run the ordinary zirv CLI for that repository before starting the host. #### MCP trust boundary | Surface | Authority and enforcement | |---|---| | Executable and `--repo` | Pinned by the Zirv supervisor for automatic registration, or chosen by explicit operator host configuration; tool arguments cannot change either. The repository directory is opened once for artifact access. | | Automatic registration | A launch-only `zirv` entry for Claude Code/Codex. No project/user host config writes or changes to other named servers; operator opt-out and Claude's strict MCP selection are respected. Codex receives operator environment override names, never credential values on argv. | | `policy.tool_access` | Re-read for every call. `deny` and `ask` refuse calls; this server has no approval-granting channel. A malformed configuration also refuses reads. | | Memory | Existing `memory.enabled`, `memory.shared_enabled`, and retrieval budgets apply. A shared key cannot shadow an enabled private/global key, even if that trusted fact misses the query budget. | | Artifact IDs | Resolved in this repository's existing artifact registry. Payload access uses a directory capability to reject traversal and symlink escapes; arbitrary file paths are not tool arguments. | | Worker results | IDs resolve inside the operator-owned result store. Canonical repository provenance or a matching scoped delegation authorizes each read; unscoped legacy filenames grant nothing. Directory capabilities reject report traversal and symlink escapes. | | Inbox recipient | Operator launch configuration/inherited supervisor identity selects a registered session in this repository. Agent and mailbox come from that record, never tool arguments. The binding is not an OS authentication boundary. | | Mail policy and receipts | Current `mail.enabled` and delivery budgets apply to every read. Existing expiry and fan-out visibility rules are reused without receipt or mailbox mutations. Sender labels remain claims. | | Returned text | Memory provenance is retained; artifacts, worker reports, mail and shared facts are labeled untrusted information, never operator instructions. | | OS account and local state | Operator-owned state is trusted as with the CLI. This local service does not isolate mutually hostile processes that already share filesystem access under the same account. | ### Signals and verdicts Four signals over the trailing window (default 10 turns): 1. **Context size** (a gate, not a vote). The floor and ceiling scale with the model's real context window (issue #155): by default the floor sits at 50% of capacity and the ceiling at 80% (`score.token_floor_ratio`/ `token_ceiling_ratio`), so a 200k-token seat still gates at 100000/160000, the pre-ratio absolutes, and a 1M-token seat gates at 500000/800000 instead of restarting at the same 160000 tokens with 840k of headroom left. When no capacity is known (codex today), the absolute 100000/160000 fallbacks apply unchanged. `score.token_floor`/ `token_ceiling` still pin an exact number outright, overriding the ratio. Below the floor the verdict is always `healthy`; at or above the ceiling it is at least `compact`. 2. **Tool-failure rate** (weight 40). 3. **Repetition loops**, three or more identical tool calls with identical input (weight 30). 4. **Reply-marker misses** on final answers (weight 30, active only when the marker hook is installed and the session is at least 10 turns old). Verdicts: score 40 or more is `advise`, 60 or more is `compact`, 80 or more is `restart`. At the token ceiling a score of 60 or more escalates to `restart`. Without the marker signal (Claude without the prompt hook, or any agent that cannot carry one) behavioral signals top out at 70, so a restart there comes only from the token ceiling. ### Configuration Layered, lowest priority first: `~/.zirv/ctx.toml`, then `/.zirv/ctx.toml`, then `ZIRV_CTX_*` environment variables, then flags. A `.zirv/` directory that is the operator's own (running `zirv`/`zirv chat` from the home directory itself) is never also read as the repository layer for `ctx.toml` or for the `system-prompt.md` prompt layer. ```toml # .zirv/ctx.toml agent = "claude" [score] window = 10 min_turns = 10 token_floor = 100000 token_ceiling = 160000 marker = "[zirv]" advise_at = 40 compact_at = 60 restart_at = 80 [wrap] debounce_ms = 3000 inject_timeout_ms = 20000 [supervise] max_restarts = 2 interval_secs = 900 max_cycle_secs = 3600 max_failures = 5 [handoff] model = "haiku" tail_items = 5 timeout_secs = 30 # the distiller is given this long before the structural # fallback is used instead [mail] enabled = true max_message_bytes = 4096 # per-message cap, applied by `zirv ctx send` max_delivered_bytes = 4096 # cap on a whole batch folded into one launch prompt keep = 50 # unread messages kept per repo before the oldest are pruned [memory] enabled = true harvest = false # opt-in; see "Memory bank" below max_entries = 50 # entries kept per repo before the oldest (by Written) are pruned max_entry_bytes = 512 # per-entry body cap max_injected_bytes = 2048 # superseded by core_max_bytes; kept only so an old config does not error shared_enabled = true # whether the repo-owned shared bank (/.zirv/memory/) is read at all core_max_bytes = 2048 # cap on the merged private+shared core layer folded into every session retrieval_max_bytes = 2048 # cap on `zirv memory recall`; reserved for future context-aware session retrieval retrieval_max_entries = 6 # max number of recalled entries, independent of bytes [chrome] banner = true # the one-time launch banner bar = true # the reserved one-row status bar events = true # the `zirv ▸` announcement channel on stderr ``` Handoffs, sockets, logs and scoring checkpoints live in the platform state directory under `zirv/ctx/`, never in the repo. Override with `ZIRV_CTX_STATE_DIR`. On unix the state directory is created `0700` and its files `0600`: it holds transcript paths, prompts and distilled handoffs. The Stop hook is a fresh process on every turn, so it leaves its parse position and the scoring state derived from it in `scoring/`, which is what keeps per-turn scoring proportional to the turn rather than to the whole session. Any doubt about a checkpoint -- a rewritten or truncated transcript, changed scoring config, an unreadable file -- silently rebuilds it from a full parse. See [Usage pacing](#usage-pacing) below for the `[pace]` table that governs subscription-window waiting. #### Tool-output compaction `zirv setup` installs the claude `PostToolUse` hook: a large `Bash` result is replaced with a compact summary before the model ever sees it, and the original stays retrievable in full with `zirv ctx output show `. The size thresholds are `output.compact_min_bytes` (4096 bytes, for a known build/test/VCS tool) and `output.compact_generic_min_bytes` (16384 bytes, for everything else); a unified diff (`git diff`/`show`/`log -p`/ `format-patch`) instead gets `output.diff_max_bytes` (65536 bytes) and a bounded per-file listing rather than a head/tail. `rg`/`grep`/`find`/`fd`/ `ls`/`dir`/`tree` results past the generic threshold get a grouped, capped rendering by default too (`output.compact_search`); set it to `false` to leave those seven verbatim at any size instead. Once a `Generic`-scope result is past its threshold, a bundled `[[output.filter]]` rule set (`output.filter_defaults`, on by default) strips common noise -- progress bars, download/package-manager chatter -- before the head/tail summary ever runs over it: | Rule | Commands matched | Strips | |---|---|---| | `pkg-python` | `pip`/`pipx`/`uv`/`poetry`/`pdm`/`conda`/`mamba` | Collecting/Downloading/wheel-build lines, progress bars | | `pkg-system` | `apt`/`dnf`/`yum`/`apk`/`pacman`/`zypper`/`brew`/`choco`/`winget`/`scoop` | Package-list/unpack/setup progress lines, progress bars | | `download-progress` | `curl`/`wget`/`Invoke-WebRequest`/`iwr`/`aria2c` | Transfer-meter header/rows, progress bars | | `build-steps` | `cmake`/`ninja`/`meson`/`bazel`/`buck` | `[n/N]`/`[n%]` build-step lines, progress bars | | `toolchain-install` | `rustup`/`nvm`/`fnm`/`volta`/`pyenv`/`rbenv`/`asdf`/`mise` | `info: downloading/installing/...` lines, progress bars | | `infra-refresh` | `terraform`/`tofu`/`terragrunt`/`pulumi`/`cdk` | `Refreshing state...`/`Still creating...` lines, progress bars | | `test-runner-js` | `jest`/`vitest`/`mocha`/`ava`/`tap`/`karma`/`playwright`/`cypress`/`deno test`/`bun test` | Passing (`✓`/`PASS`/`ok N -`) lines | | `progress-noise` | every other command (catch-all) | Progress-bar lines only | The first matching rule (in that order) wins. Declare a `[[output.filter]]` rule with the same `name` in `~/.zirv/ctx.toml` to replace one of these outright -- an operator rule always wins over a bundled rule sharing its name: ```toml # ~/.zirv/ctx.toml [[output.filter]] name = "pkg-python" match_command = "^(pip|uv|poetry)\\b" strip_lines = ["^\\s*Downloading "] ``` Set `output.filter_defaults = false` to drop the bundled rules entirely and keep only your own. #### Trust boundary | Surface | Trust | Narrowing? | |---|---|---| | Native release availability | fixed in the binary | no flag, configuration, environment variable or Cargo feature can enable native execution | | `~/.zirv/ZIRV.md`, `~/CLAUDE.md`, `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` (operator-global) | operator | n/a — operator-authored | | `/ZIRV.md`, `/.zirv/ZIRV.md`, nested `ZIRV.md`, `AGENTS.md`, `CLAUDE.md`, singular `AGENT.md` (repo-owned, any scope) | repo-owned, untrusted | narrows only — read as prose context, never as authority | Every native instruction file inside the repository checkout — `ZIRV.md` (root, `.zirv/` fallback, or nested), `AGENTS.md`, `CLAUDE.md`, and the singular `AGENT.md` compatibility alias — is `RepoUntrusted`: it can steer a session's prose but can never widen sandboxing, approvals, credentials, provider/account/billing routing, tool grants, workflow policy floors, or settings precedence, the same asymmetry `REPO_FORBIDDEN` enforces for `ctx.toml` below. Only the operator-global `~/.zirv/ZIRV.md` (and its `CLAUDE.md`/`AGENTS.md` counterparts) carries `Operator` trust, and even then only as prose context — instructions are context, never permissions, at every scope. A repository config is part of a checkout, so cloning a repository must not be enough to change what zirv executes. `/.zirv/ctx.toml` may not set `agent`, `agent_bin`, `supervise.on_failure`, `handoff.model`, `optimize.model`, `sandbox.enabled`, `prompt.enabled`, `prompt.repo_layer`, `prompt.max_repo_bytes`, `prompt.harnesses`, `prompt.codex_orchestrator`, `prompt.verbosity`, `chat.claude_permission_mode`, `mail.enabled`, `mail.max_delivered_bytes`, `chrome.events`, any `memory.*` key, any `dash.*` key, any `pace.*` key, any `price.*` key, `review`, `worker.claude`, `worker.codex`, `worker.default_depth`, `worker.default_read_only`, `handover`, any `session.*` key, any `runtime.*` key, or any of the five keys that feed the token gate (`score.token_floor`, `score.token_ceiling`, `score.token_floor_ratio`, `score.token_ceiling_ratio`, `score.model_context_tokens`); doing so is an error that names the key. Set those in `~/.zirv/ctx.toml`, or with the matching `ZIRV_CTX_*` variable below, which comes from the operator rather than the checkout: The same narrowing-only rule applies to native execution. Native tools receive the already-resolved policy and resource claims from trusted runtime state; repository instructions and model output cannot add roots, provider credentials, network targets, approvals, or a different seat generation. Process environment overrides are part of the broker-signed action and may not replace protected credential variables. Declared process effects only request additional sandbox access; under-declaring an effect leaves that resource read-only or disconnected rather than bypassing policy. The native agent loop adds no repository-settable key either. Which runtime runs, which provider route it spends, which role it holds, which session it resumes and whether a fixture transport stands in for a real provider all come from the command line and from the operator-owned native provider configuration in `~/.zirv/native.toml` — never from a checkout. Model output is untrusted input throughout: a tool call it emits is admitted by the shared before-tool service and the N04 broker before anything runs, and its "I am finished" token is one input to the final status rather than the answer. A repository can still narrow, through the same `[supervise] orchestrator_writes` posture that governs the harness path, which the native loop applies to its own `file_write`/`apply_patch` calls. Native workers and the delegation tools add no repository-settable key either. Which runtime a worker runs on, which route it spends and which task it claims come from the command line (or from the parent's own delegation tool call), never from a checkout. A delegation tool call is model output and is treated as such: it crosses the broker as an `ExecutionAction::Delegate` under the same seat generation fence every other native tool runs behind, the delegation handle it names is validated as `[A-Za-z0-9_-]{1,128}` before it can reach a file, and the worker it asks for is narrowed against the parent's own envelope (`--path-scope`, `--no-network`, `--depth`, writing vs read-only) — asking for more than the parent holds is refused, never clamped. A nested worker gets its own principal and one hop less delegation depth, so it inherits none of the parent's session authority. The delegation record itself lives under the operator-owned state directory, never in the repository. Native workflows, verification and helper calls add no repository-settable key either. Which runtime a workflow reviewer, agent seat or script `agent:` step runs on comes from the command line or the script the operator wrote; which route a helper spends comes from the operator-owned `[roles]` table in `~/.zirv/native.toml`, and a role with no entry has no native path at all. A helper session is constructed with no writer permit, so its read-only character is the broker's decision at effect time rather than a prompt a model could be talked out of, and `workflow_advance`/`workflow_approve` are priced as shared-scope writes for the same reason. The workflow id a tool call names is validated as `[A-Za-z0-9_-]{1,128}` before it can reach a file, so model output cannot address state outside the workflow store. The completion gate reads the workflow's own recorded branch and the verification store, never the model's account of them. The native meta-orchestrator adds no repository-settable key either. A seat's **role** comes from the persisted seat record zirv itself minted, never from model output, and that role alone decides whether the seat may delegate and whether a child it delegates may write — so a read-only coordinator can still dispatch a writing implementer, and a reviewer stays read-only however the request was spelled. A route a delegating model names for a role is admitted only when it clears operator policy and stays on the billing posture the operator seated that role on; an operator's own `--route` is the operator speaking and is untouched. Task and group ids a tool call names are validated as `[A-Za-z0-9_-]{1,128}` before they can reach a file, and `task_create`/`task_claim`/`group_create` are priced as shared-scope writes, so a seat with no writer permit reads the board and cannot move a piece on it. The coordinator's own record lives under the operator-owned state directory, never in the repository. The local runtime protocol ([`zirv ctx api`](#runtime-protocol-v1-zirv-ctx-api)) adds no configuration key, and deliberately so: its endpoint is always derived from the operator-owned state directory, there is no setting or flag that names one, and the server's policy — which methods exist, which capabilities are advertised, who may connect — is compiled into the binary. A repository therefore has nothing to narrow here, and nothing to widen either. | Forbidden repo key | Set instead via | |---|---| | `agent` | `ZIRV_CTX_AGENT` | | `agent_bin` | `ZIRV_CTX_AGENT_BIN` | | `supervise.on_failure` | `ZIRV_CTX_ON_FAILURE` | | `handoff.model` | `ZIRV_CTX_MODEL` | | `optimize.model` | `ZIRV_CTX_OPTIMIZE_MODEL` | | `sandbox.enabled` | `ZIRV_CTX_SANDBOX` | | `sandbox.extra_allow` | `ZIRV_CTX_SANDBOX_EXTRA_ALLOW` | | `sandbox.scrub_subprocess_env` | `ZIRV_CTX_SANDBOX_SCRUB_SUBPROCESS_ENV` | | `prompt.enabled` | `ZIRV_CTX_PROMPT` | | `prompt.repo_layer` | `ZIRV_CTX_PROMPT_REPO` | | `prompt.max_repo_bytes` | `ZIRV_CTX_PROMPT_MAX_REPO_BYTES` | | `prompt.harnesses` | `ZIRV_CTX_PROMPT_HARNESSES` | | `prompt.codex_orchestrator` | `ZIRV_CTX_PROMPT_CODEX_ORCHESTRATOR` | | `prompt.verbosity` | `ZIRV_CTX_PROMPT_VERBOSITY` | | `chat.claude_permission_mode` | `ZIRV_CTX_CHAT_CLAUDE_PERMISSION_MODE` | | `context.max_common_bytes` | `ZIRV_CTX_CONTEXT_MAX_COMMON_BYTES` | | `context.max_harness_bytes` | `ZIRV_CTX_CONTEXT_MAX_HARNESS_BYTES` | | `context.max_harness_roster_bytes` | `ZIRV_CTX_CONTEXT_MAX_HARNESS_ROSTER_BYTES` | | `context.instructions_max_bytes` | `ZIRV_CTX_CONTEXT_INSTRUCTIONS_MAX_BYTES` | | `context.lint_max_pairs` | `ZIRV_CTX_CONTEXT_LINT_MAX_PAIRS` | | `mail.enabled` | `ZIRV_CTX_MAIL` | | `mail.max_delivered_bytes` | `ZIRV_CTX_MAIL_MAX_DELIVERED_BYTES` | | `chrome.events` | `ZIRV_CTX_QUIET` (see the note below on why this one's name looks different) | | `memory.enabled` | `ZIRV_CTX_MEMORY` | | `memory.harvest` | `ZIRV_CTX_MEMORY_HARVEST` | | `memory.max_entries` | `ZIRV_CTX_MEMORY_MAX_ENTRIES` | | `memory.max_entry_bytes` | `ZIRV_CTX_MEMORY_MAX_ENTRY_BYTES` | | `memory.max_injected_bytes` | `ZIRV_CTX_MEMORY_MAX_INJECTED_BYTES` | | `memory.shared_enabled` | `ZIRV_CTX_MEMORY_SHARED` | | `memory.core_max_bytes` | `ZIRV_CTX_MEMORY_CORE_MAX_BYTES` | | `memory.retrieval_max_bytes` | `ZIRV_CTX_MEMORY_RETRIEVAL_MAX_BYTES` | | `memory.retrieval_max_entries` | `ZIRV_CTX_MEMORY_RETRIEVAL_MAX_ENTRIES` | | `memory.harvest_max_entries` | `ZIRV_CTX_MEMORY_HARVEST_MAX_ENTRIES` | | `memory.harvest_max_bytes` | `ZIRV_CTX_MEMORY_HARVEST_MAX_BYTES` | | `memory.session_enabled` | `ZIRV_CTX_MEMORY_SESSION` | | `memory.journal_max_entries` | `ZIRV_CTX_MEMORY_JOURNAL_MAX_ENTRIES` | | `task.max_parent_outcome_bytes` | `ZIRV_CTX_TASK_MAX_PARENT_OUTCOME_BYTES` | | `dash.enabled` | `ZIRV_CTX_DASH` | | `dash.sidebar_cols` | `ZIRV_CTX_DASH_SIDEBAR_COLS` | | `dash.roster_max_age_secs` | `ZIRV_CTX_DASH_ROSTER_MAX_AGE_SECS` | | `dash.max_panes` | `ZIRV_CTX_DASH_MAX_PANES` | | `dash.mouse` | `ZIRV_CTX_DASH_MOUSE` | | `dash.workdir_roots` | `ZIRV_CTX_DASH_WORKDIR_ROOTS` | | `supervise.max_heavy_workers` | `ZIRV_CTX_SUPERVISE_MAX_HEAVY_WORKERS` (deprecated alias for `max_heavy_operations`) | | `supervise.max_heavy_operations` | `ZIRV_CTX_SUPERVISE_MAX_HEAVY_OPERATIONS` | | `supervise.max_writers` | `ZIRV_CTX_SUPERVISE_MAX_WRITERS` | | `supervise.idle_no_tool_secs` | `ZIRV_CTX_SUPERVISE_IDLE_NO_TOOL_SECS` | | `supervise.in_tool_secs` | `ZIRV_CTX_SUPERVISE_IN_TOOL_SECS` | | `supervise.stall_grace_secs` | `ZIRV_CTX_SUPERVISE_STALL_GRACE_SECS` | | `supervise.compact_stall_secs` | `ZIRV_CTX_SUPERVISE_COMPACT_STALL_SECS` | | `supervise.chain_max_restarts` | `ZIRV_CTX_SUPERVISE_CHAIN_MAX_RESTARTS` | | `supervise.chain_max_gap_secs` | `ZIRV_CTX_SUPERVISE_CHAIN_MAX_GAP_SECS` | | `pace.use_credits` | `ZIRV_CTX_PACE_USE_CREDITS_CLAUDE` (the table-node match also blocks `pace.use_credits.codex` alone) | | `pace.poll_enabled` | `ZIRV_CTX_PACE_POLL` | | `pace.poll_min_interval_secs` | `ZIRV_CTX_PACE_POLL_MIN_INTERVAL_SECS` | | `pace.blind_delay_secs` | `ZIRV_CTX_PACE_BLIND_DELAY_SECS` | | `pace.spawn_soft_pct` | `ZIRV_CTX_PACE_SPAWN_SOFT_PCT` | | `pace.spawn_hard_pct` | `ZIRV_CTX_PACE_SPAWN_HARD_PCT` | | `pace.run_budget_tokens` | `ZIRV_CTX_PACE_RUN_BUDGET_TOKENS` | | `pace.estimator` | `ZIRV_CTX_PACE_ESTIMATOR` | | `pace.collector_max_age_secs` | `ZIRV_CTX_PACE_COLLECTOR_MAX_AGE_SECS` | | `pace.five_hour_budget_tokens` | `ZIRV_CTX_FIVE_HOUR_BUDGET` | | `pace.seven_day_budget_tokens` | `ZIRV_CTX_SEVEN_DAY_BUDGET` | | `pace.count_cache_reads` | `ZIRV_CTX_PACE_COUNT_CACHE_READS` | | `review` (`review.claude`, `review.codex`) | `ZIRV_CTX_REVIEW_MODEL_CLAUDE` / `ZIRV_CTX_REVIEW_MODEL_CODEX` | | `worker.claude` | `ZIRV_CTX_WORKER_MODEL_CLAUDE` | | `worker.codex` | `ZIRV_CTX_WORKER_MODEL_CODEX` | | `worker.default_depth` | `ZIRV_CTX_WORKER_DEFAULT_DEPTH` | | `worker.default_read_only` | `ZIRV_CTX_WORKER_DEFAULT_READ_ONLY` | | `handover` (`handover..`) | `ZIRV_CTX_HANDOVER__` (e.g. `ZIRV_CTX_HANDOVER_CLAUDE_DEEP`) | | `endpoint` (`endpoint.claude`, `endpoint.codex`) | none -- `~/.zirv/ctx.toml` only, chooses which vendor account a seat spends | | `route..execution` | `~/.zirv/native.toml` only; selects an official provider process and optional absolute executable path. Repository layers cannot select executables, login methods, billing or startup settings; all effects retain the native broker | | Claude Code authentication environment and public user settings | User-owned process environment and `~/.claude/settings.json` (or an absolute `CLAUDE_CONFIG_DIR` outside the repository); only authentication settings are carried into the restricted model invocation. Official login receives options after `--`. Inherited auth values are excluded from persisted settings and diagnostic output | | `native.toml` keys other than `policy.allowed_routes` and `policy.compaction` | `~/.zirv/native.toml` only; repository `allowed_routes` is intersected with the operator set, and repository `compaction` may only narrow `automatic` to `advisory`, never the reverse | | `safety.allow` | `ZIRV_CTX_SAFETY_ALLOW` | | `safety.escape_allow` | `ZIRV_CTX_SAFETY_ESCAPE_ALLOW` | | `safety.default` | `ZIRV_CTX_SAFETY_DEFAULT` | | `safety.interactive_default` | `ZIRV_CTX_SAFETY_INTERACTIVE_DEFAULT` | | `safety.sql` | `ZIRV_CTX_SAFETY_SQL` | | `score.token_floor` | `ZIRV_CTX_TOKEN_FLOOR` | | `score.token_ceiling` | `ZIRV_CTX_TOKEN_CEILING` | | `score.token_floor_ratio` | `ZIRV_CTX_SCORE_TOKEN_FLOOR_RATIO` | | `score.token_ceiling_ratio` | `ZIRV_CTX_SCORE_TOKEN_CEILING_RATIO` | | `score.model_context_tokens` | `ZIRV_CTX_SCORE_MODEL_CONTEXT_TOKENS` | | `workflow.repo_checks_enabled` | `ZIRV_CTX_WORKFLOW_REPO_CHECKS` | | `workflow.repo_skills_enabled` | `ZIRV_CTX_WORKFLOW_REPO_SKILLS` | | `workflow.repo_agents_enabled` | `ZIRV_CTX_WORKFLOW_REPO_AGENTS` | | `workflow.repo_workflows_enabled` | `ZIRV_CTX_WORKFLOW_REPO_WORKFLOWS` | | `workflow.deploy.tier` | `ZIRV_CTX_WORKFLOW_DEPLOY_TIER` | | `workflow.adoption` | `ZIRV_CTX_WORKFLOW_ADOPTION` | | `workflow.maintain` | `~/.zirv/ctx.toml only` | | `report.repository` | `ZIRV_CTX_REPORT_REPOSITORY` | | `workflow.telemetry_enabled` | `ZIRV_CTX_WORKFLOW_TELEMETRY` | | `workflow.telemetry_max_events` | `ZIRV_CTX_WORKFLOW_TELEMETRY_MAX_EVENTS` | | `workflow.telemetry_retention_days` | `ZIRV_CTX_WORKFLOW_TELEMETRY_RETENTION_DAYS` | | `workflow.check_env_passthrough` | `ZIRV_CTX_WORKFLOW_CHECK_ENV_PASSTHROUGH` | | `workflow.review_worker_budget_tokens` | `ZIRV_CTX_WORKFLOW_REVIEW_WORKER_BUDGET_TOKENS` | | `workflow.review_worker_max_tool_calls` | `ZIRV_CTX_WORKFLOW_REVIEW_WORKER_MAX_TOOL_CALLS` | | `workflow.auto_spawn_on_gate` | `ZIRV_CTX_WORKFLOW_AUTO_SPAWN_ON_GATE` | | `workflow.allow_empty_verify` | `ZIRV_CTX_WORKFLOW_ALLOW_EMPTY_VERIFY` | | `workflow.builtin_checks_exclude` | `ZIRV_CTX_WORKFLOW_BUILTIN_CHECKS_EXCLUDE` | | `workflow.max_context_bytes` | `ZIRV_CTX_WORKFLOW_MAX_CONTEXT_BYTES` | | `price.stale_after_days` | `ZIRV_CTX_PRICE_STALE_AFTER_DAYS` | | `price.table_path` | `ZIRV_CTX_PRICE_TABLE_PATH` | | `search.max_output_bytes` | `ZIRV_CTX_SEARCH_MAX_OUTPUT_BYTES` | | `output.compact` | `ZIRV_CTX_OUTPUT_COMPACT` | | `output.compact_min_bytes` | `ZIRV_CTX_OUTPUT_COMPACT_MIN_BYTES` | | `output.compact_generic_min_bytes` | `ZIRV_CTX_OUTPUT_COMPACT_GENERIC_MIN_BYTES` | | `output.verbatim` | `ZIRV_CTX_OUTPUT_VERBATIM` | | `output.max_summary_bytes` | `ZIRV_CTX_OUTPUT_MAX_SUMMARY_BYTES` | | `output.compact_search` | `ZIRV_CTX_OUTPUT_COMPACT_SEARCH` | | `output.filter` | `~/.zirv/ctx.toml only` | | `output.filter_defaults` | `ZIRV_CTX_OUTPUT_FILTER_DEFAULTS` | | `fallback.orchestrator_rollover_headroom_pct` | `ZIRV_CTX_FALLBACK_ORCHESTRATOR_ROLLOVER_HEADROOM_PCT` | | `fallback.rollover_cooldown_secs` | `ZIRV_CTX_FALLBACK_ROLLOVER_COOLDOWN_SECS` | | `fallback.reactive_force_after_secs` | `ZIRV_CTX_FALLBACK_REACTIVE_FORCE_AFTER_SECS` | | `fallback.health.open_after_failures` | `ZIRV_CTX_FALLBACK_HEALTH_OPEN_AFTER_FAILURES` | | `fallback.health.window_secs` | `ZIRV_CTX_FALLBACK_HEALTH_WINDOW_SECS` | | `fallback.health.cooldown_secs` | `ZIRV_CTX_FALLBACK_HEALTH_COOLDOWN_SECS` | | `fallback.health.degrade_error_rate_pct` | `ZIRV_CTX_FALLBACK_HEALTH_DEGRADE_ERROR_RATE_PCT` | | `fallback.health.degrade_min_samples` | `ZIRV_CTX_FALLBACK_HEALTH_DEGRADE_MIN_SAMPLES` | | `fallback.health.degrade_ttft_ms` | `ZIRV_CTX_FALLBACK_HEALTH_DEGRADE_TTFT_MS` | | `session.persistent` | `ZIRV_CTX_SESSION_PERSISTENT` | | `session.history` | `ZIRV_CTX_SESSION_HISTORY` | | `session.scrollback_rows` | `ZIRV_CTX_SESSION_SCROLLBACK_ROWS` | | `session.stale_after_secs` | `ZIRV_CTX_SESSION_STALE_AFTER_SECS` | | `capabilities` | `ZIRV_CTX_CAPABILITIES` | | `runtime` | `ZIRV_CTX_RUNTIME` | `capabilities` is listed as a whole table rather than key by key: every key under it names an MCP server command zirv spawns, a remote endpoint it authenticates to, a credential reference, or a browser binary it launches, so there is no narrowing half a repository checkout could legitimately set. `runtime` is a whole table for the same reason, in both directions: it decides which provider account a session with no explicit `--runtime` spends, and a checkout moving that onto the operator's metered native routes — or off them — is widening either way. `ZIRV_CTX_RUNTIME` sets `runtime.default` (`harness` or `native`); per-role overrides live in `[runtime.roles]` in `~/.zirv/ctx.toml`. See [Native setup, diagnosis and rollback](#native-setup-diagnosis-and-rollback). The `mail.*`/`chrome.events` entries close the same hole `prompt.max_repo_bytes` does: mail is folded into a launched worker's prompt as its own layer, so a repo raising its own delivered-mail cap (or turning delivery back on after an operator disabled it) would make the operator's choice decorative; a repo silencing the announcement channel would hide its own degradation notices from anyone running zirv there. `prompt.harnesses` closes the same loop for the derived per-adapter harness-roster layer: a repo checkout must not be able to force that layer back on for an operator who turned it off. `prompt.codex_orchestrator` closes the same loop once more for codex's own orchestrator-conventions layer (issue #167): a repo checkout must not be able to re-enable it for an operator who turned it off. `prompt.verbosity` (issue #427) closes the same loop for the Orchestrator-only meta-harness orientation layer's own named tier: a repo checkout must not be able to raise the tier back up for an operator who chose a lower one. `session.*` closes a sharper version of the same hole: `session.persistent` decides whether cloning a repository is enough to make sessions started from it outlive the operator's terminal, and `session.history` decides whether rendered terminal output — tokens, keys and file contents included — is written to disk at all. Neither is a decision a checkout gets to make. `memory.*` closes the same hole again for the memory bank's *configuration* (not its content -- see below): a repo checkout must not be able to switch either scope's gate on or off for itself, raise its own caps, or switch automatic harvesting on for anyone who runs zirv there (see [Memory bank](#memory-bank) below -- the *shared* scope's whole point is that its *content* is deliberately repo-committed, but a checkout still may not flip its own `shared_enabled` gate any more than the private scope's `enabled` gate). `dash.*` closes it once more for the session multiplexer `zirv chat` opens on a capable terminal: a repo checkout must not be able to switch it on or off, resize its sidebar, change how long a quit-time restore roster stays offered, raise its own pane cap, or decide whether the dashboard captures the mouse. `pace.*` closes it for usage pacing: a repo must not be able to flip a spend decision, re-enable the active vendor-API poll fallback an operator turned off, or change its cadence. `review`/`worker` close it for which model spends the operator's tokens running background review or delegated-worker sessions. `handover` closes the same hole for `zirv ctx handover`: a repo checkout must not be able to pick which harness or model the orchestrator seat swaps onto mid-session. `agent` closes a narrower hole discovered once codex shipped out of the box: an explicit `agent = "codex"` reaches `resolve_default`'s *configured* arm, which never consults the repo-narrowing guard the no-`agent`-configured fallback loop has (see [.settings.toml](#settingstoml) below) -- without this, a repo checkout could pick which vendor account gets spent with that guard never in the way. `workflow.adoption` closes the same hole once more for the workflow-adoption nudge/enforce gate (issue #223): a repo checkout must not be able to turn its own adoption pressure down to `off`, or up to `enforce` to hold an operator's own agent dispatches hostage. `fallback.orchestrator_rollover_headroom_pct`/`fallback.rollover_cooldown_secs`/`fallback.reactive_force_after_secs` (issue #358) close the same hole for automatic orchestrator-seat rollover: the on/off switch (`fallback.auto_orchestrator_rollover`) stays repo-narrowable like `fallback.enabled`, but tuning *when* an already-enabled rollover fires or how soon another one may follow picks the same kind of vendor-spend decision `handoff.model`/`optimize.model` already gate, so only the operator may set either. `fallback.health.open_after_failures`/`fallback.health.window_secs`/`fallback.health.cooldown_secs` (issue #455) close it once more for health-aware routing: how many failures make zirv stop trusting a vendor route, over what window, and how long it stays distrusted decides where the operator's tokens get spent, so only they may tune it. `fallback.health.degrade_error_rate_pct`/`fallback.health.degrade_min_samples`/`fallback.health.degrade_ttft_ms` are the same decision one step earlier -- how bad a route has to get before zirv starts ranking it behind the operator's other account -- and are operator-only for the same reason. `fallback.health.enabled` itself stays repo-narrowable like `fallback.enabled` — a checkout may switch the breaker off, never on. Everything else, including `chrome.banner`/`chrome.bar`, `supervise.max_nudges`, and every threshold, is still repo-configurable. #### Environment variables worth knowing | Variable | Effect | |---|---| | `ZIRV_CTX_STATE_DIR` | Where handoffs, sockets, logs and usage state live | | `ZIRV_CTX_TRANSCRIPT` | Pins the transcript `wrap` watches, overriding what the turn signal reports (see below) | | `ZIRV_CTX_SOCKET`, `ZIRV_CTX_SESSION` | Exported into the supervised agent so its hook can find the supervisor. Set by zirv, not by you | | `ZIRV_AGENT__ENABLED` | Enables or disables one adapter (`ZIRV_AGENT_CODEX_ENABLED`); see [.settings.toml](#settingstoml) below. Must be exactly `true` or `false` -- any other value is a hard error naming the variable, matching the strictness of every `ZIRV_CTX_*` boolean | Every `[section] key` in the tables above also has a `ZIRV_CTX_*` variable; the names follow the key, for example `ZIRV_CTX_DEBOUNCE_MS` for `wrap.debounce_ms`, with two deliberate exceptions: a section's own top-level `enabled` flag drops the `_ENABLED` suffix (`ZIRV_CTX_MAIL` for `mail.enabled`, `ZIRV_CTX_PROMPT` for `prompt.enabled`, and so on), and `ZIRV_CTX_QUIET` is a named alias for `chrome.events` set to its *opposite* (`ZIRV_CTX_QUIET=true` turns events off) rather than `ZIRV_CTX_CHROME_EVENTS`, because "quiet" is the more natural spelling for the flag most people will actually reach for. ### Native provider routes > Coming soon: native execution is unavailable in this release. Native routing is opt-in through a separate `~/.zirv/native.toml`; older zirv binaries ignore this file and continue reading the unchanged `ctx.toml`. Start with `zirv ctx provider init`, inspect the offline inventory with `zirv ctx provider list [--json]`, and validate role access with `zirv ctx provider check [--live] [--role ] [--json]`. Live checks are off by default and only call the configured model-list endpoint. Store a `store:` credential without accepting its value as a zirv argument with `zirv ctx provider credential set `. ```toml schema = 1 [endpoint.local] provider = "openai-compatible" base_url = "http://127.0.0.1:11434" vendor = "ollama" [account.work] provider = "anthropic" credential = "env:ANTHROPIC_API_KEY_WORK" billing = "api" pool = "work" [route.work-sonnet] account = "work" endpoint = "anthropic" model = "claude-sonnet-5" [roles] orchestrator = "work-sonnet" [policy] allowed_routes = ["work-sonnet"] ``` Providers with a default URL have an implicit endpoint named after the provider. `openai-compatible` and `aws-bedrock` instead require a `vendor`, and a `base_url` unless the vendor's route profile has a documented one. Account pools default to the account id; two accounts may deliberately share one `pool` when they share quota. Every API-billed account except `openai-compatible` must declare a credential reference. Subscription-billed accounts may omit one. A direct API route on such an account stops at `configured` with an entitlement problem; an explicit provider-owned execution route uses the official harness login, as described below. Credential references are `env:NAME`, `store:`, or `file:` (`~` expands; Unix files must be mode 0600 or stricter). Claude Code/Codex harness login tokens are refused: Claude.ai and ChatGPT subscriptions are entitlements for the harness backend, not native API credentials, and `credential set` refuses those store refs before reading a secret. Every route must declare a model that remains non-empty after an optional matching `vendor/` prefix. Model ids and aliases resolve by exact case-insensitive match. A matching `vendor/` prefix is accepted, but `@date` and `:suffix` decorations are never stripped or silently substituted. The evidence ladder is `recognized` → `configured` → `credentialed` → `reachable` → `authenticated` → `validated`. Catalogue recognition never claims account access, and `authenticated` specifically means a credential was accepted. A credential-less compatible route stops at `configured` offline and can reach only `reachable` during a live check. Direct API offline commands stop at `credentialed`; official-harness checks may attest a signed-in account without a model call (model access remains unverified). For direct API routes, `--live` can establish reachability/authentication, while `validated` remains unavailable until the native model transports record a real validation. Credentials are withheld from plaintext HTTP on non-loopback hosts and that live probe is skipped; loopback HTTP remains available for local runtimes. The optional repository layer `/.zirv/native.toml` may contain only `schema`, `[policy].allowed_routes` and `[policy].compaction`. Its routes are intersected with the operator's set, so a checkout can narrow access but cannot add accounts, endpoints, routes, role bindings, credentials, or permissions. #### Provider-owned execution in the native UI > Coming soon: native execution is unavailable in this release. A route may use an official provider harness while retaining Zirv's native conversation UI, tasks, approvals and tool broker. This is distinct from direct API execution: the official process owns the model connection, agent loop and local conversation. Provider identity, execution backend, authentication owner and billing are separate route facts. Initially the adapter registry supports `claude-code`; adding another provider requires an execution adapter and an independent review of that provider's rules, not a subscription-token transport. ```toml # ~/.zirv/native.toml (operator configuration only) schema = 1 [account.personal-claude] provider = "anthropic" billing = "subscription" # Use "api" for Console/API/cloud billing. # No credential. Claude Code owns authentication. [route.personal-claude] account = "personal-claude" model = "sonnet" execution = { adapter = "claude-code" } # Optional absolute path; spaces are supported: # execution = { adapter = "claude-code", program = "/opt/Claude Code/claude" } [roles] orchestrator = "personal-claude" worker = "personal-claude" ``` Run `zirv ctx provider login personal-claude` to hand the terminal directly to **official Claude Code's login**, then `zirv ctx provider status personal-claude`. Zirv does not read, import, store or refresh subscription credentials, login URLs or authorization codes. A missing binary is never silently installed. Removing this route from `native.toml` leaves Claude Code signed in. To log out of the official installation, run `claude auth logout` yourself; that affects Claude Code sessions outside Zirv too. All official authentication methods and subscription plans are accepted. Pass login options after `--`, for example `zirv ctx provider login personal-claude -- --console` or `-- --sso`. API keys, cloud credentials and profiles configured in the launching user's environment remain available to Claude Code. Its public user settings retain `apiKeyHelper`, `awsAuthRefresh`, `awsCredentialExport`, login restrictions and authentication environment variables. `CLAUDE_CONFIG_DIR` may select an absolute user configuration directory outside the repository. Zirv never copies inherited authentication secrets to its settings, journal or diagnostics. `zirv chat --runtime native` uses this route for the orchestrator. Native workers use their configured role routes and retain the existing task, mail, delegation, writer-lease and generation fences. The UI identifies the backend and selected billing. Provider status and headless final status report the detected official authentication method, upstream provider and billing, using `unknown` when the public status cannot establish them. Headless final status schema **3** includes an `execution` object with backend/version/auth owner, estimated API-equivalent cost, and separate nullable billed-spend and remaining-allowance fields. **Initial capability boundary.** Requires a local, unmodified native Claude Code 2.1.248+ executable in the 2.1 series, its documented restricted/settings/tool/MCP flags, and a public status response attesting authentication. Windows/WSL (managed-policy verification pending), npm shell shims, managed startup policy, unauthenticated status, and an explicit token-budget ceiling are rejected before a model turn. Turn/time limits, streaming, follow-up and exact-session continuation are supported. Steering waits until the next turn; it is never reported as immediate. No live installation/platform compatibility is implied by fixture tests. Claude Code starts in a private Zirv directory with project settings excluded, only authentication settings carried from public user configuration, hooks disabled, and only the explicit Zirv MCP server. Managed startup configuration that could override this boundary is rejected. Claude built-in tools and internal subagents are unavailable on this route; coding, shell, external services and independently scheduled workers use Zirv MCP tools. Every such effect passes the existing native broker, sandbox, exact-action approval, repository narrowing and generation checks. A denied approval produces a tool error, never an automatic permission-mode escalation. Streamed tool observations are not executed again. Tool subprocesses receive no provider credentials. **Billing.** Claude Code selects authentication using its own precedence. The account's `billing` declares scheduling intent; it does not force a login method. Keep it aligned with the method you select in Claude Code. Detected billing is reported separately and is never hardcoded to subscription. No authentication failure or exhausted allowance causes an automatic API fallback. All aliases of the local official login share one `anthropic` capacity pool. Unknown allowance is unknown, not unlimited. The official result's dollar figure and `zirv ctx spend` are API-equivalent estimates, not invoices. Billed spend remains unknown. Paid usage credits can permit additional upstream charges: disable them in Claude's **Settings → Usage** if desired; Zirv cannot verify that switch or promise zero additional charges. A successful turn persists an exact Claude session reference bound to its Zirv seat, route and workspace. Follow-up resumes only that reference. Cancellation before any tool effect can resume; a crash or interrupted effect with uncertain delivery leaves a reconciliation block. Inspect effects and use a new session with a portable checkpoint before retrying; Zirv never blindly replays the task. A provider-owned continuation cannot become an API conversation. Cross-runtime handoff transfers Zirv task/artifact/approval state, with the existing exclusive writer and worker/mail disposition rules. **Policy evidence, checked 2026-09-15.** This design relies on Anthropic's explicit allowance for running the unmodified binary under the applicable terms and conditions, including the product operator agreeing to Commercial Terms and each user authenticating and being billed under their own agreement. Zirv does not resell usage or claim Anthropic approval. Written confirmation for this exact integration has not been obtained; this is not a guarantee of policy compliance. It does not offer Zirv-owned Claude login or claim endorsement. The June 15 pause means print-mode usage currently draws subscription limits; this is not a guarantee of permanent eligibility. The SDK documentation retains restrictive third-party-login language; extending this design beyond the unmodified-binary allowance requires clarification from Anthropic. See [legal and compliance](https://code.claude.com/docs/en/legal-and-compliance), [the current subscription notice](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan), [programmatic execution](https://code.claude.com/docs/en/headless), and the [decision and validation note](docs/superpowers/2026-09-15-provider-execution.md). This replaces #644's Claude-specific token-import proposal; it grants no entitlement for other providers. Direct Anthropic API routes still work without Claude Code installed and continue rejecting harness-login secrets before HTTP. **Opt-in live smoke (uses your own allowance).** Record `claude --version` and the sanitized `provider status` output. In an authorized disposable worktree, open `zirv chat --runtime native`, ask for a bounded file edit and its verification, and check streamed text, one tool receipt per effect, and the backend/billing label. Send a follow-up and confirm the same external session in the journal's `provider_execution` checkpoint. Interrupt a response before tool work, then resume with a follow-up. For an interrupted effect, verify the reconciliation block instead of replaying it. Finally restart/reattach the Zirv session and check task/mail state. Never copy raw auth output, MCP configuration or login codes into a report. Fixture results and actual live versions/platforms must be reported separately. #### Native setup, diagnosis and rollback > Coming soon: native execution is unavailable in this release. Native setup is **coming soon**. Provider commands and migration to native are blocked in this release; `zirv ctx doctor` reports `coming-soon` without probing providers or credentials. The commands below document the future setup flow: ```bash zirv ctx provider init # write ~/.zirv/native.toml $EDITOR ~/.zirv/native.toml # declare account, route, roles zirv ctx provider credential set work # store the secret out of band zirv ctx doctor # verify, class by class zirv ctx exec --runtime native -- "…" # first native run ``` `zirv ctx doctor [--role ] [--live] [--json]` is the readiness command. For every role it prints which backend an unflagged session would get and which authority decided that (`flag`, `runtime.roles`, `runtime.default`, `built-in`), which route it would spend, and how far that route got up the evidence ladder. Every problem it finds is sorted into exactly one class, because the operator's next action is different for each: | Class | What it means | What to do | |---|---|---| | `missing-auth-material` | No API key resolved for the account, or the one that resolved was rejected | Set the `env:`/`store:`/`file:` reference, or `zirv ctx provider credential set ` | | `inaccessible-model` | Auth material works; this model is not one this account may call | Pick a model the account is entitled to, or fix the alias | | `missing-tool` | A binary, MCP server or configured integration is absent — including a native adapter zirv has not shipped yet | Install/configure it; a missing adapter is a zirv gap with a tracking issue, never an entitlement excuse | | `unsupported-isolation` | No verified process containment on this platform | Install `bwrap` (Linux); on Windows there is no verified backend yet, and sandboxed invocations are refused rather than run unconfined | | `service-failure` | The endpoint is configured and credentialed but did not answer | Check the endpoint URL, the network, and the provider's status | | `upstream-entitlement` | A genuine upstream limitation, not a zirv gap: a subscription-billed account, or a vendor surface that exists only inside that vendor's CLI | Use an API-billed account, or keep that surface on the harness backend | For provider-owned routes, doctor checks the official CLI's non-model version/auth interfaces and may create its private startup directory. It exits `1` only when a role that *would* run natively has no usable route. Its output is redacted the way `zirv ctx snapshot` is — every line is screened for credential shapes, high-entropy and opaque runs, and any line that opens like a conversation turn is replaced outright — so a doctor dump is safe to paste into a bug report. It carries no transcript text and no continuation data by construction. **Billing.** A route's `billing` is `api` or `subscription`. Direct provider execution requires API billing; explicit provider-owned execution can use its official harness login under the boundary above. Subscription tokens remain refused as native API credentials (`credential set` refuses those store refs before it reads a secret). Accounts that share quota share a `pool`, and the usage windows (`zirv ctx usage`) aggregate per pool, so two accounts on one plan are not double-counted. `zirv ctx spend --by` groups the delegation ledger by `harness`, `model`, `task-class`, or `worker` instead -- it has no `pool` dimension of its own. **Choosing the default.** `~/.zirv/ctx.toml`'s `[runtime]` table decides which backend a session gets when the command line does not say: ```toml # ~/.zirv/ctx.toml [runtime] default = "native" # or "harness"; absent means "harness" [runtime.roles] reviewer = "native" # per-role override, outranks `default` worker = "harness" ``` An explicit `--runtime harness|native` always wins over both. `zirv ctx exec` and `zirv ctx agent` default that flag to `configured`, which is exactly "ask this table"; `zirv chat` with no `--runtime` resolves the same way at the `orchestrator` role. A value this build does not recognise degrades to the harness with a one-line note rather than failing the command — `zirv ctx doctor` is where it is reported. The whole `[runtime]` table is `REPO_FORBIDDEN` (see [Trust boundary](#trust-boundary)). **Migration and rollback.** `zirv ctx config migrate [--to harness|native] [--dry-run]` brings `~/.zirv/ctx.toml` to schema 2 — the `[runtime]` table above — backing the previous document up to `~/.zirv/ctx.toml.pre-schema-2.bak` and recording the schema in a sidecar `~/.zirv/ctx.migration.toml`. The marker is deliberately *not* a key inside `ctx.toml`: that file is parsed with unknown keys rejected, so an older zirv binary would refuse the whole configuration rather than ignore one key. Running the migration twice writes nothing the second time and says so, so a re-run can never overwrite the real pre-migration backup. `zirv ctx config migrate --downgrade` restores that backup byte for byte (or, with no backup, removes the `[runtime]` table), and deletes both markers. It is the supported way back to an older zirv: install the older binary *after* downgrading, since an older binary cannot parse `[runtime]` either. Nothing else is part of the transaction — `~/.zirv/native.toml`, native journals, and the harness conversation references those journals carry are all outside the file being migrated and survive a round trip in either direction. Both modes can run concurrently on one machine throughout: a native and a wrapped session share the state directory, session registry, mail, task cards and cost ledger. #### Entitlement limitations versus implementation gaps These are not the same thing and zirv never conflates them. An *entitlement limitation* is something the upstream vendor does not sell zirv access to; an *implementation gap* is work zirv has not done, and every one of them has a tracking issue. `zirv ctx doctor` classifies the first as `upstream-entitlement` and the second as `missing-tool`, and `docs/design/native-parity.md` lists both exhaustively. The current lists: Genuine upstream entitlement limitations: - **Subscription plans are not API entitlements.** A Claude.ai or ChatGPT subscription cannot be spent through a direct provider API call. Native direct API routes on a subscription-billed account stop at `configured` with that problem named. Explicit official execution routes use the vendor harness while retaining the native Zirv seat. - **Harness login tokens are refused as credentials.** Reusing the vendor CLI's stored login for direct API calls is outside what that token is issued for; `credential set` refuses those store refs before reading a secret. - **Vendor-CLI-only surfaces.** `zirv ctx wrap` supervises a vendor TUI by definition, and `zirv ctx handover` swaps one vendor CLI for another; a native session has neither. Changing a native seat's model is `[roles]` configuration, not a handover. - **Model entitlement per account.** A model absent from an account's own model list is that account's entitlement, reported as `inaccessible-model`; zirv cannot grant it. Implementation gaps (zirv's own work, each tracked): - **Route profiles with no adapter yet** are reported as `missing-tool` with their tracking issue in the message, explicitly as "a zirv gap, not an upstream entitlement limit". - **Windows process isolation.** There is no verified restricted-token/ AppContainer helper shipped, so `PlatformIsolation::detect` reports unavailable on Windows and sandboxed invocations are refused rather than run unconfined. Linux (`bwrap`) and macOS (`sandbox-exec`) are supported. - **Live-provider validation.** Every parity row is fixture- or service-level; the `validated` rung of the evidence ladder is not reachable until the N19 validation pass records a real one. #### Native compaction > Coming soon: native execution is unavailable in this release. A native session compacts itself rather than calling a harness `/compact`. Committed journal events and the provider's own measured input footprint (fresh input plus cache writes plus cache reads) are projected into zirv's existing rot scoring engine, with the token gate sized from the route model's declared context window less the run's output reservation — not from the harness-transcript token constants. An unknown context window falls back to the absolute `score.token_floor`/`score.token_ceiling` defaults rather than a guess. Four typed triggers are reported: `context_overflow` (the provider refused the request), `token_pressure` (measured input reached the derived ceiling), `repeated_identical_errors`, and `loss_of_progress`. The first two force a compaction; the others are advice until the rot gate escalates. A compaction writes a versioned portable checkpoint — objective, hard constraints, task/workflow refs, every acknowledged input, task claims, completed-action receipts, outstanding tool calls, and evidence by SHA-256 — as an atomic export under `/native-checkpoints/` plus one journal event, which is the commit point. The original history and every stored artifact are retained: the summary only replaces what the next provider request sends, and the cacheable system prefix is never rewritten. The summary boundary never crosses a tool call whose effect has not settled, so compaction cannot mark a pending action complete, and acknowledged input that has not been answered is repeated verbatim. Distillation runs through the session's own native route with a bounded output budget and no tool schemas at all, and falls back to a deterministic structural summary whenever no model capacity, credential or valid reply is available — so it works with no coding-harness binary installed. A same-route resume keeps the provider's opaque continuation envelope. A route, model, endpoint, account or protocol change discards it and rebuilds a portable semantic history from the checkpoint and the journal: text and refusals only, with no hidden reasoning, no provider signature, and no synthesized tool outcome — an unknown outcome is carried as unknown. `[policy].compaction` is `automatic` (the default) or `advisory` (zirv reports the pressure and never compacts on its own). A repository layer may narrow it to `advisory` and can never widen it back — see [Trust boundary](#trust-boundary). `zirv ctx status` prints one line per native session that has compacted or resumed, with the newest reason, and `zirv ctx exec --runtime native` reports the same facts in its final-status JSON. #### Route profiles Every configured route binds to a *route profile*: the versioned record of one vendor route's documented base URL, request path, credential class, capability caveats and permitted provider-native options. `zirv ctx provider list` prints the whole registry and the profile each route bound to. A route whose vendor has no profile is refused at load time rather than sent to a guessed endpoint. The primary families keep their own transports (`anthropic`, `openai`, `google`, `google-vertex`). Everything else speaks one of two: | Family | Provider | Vendor | Protocol | |---|---|---|---| | DeepSeek, xAI, Qwen (DashScope), Moonshot/Kimi, Mistral, Zhipu/GLM, MiniMax, Meta (Llama API) | `openai-compatible` | the vendor slug | chat completions | | Ollama, LM Studio, vLLM | `openai-compatible` | `ollama` / `lmstudio` / `vllm` | chat completions | | Azure OpenAI | `azure-openai` | (fixed) | chat completions | | Amazon Nova and every Bedrock-hosted family | `aws-bedrock` | the vendor slug | Bedrock Converse | A vendor with a documented base URL does not need one configured: ```toml [endpoint.deepseek] provider = "openai-compatible" vendor = "deepseek" [account.deepseek] provider = "openai-compatible" credential = "env:DEEPSEEK_API_KEY" [route.reason] account = "deepseek" endpoint = "deepseek" model = "deepseek-v4-pro" [route.reason.extensions] temperature = 0.2 ``` `[route..extensions]` carries provider-native request options and is validated against the profile's typed allow-list: an unknown key, a wrong type or an out-of-range value is a load error that names the accepted keys. There is no free-form passthrough, and an extension can never overwrite a protocol-owned field such as `messages` or `tools`. A compatible endpoint is not a feature superset of OpenAI's. Capabilities are declared per profile, and a request asking for something the profile does not declare -- tools, a reasoning-effort control, a thinking configuration, prompt caching -- is refused with a typed failure instead of being silently stripped. Reasoning text a chat-completions endpoint emits (`reasoning_content`) is streamed for display but never replayed as continuation state, because it carries no signature. #### Local models `ollama`, `lmstudio` and `vllm` routes default to their runtime's own loopback address (`127.0.0.1:11434`, `:1234`, `:8000`). Their credential class is "none" or "optional": no key is fabricated for them, and declaring one on a key-less local route is an error rather than a secret sent to a local server. Plain `http://` is accepted only for a loopback or private address (a literal one -- a hostname is never assumed local); a credential is still never sent in the clear to a non-loopback host. #### Cloud routes `aws-bedrock` requires `account..region` and signs every request with SigV4; its credential is a JSON object rather than a bare key, because a signature needs a key pair: ```toml [endpoint.bedrock] provider = "aws-bedrock" base_url = "https://bedrock-runtime.us-east-1.amazonaws.com" vendor = "anthropic" [account.aws] provider = "aws-bedrock" credential = "store:aws-bedrock" # {"access_key_id":"…","secret_access_key":"…"} region = "us-east-1" [route.bedrock-sonnet] account = "aws" endpoint = "bedrock" model = "claude-sonnet-5" ``` `azure-openai` requires `account..api_version` and a per-route `deployment`; the request is addressed at `{base}/openai/deployments/{deployment}/chat/completions?api-version=…` with an `api-key` header, and carries no model id, because on Azure the deployment names the model. Each identity field is required by exactly one provider and forbidden on every other, so a Vertex `project`, a Bedrock `region` and an Azure `api_version` can never be read as one another. #### Broker subscriptions GitHub Copilot and Factory/Droid resell models under their own subscription identity, with no documented, separately authorized direct API. Their profiles are `legacy-only` with that reason recorded, configuring one as a native route is refused, and they remain available through their harness backend. `zirv ctx provider list` distinguishes this from `planned`, which means zirv has not written an adapter yet -- the two are never conflated. ### .settings.toml `ctx.toml` tunes how the ctx supervisor *behaves*; `.zirv/.settings.toml` is a separate, zirv-wide file that only answers yes/no questions about what zirv may *use*. The rule of thumb: if the question is "yes/no, may zirv use this thing", it goes in `.settings.toml`; anything else goes in `ctx.toml`. Today that means one section, per-adapter enable/disable: ```toml # .zirv/.settings.toml [agents.codex] enabled = false ``` Layered the same way as `ctx.toml` -- `~/.zirv/.settings.toml`, then `/.zirv/.settings.toml`, then `ZIRV_AGENT__ENABLED` (a boolean) -- but folded per agent rather than deep-merged: ``` final(name) = env(name) if set else home(name).unwrap_or(true) && repo(name).unwrap_or(true) ``` Every known adapter defaults to enabled. The environment is the operator, in both directions: `ZIRV_AGENT_CODEX_ENABLED=true` re-enables an agent a repo disabled, and `=false` disables one nothing else touched. A repository can only narrow what it inherited -- `enabled = true` in a repo's own `.settings.toml` is a silent no-op, since there is nothing there for a repo to refuse. Disabling an agent is checked before that adapter's own readiness, so `--agent codex` with codex disabled reports the disable, not codex's own `ready()` outcome. `zirv ctx status` lists every known adapter, whether it is enabled, and (when not) which file or variable disabled it. A malformed *repo* `.settings.toml` never falls back to a fully permissive gate: `zirv ctx optimize` and the Stop hook both fall back to the operator's own layers only (home file, then environment) if the full config cannot be loaded, so a broken repo file can narrow what an operator already disabled but can never revive it. A repo disable can also never *pick* an agent on the operator's behalf: if a repo-only `.settings.toml` disables the agent that would otherwise have been the default (no `--agent`, no `agent =` configured, and that adapter is the first enabled-and-ready one in registry order), zirv refuses rather than silently falling back to a different, still-enabled adapter -- naming both the disabled agent and the one it would have picked, and how to choose explicitly (`--agent`, `agent =` in your own `~/.zirv/ctx.toml`, or the `ZIRV_CTX_AGENT` environment variable — the repo's `.zirv/ctx.toml` cannot set `agent`; it is a forbidden repo key). Narrowing which agent is *possible* is a repo's call; narrowing which one you actually get is not. ### Hook registration (Claude Code) Add to `~/.claude/settings.json`: ```json { "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "zirv ctx hook stop" }] }], "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "zirv ctx hook prompt" }] }], "PreCompact": [{ "hooks": [{ "type": "command", "command": "zirv ctx hook pre-compact" }] }], "PreToolUse": [{ "matcher": "Agent|Task", "hooks": [{ "type": "command", "command": "zirv ctx hook pretool" }] }], "SessionStart": [{ "matcher": "resume|clear", "hooks": [{ "type": "command", "command": "zirv ctx hook session-start" }] }] } } ``` `SessionStart` (issue #244) re-injects the latest stored handoff on `resume`/`clear` — a bare `claude --resume` or `/clear` sees it too, not only `zirv ctx resume`'s own explicit flow. The Stop hook forwards verdicts to a supervising `wrap` or `exec` when one owns the session, and otherwise prints a non-blocking advisory. It never blocks a stop, and it exits 0 even when it is invoked wrongly, because Claude Code reads a Stop hook's exit 2 as "block the stop". The Stop hook is also how a supervisor learns which file the agent is writing: the agent mints its own session id, so the transcript path travels on the turn signal the hook sends. Register it, or `wrap` has nothing to verify a compaction against and no context to distil a restart handoff from. Current Codex versions support lifecycle hooks with the same JSON event shape. `zirv setup apply` merges Zirv's handlers into `~/.codex/hooks.json`; review and trust new definitions with `/hooks` in Codex. The older `zirv ctx hook notify` compatibility entry point remains available for Codex versions configured with the external `notify` program. Rollout event parsing is still tracked in [issue #11](https://github.com/Glubiz/zirv-cli/issues/11). ### Interactive use ```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: ``` 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 `-- ` 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. ```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 | Code | Meaning | |---|---| | the child's own code | the run finished on its own | | `75` | rot 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` | | `76` | the same, for a wall-clock timeout rather than rot | | `77` | the token or tool-call budget was reached; the run checkpoints and stops without restarting | | `78` | provider capacity or overload errors persisted until the restart budget was spent | | `79` | the provider account ran out of credits or quota; the run stops without retrying | | `80` | a writing delegation was refused because the tree already has a live writer; retry after it finishes or use `--worktree` | | `81` | progress stalled after a steering nudge and its grace period, and the restart budget was spent | | `82` | the worker report failed its result contract or claimed deliverables that do not exist | 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. ### Usage pacing Long autonomous runs die if a subscription window (5 hour rolling, 7 day) runs dry mid-task. `zirv ctx loop` and `zirv ctx exec` consult a pacing gate before every spawn and every restart, and wait instead of exiting when a window is at or above `pace.max_percent` (default 99). Three data layers, best available wins: 1. **Collector**, server-authoritative. Claude Code's statusline input carries `rate_limits.five_hour` and `rate_limits.seven_day` for Pro and Max sessions after the first response. Wire your statusline through the tee and every live session keeps machine-wide state fresh: ```json { "statusLine": { "type": "command", "command": "zirv ctx usage tee -- bash ~/.claude/statusline-command.sh" } } ``` The tee records the fields, then runs your original command unchanged. It always exits 0 and always prints a statusline, so a failure here can never leave you looking at a blank one. 2. **Estimator**, an approximation. When no fresh collector reading exists, zirv sums token usage across local transcripts (including subagent files) over the trailing window. It is off until you set a budget, because a plan's real token allowance is undocumented and a made-up default would read as data: ```toml [pace] five_hour_budget_tokens = 0 # set to enable the 5h estimate seven_day_budget_tokens = 0 # set to enable the 7d estimate count_cache_reads = false # cache reads are discounted, so excluded ``` 3. **Circuit breaker**, authoritative on trip. If the agent prints a documented limit-hit notice, that is treated as 100% no matter what the other layers say: the run is parked until the window resets and then relaunched, **without consuming the restart budget**. Full pacing configuration: ```toml [pace] enabled = true max_percent = 99.0 collector_max_age_secs = 900 estimator = true jitter_secs = 30 fallback_delay_secs = 900 # used when a window's reset time is unknown wait_slack_secs = 3600 # head room added to the window's own length # max_wait_secs = 7200 # optional absolute override, see below ``` #### How long a pause can last The wait is bounded per window, not by one global clock: at most the window's own length plus `wait_slack_secs`, so a five-hour trip is bounded near six hours and a seven-day trip is allowed to wait out the week. That distinction matters, because resuming a seven-day window every few hours would spend tokens against a window that has not reset, which is exactly what pacing exists to prevent. When a window's reset time is known and lands inside that bound, the pause ends at the reset (plus jitter) and not before. Set `max_wait_secs` only if you would rather a supervisor give up waiting and proceed after a fixed time; it replaces the per-window bound entirely and is unset by default. A pause is announced once, not once per check, and appears in the decision log as a single `pace-wait` entry. Parks and relaunches are logged too. Check the current picture, including how fresh each reading is, with `zirv ctx usage`. ### Cross-harness fallback and handover Beyond waiting out a subscription window, zirv can steer work onto a *different* harness. `fallback.rs` connects the agent roster, usage windows, model-tier ladder, and delegation path: a **new** delegation (`zirv ctx agent`, a dashboard spawn) can be rerouted away from an exhausted or measured-low-headroom harness before it ever starts, while an **already running** supervised session can only move once that harness itself stops on a recognized usage-limit message — steering never interrupts a session that is still making progress. Either way the alternate harness must be enabled, ready, capacity-compatible, budget-compatible, and able to provide a verified equivalent model tier (`cheap`/`standard`/`deep`); zirv never guesses a tier translation for an operator-pinned model it cannot verify. `zirv ctx status` reports the resolved policy and, for each harness in the fallback order, its readiness, capacity and current headroom: ``` fallback: enabled | order claude -> codex | steer below 20% headroom | candidate min 10% | unknown assumes 25% fallback claude: enabled / ready / full / 62% measured fallback codex: enabled / ready / small-only / 25% assumed ``` Each harness line is `{enabled|disabled} / {ready|unavailable} / {small-only|full} / {headroom}`: whether `.settings.toml`/`ZIRV_AGENT__ENABLED` has that adapter on, whether it currently resolves and is ready to launch, whether `[agents]` capacity-limits it to small tasks only, and the same measured/ assumed/opted-out headroom reading described above. Configure it under `[fallback]` in `~/.zirv/ctx.toml`: ```toml [fallback] enabled = true order = ["claude", "codex"] predictive_headroom_pct = 20.0 # steer new work below this headroom min_candidate_headroom_pct = 10.0 # a candidate needs at least this much headroom to accept work unknown_headroom_pct = 25.0 # assumed headroom when no reading exists (0 opts out) small_task_max_tokens = 40000 small_task_max_tool_calls = 24 adaptive_delegation = true # issue #358: route new delegations through the pure capacity allocator auto_orchestrator_rollover = false # issue #358: let the orchestrator seat itself roll over automatically (off by default) orchestrator_rollover_headroom_pct = 20.0 # issue #358: threshold that arms a proactive rollover (defaults to predictive_headroom_pct) rollover_cooldown_secs = 600 # issue #358: minimum gap between two automatic rollovers reactive_force_after_secs = 120 # issue #401: grace before a confirmed block forces a mid-turn structural handover [fallback.harness.codex] # issue #358: per-harness overrides, both optional max_active = 3 reserve_headroom_pct = 15.0 ``` A repository checkout may only narrow these values (see [Trust boundary](#trust-boundary) above); `ZIRV_CTX_FALLBACK*` environment variables are the operator's final override. `orchestrator_rollover_headroom_pct`, `rollover_cooldown_secs`, and `reactive_force_after_secs` are repo-forbidden outright (see the table above) — tuning an already-enabled rollover's timing is an operator decision, the same as `handoff.model`. **The displaced harness is parked, not closed.** A rollover has to quit the source child — one seat, one live pane — but quitting a harness does not destroy its conversation. The seat records the harness it was rolled off, together with that harness's OWN conversation id (observed at a turn boundary, which is the only place zirv's session uuid and the harness's conversation id are both visible), and keeps it until the seat comes home. When that harness reads healthy again — its window reset, its endpoint recovered — the reclaim path returns the seat to it and **resumes that same conversation** rather than starting the operator over, with the interim harness's own distilled handoff packet delivered as the resumed conversation's first message: continuous conversation, plus a record of everything that happened while it was parked. The first displacement wins, so a seat that hops twice still wants its original harness back; an operator's own `zirv ctx handover` owes no return. A return is only taken on a harness that can resume *and* accept a prompt in the same launch (claude's `--resume ""`); anything else keeps the previous behaviour — a cold launch carrying the packet — because resuming a conversation without telling it what happened in its absence would silently drop the interim's work. **A rollover can cross runtimes, and the seat keeps its identity.** The same prepare/commit/abort transaction moves a seat between backends as well as between harnesses — `harness→harness`, `harness→native`, `native→harness`, `native→native` — and the seat's short id and generation lineage are exactly what does not change. What changes is which backend answers at that address, which `zirv ctx status` names inline (`seat: claude opus [native] gen 3`) alongside the harness or route the seat was displaced from and a one-line rollover record: trigger, direction, every route tried with why it was refused, and whether the seat committed, **kept the original session** (preparation failed, so the seat never moved), or **parked** honestly with all its durable state intact. The record lives at `/sessions/.rollover.json`. Before a successor is prepared at all, a native source reaches a safe boundary: in-flight work is drained or explicitly cancelled, any tool effect that *began and never reported* is carried as outcome-unknown (never retried, never called failed), and one portable checkpoint — acknowledged input with whatever is still owed, task claims, completion receipts, outstanding tools, evidence — is committed to the journal. An outcome-unknown effect **halts the successor** until it is reconciled: the whole point is that a fresh model must not re-run it. Portable checkpoints stay strictly separate from a provider's own continuation envelope, and only a native successor on the *identical* route may keep that envelope; every other direction rebuilds a legal semantic history, and a coding-harness successor never sees another vendor's envelope at all. Two rules are worth stating because they are refusals rather than fallbacks. A successor is validated — policy, capabilities, context room, billing authority, authentication, budget, startup — *before* the source is given up, and a failure keeps the original session. And a rollover is only authorized to move work onto the billing posture the seat **already** spends (plus local runtimes, which have no credential and no invoice): moving subscription work onto metered API credit is a decision an operator makes, never a silent consequence of capacity. Native subagents a rolling seat owns are finished, stopped, or retained under recorded ownership — zirv has no mechanism that migrates a running worker to another seat, so it never claims one. Only one seat generation is write-capable at a time, and that is enforced rather than assumed: a stale generation is refused at native tool effects (the journal's own fence), at wrapped tool effects (the pre-tool hook), at `zirv agent` delegation, at the coordinator's plan graph and at the per-tree writer lease. In the window between prepare and commit the *source* still holds the seat, so a successor that has been validated but not committed cannot write either — which is what makes a crash at either boundary incapable of producing two writers. **Cross-harness capacity, at a glance.** `zirv ctx status` (and `zirv ctx status --json` for machine-readable output) reports a pool section built on the same pure allocator: each harness's scheduling state (`ready` / `draining` / `hard-blocked` / `unknown` / `disabled`), its provider's headroom after outstanding reservations, and — when `auto_orchestrator_ rollover` is on — the orchestrator seat's own fencing generation. Provider token reservations are tracked in a small durable ledger (`/reservations/.json`) so two admitted-but-unsettled delegations against the same billed account are never double-counted (capacity snapshot formula, the fenced rollover transaction, anti-flap rules). #### Health-aware routing Usage headroom answers *may this account spend more*. It says nothing about whether the endpoint can be reached: a session that dies on `API Error: Connection refused` has full headroom and zero capacity. So alongside the headroom rules above, zirv keeps a small circuit breaker per **harness**, fed by the structured provider-error rows a transcript already carries. The harness is the whole identity: the failing hop in a transport or server failure is the connection or the endpoint, not the model, so an open breaker on one model would have to steer work away from the others anyway. The last model seen is recorded alongside it, as information for `status` only. Six phases, per route: 1. **healthy** — routed to normally. 2. **suspect** — some transport/server failures inside `window_secs`, still routed to. 3. **degraded** — reachable, but measurably worse than it should be; still routed to, just ranked behind every healthy alternative. 4. **open** — `open_after_failures` reached: excluded from routing for `cooldown_secs`. 5. **half-open** — the cooldown elapsed; exactly one trial is admitted. 6. **unavailable** — an authentication, permission or model-not-found error; denied at once rather than after `open_after_failures`, then re-probed on the same `cooldown_secs` so a credential you have since fixed heals on its own. Only *transport* (connection refused/reset, DNS, proxy, socket hang-up, timeouts) and *server* (overloaded, internal server error, service unavailable, bad or timed-out gateway) failures count towards opening a breaker. `unavailable` needs an explicit provider token (`authentication_error`, `invalid_api_key`, `permission_error`) or a structured 401/403/404 — never ordinary English, so a sandbox saying "permission denied" about a file cannot deny a vendor route. A rate limit is capacity, which the headroom rules above already own; a context overflow belongs to the session, which rot owns; anything unattributed changes nothing. Classification is gated on the structured rows only (Claude's `isApiErrorMessage`, codex's `task_complete.error`), so an agent merely *writing* about an outage can never trip a breaker. **Degraded** is the soft half of the same idea, and the only phase that reduces a route rather than excluding it. Two signals can reach it, both read over `window_secs`: - a rolling transport/server error rate at or above `degrade_error_rate_pct`, once the window holds at least `degrade_min_samples` dated turn outcomes (failures plus completed error-free turns); - a first-token latency whose median is at or above `degrade_ttft_ms`, over the same minimum sample count. **Opt-in**: `degrade_ttft_ms` is unset by default. Leaving degraded takes more than crossing back over the line: the error rate must fall below **half** `degrade_error_rate_pct`, or the latency median below **three quarters** of `degrade_ttft_ms`, or the evidence must age out of the window entirely. Without that hysteresis a route sitting on the threshold would flap in and out on every poll. A single completed turn never clears a degradation on its own — a rate is not a turn. The latency signal is off by default because of what a transcript can actually measure. Neither harness records a first-token time, so it is derived from row timestamps: the gap between the user row that opened the turn and the first assistant row after it. That interval includes the model's own thinking, so a reasoning model reads "slow" on a perfectly healthy endpoint. Inter-token latency is not measurable from either transcript at all. So latency only ever *reduces* a route: it never opens a circuit, never marks a route unavailable, and never migrates a session. A degraded route is reduced, never excluded. It loses every comparison to a healthy candidate regardless of headroom, and `zirv agent` steers a delegation off it — but only when a healthy alternative exists. If every route is degraded, the work stays where it was asked for: trading one degraded route for another is churn, not a reroute. A degraded seat route is not a rollover trigger either. **Shared endpoints.** When `[endpoint.claude]` and `[endpoint.codex]` are both pointed at one gateway, the two routes have one dependency, and failing over between them just buys a second failure. So a breaker that opened on a **transport or server** failure also denies any other harness whose configured `base_url` resolves to the same `host[:port]`, naming the route that actually failed. Only a configured endpoint override defines a shared dependency — native default accounts are always independent, because zirv knows nothing about the estate behind them. Authentication failures never propagate (a rejected credential belongs to one account, not to the host), and neither do rate limits, `unavailable`, or degradation. The dependency is only ever the host and port: no credentials, no paths, no full URLs reach a log or a status row. An alias denial excludes *placements* only — the seat sitting on the aliased route keeps working and never rolls over for its sibling's failure. **One trial at a time.** A half-open route admits exactly one recovery probe, so two dispatches landing in the same cooldown cannot both "test" a broken endpoint. Whoever gets there first claims the trial, recorded as one `health-trial` line; anyone else is told who is holding it, and when it frees itself, and re-runs placement once with that route excluded. A dispatch that still has nowhere healthy to go is refused rather than launched onto the route that was already being probed. The claim expires by itself after `cooldown_secs`, so a probe that never reports back cannot hold a route shut. A claim attempt that finds the route's own health record momentarily locked by another process is refused the same way rather than waiting on it — the lock holder may be halfway through claiming the very same trial. A trial that succeeds recovers the route and clears the claim; one that fails reopens the breaker and clears it too. An open route is excluded from `zirv agent`'s automatic reroute, from the orchestrator seat's successor choice, and from the pool view's candidate ranking — carrying its own reason, not a bare `hard-blocked`. An open breaker on the **seat's own** route is itself a rollover trigger, and takes the reactive path, so the handoff is the host-computed structural packet: zirv never asks a route that just refused connections to distil a handoff. That structural packet also reconciles a partial stream (issue #455): a tool call whose result never arrived by the end of the scanned transcript is listed as `UNRESOLVED` rather than silently dropped -- with any file it claimed to modify marked `(unconfirmed)` -- and a reply cut short by the failure is withheld from `Done` and reported as `PARTIAL` instead of standing in as a finished answer, so the successor checks the real outcome before repeating or assuming away the side effect. Codex's own rollout carries no per-turn "stream ended mid-generation" marker, so its cut-tail detection only covers a `task_complete` that itself reports an error, not a raw stream cut with no completion event at all. If every route is denied, the seat parks exactly as it does when every account is exhausted, and the park reason names the health failures. State lives in `/health/.json` — delete that file to reset a route by hand; there is no verb for it, and every phase heals on its own anyway. Each phase change is one `health-open` / `health-half-open` / `health-recovered` / `health-unavailable` line in the decision log, never one line per failure. `open_after_failures` must be 1..=20 (the size of the per-route observation ring), and both `window_secs` and `cooldown_secs` must be above zero; a value outside that is refused at config load rather than leaving a breaker that can never trip. The same applies to the degrade knobs: `degrade_error_rate_pct` must be 1..=100, `degrade_min_samples` at least 2, and `degrade_ttft_ms`, when set at all, at least 1000. Observations are stamped with the transcript **row's** own timestamp, not the clock, and de-duplicated by the row's own id — so re-reading a transcript from the start (a new supervisor, a rebuilt scoring checkpoint) can neither fold old failures into the current window nor count the same failure twice. That holds for every class, `unavailable` included: an authentication error older than `window_secs` is history and changes nothing, and healing a route clears its phase without forgetting which rows it has already counted. A row whose transcript states no timestamp at all is only ever counted when the poll that read it was reading genuinely new bytes. Both the interactive supervisor and the headless `exec`/`loop` supervisors feed the same records, each under the route's own lock, and a supervisor that finds the lock held skips the fold rather than waiting on another process. `zirv ctx status` shows a line per route that is not healthy (and nothing at all when every route is fine), plus the exclusion reason in the pool section: ``` pool health: claude open (3 transport/server error(s), last model opus; next health check ~unix 1700000300 (estimate)) health: codex degraded (2 transport/server error(s); codex: error rate 25% over 8 turns) excluded claude: claude: route health open: 3 transport error(s) in 10m; next health check in ~5m (estimate) ``` A half-open route names whoever holds its trial the same way (`half-open (0 transport/server error(s); trial in flight: 7f3a2b1c (12s))`). The retry time is always an *estimate*: it is when the breaker admits its next trial, not when the endpoint is known to be back. `zirv ctx status --json` carries the same rows under the pool view's `health` field, omitted entirely while every route is healthy. Configure it under `[fallback.health]`: ```toml [fallback.health] enabled = true # issue #455: master switch (also off whenever fallback.enabled is off) open_after_failures = 3 # transport/server failures inside the window that open a route window_secs = 600 # the sliding window failures are counted over cooldown_secs = 300 # how long an open route stays excluded before one trial degrade_error_rate_pct = 25 # rolling error rate that ranks a reachable route last degrade_min_samples = 8 # dated turn outcomes the window needs before either degrade signal fires # degrade_ttft_ms = 20000 # opt-in first-token p50 that degrades a route; unset by default ``` `--force` and an explicit pin bypass health exactly as they bypass headroom: health only ever adds exclusions, it never overrules a route you asked for outright. **`zirv ctx handover`** performs the swap directly, on demand, mid-session — the same mechanism the dashboard's `Ctrl+A o` picker and automatic fallback routing both use underneath: ```bash zirv ctx handover --agent codex --model standard zirv ctx handover --model deep # same harness, a different model tier zirv ctx handover --dry-run # print the resolved swap; change nothing ``` `--model` accepts a literal model id or a generic tier (`cheap`/`standard`/ `deep`), resolved per target harness — `claude`'s `deep` is `opus`, `codex`'s is `gpt-5.6-sol`, for example, each overridable via `[handover.]` in `~/.zirv/ctx.toml` or `ZIRV_CTX_HANDOVER__`. The swap carries a distilled handoff packet across so the successor session picks up task continuity; by default it waits for a verified-idle turn boundary, and `--force` swaps mid-turn instead. `handover` (and `handover..`) is repo-forbidden: swapping the orchestrator seat's harness or model picks which vendor account gets spent, so only the operator's own `~/.zirv/ctx.toml`, `ZIRV_CTX_HANDOVER_*`, or flags may set it. ### Reviewing your instruction files `zirv ctx optimize` reads the CLAUDE.md hierarchy and the settings layers that steer every session, checks them against recent transcripts and the decision log, and prints a report with proposed edits as unified diffs. ```bash zirv ctx optimize # full analysis, one cheap model call zirv ctx optimize --no-model # deterministic checks only, no model call ``` It reports four kinds of finding: instructions stated in more than one layer, instructions naming files or hook programs that no longer exist, contradictions between layers, and instruction gaps that correlate with repeated tool failures or user corrections. **It never edits an analysed file.** Every proposal is a diff you apply yourself. A finding about a file inside this repository (CLAUDE.md, `.claude/settings.json`) gets a repo-relative `a/`/`b/` header, so `git apply` works from the repo root. A finding about a file outside the repo (your global CLAUDE.md, `~/.claude/settings.json`) gets a plain absolute path with no `a/`/`b/` prefix instead, meant for hand application: the report says which is which for every diff it prints. A copy of each report is kept under the state dir, and each run appends to the decision log. When a finished session shows a high tool-failure rate, the Stop hook queues an "optimize recommended" entry and mentions it once in its advisory; it never runs the analysis itself. ### Memory bank Besides mail (session-to-session notes) and handoffs (task continuity across a restart), zirv keeps a third, longer-lived store per repository: a small bank of durable facts about the repository itself, independent of any one task or session. ```bash zirv ctx remember --key staging-db-creds --text "the staging DB creds live in 1Password under staging-db" zirv ctx recall zirv ctx forget staging-db-creds ``` See [`zirv memory`](#zirv-memory) above for a newer, scope-aware surface over this same bank (`status`/`list`/`recall `/`remember `/ `forget `/`verify `, `--shared` for the repository-owned scope below) that works without starting an AI session — the two verbs above keep working unchanged alongside it. **The handoff-vs-memory boundary matters.** A handoff is task continuity: what this specific task was doing, what remains, what to try next — written once per restart, and only ever useful to the session that picks that particular task back up. Memory is repository facts: things true regardless of which task is in progress — a build command, where a credential lives, a convention, a gotcha about how a dependency behaves. Task state does not belong in memory, and a repository fact does not belong in a handoff. By default nothing is added to the bank automatically — `zirv ctx remember` is a deliberate act, by a session or a human. Set `[memory] harvest = true` (or `ZIRV_CTX_MEMORY_HARVEST=true`) to let zirv *also* try to extract durable facts on its own: right after a rot restart distills a handoff (`exec` or `wrap`, and only from a genuinely distilled handoff, never the mechanical fallback), one extra cheap-model call looks at the handoff's `Gotchas learned` and `Files touched` sections and proposes zero or more `key: value` facts. Harvesting stays off by default because a cheap model can be confidently wrong: an unreviewed guess landing in a bank every future session reads is a worse failure mode than simply not harvesting. A harvested entry replaces any existing entry with the same key rather than duplicating it, the same as an explicit `remember` does, and a distiller failure or timeout leaves the bank untouched. Every entry has a `Verified` stamp alongside its `Written` one; an entry older than 30 days without being re-verified is flagged as stale wherever the bank is summarized (`zirv ctx status`, `zirv ctx optimize`). `zirv ctx optimize`'s report includes a memory-bank summary block — count, total bytes, oldest/newest age, how many are stale, a duplicate-key check — but **never quotes an entry's key or body**: that content is repository-scoped, cross-session data with nothing to do with what `optimize` is reviewing, and it is read separately from (never folded into) the surfaces sent to the judgment model. ```toml [memory] enabled = true harvest = false # off by default; see above max_entries = 50 max_entry_bytes = 512 max_injected_bytes = 2048 # superseded by core_max_bytes; kept only so an old config does not error shared_enabled = true core_max_bytes = 2048 # cap on the merged private+shared core layer folded into every session retrieval_max_bytes = 2048 # cap on `zirv memory recall`; reserved for future context-aware session retrieval retrieval_max_entries = 6 # max number of recalled entries, independent of bytes ``` There are two memory scopes, gated separately but not independently: `enabled` is a master switch that disables both scopes, and `shared_enabled` is a second, shared-only toggle underneath it -- `enabled = false` always wins, so an operator who turned memory off before the shared scope existed does not silently start receiving repo-controlled prompt content on upgrade. The **private** bank still lives under the state dir, never in the repo (the same "a checkout is not the operator" reasoning as handoffs and mail). The **shared** bank is the opposite by design: `/.zirv/memory/` is untrusted repository content, one key-addressed file per entry, meant to be committed, reviewed, and hand-edited like any other file in the checkout -- that is the whole point of a Git-friendly shared memory bank. What stays forbidden either way is the *configuration*, not the content: every `[memory]` key, including `shared_enabled`, is repo-forbidden outright -- a checkout may not set any of them to any value at all, so it can neither switch either scope's own gate on or off for itself, raise its own caps, nor turn on automatic harvesting. Only `~/.zirv/ctx.toml`, `ZIRV_CTX_*`, or flags (the operator) may. See [Trust boundary](#trust-boundary) above. ### Consistent sessions When zirv starts an agent through `wrap`, `exec`, `loop` or `resume` it injects a small system prompt so sessions behave the same way every time. Three layers concatenate, in order: 1. A shipped default baked into the binary: respect repo conventions, use tools deterministically, report failures honestly. 2. Your own additions, by session role: `~/.zirv/system-prompt.md` for an interactive session you drive yourself (`chat`, `wrap`, `resume`), or `~/.zirv/system-prompt.worker.md` for a delegated headless worker (`agent`, `exec`, `loop`). Both are optional, and neither role reads the other's file — if you had worker-facing instructions in `system-prompt.md`, copy them into `system-prompt.worker.md`. 3. `/.zirv/system-prompt.md`, the repository's additions (one file, both roles). The repo layer is **untrusted input**, treated the same way `ctx.toml`'s repo layer is: it is capped in size, labeled in the composed prompt as coming from the checkout, and stated not to override anything above it. A repository cannot turn its own layer on or raise its own cap: `prompt.enabled`, `prompt.repo_layer` and `prompt.max_repo_bytes` are all rejected from a repo config. Set them in `~/.zirv/ctx.toml`, or with `ZIRV_CTX_PROMPT`, `ZIRV_CTX_PROMPT_REPO` and `ZIRV_CTX_PROMPT_MAX_REPO_BYTES`. ```toml [prompt] enabled = true repo_layer = true max_repo_bytes = 4096 ``` `prompt.verbosity` (issue #427; `"minimal"` | `"standard"` | `"verbose"`, default `"verbose"`) picks how much of the Orchestrator-only meta-harness orientation layer (zirv's own explanation of itself, the checkpoint/mail habit, delegation mechanics, lifecycle sizing, the design-approval gate, the review policy) is injected. `"verbose"` is today's full text, byte for byte -- the default changes nothing for an operator who never touches this key. `"standard"` drops only the purely descriptive framing and self-discovery bullets; every bullet that changes what the session does is kept verbatim. `"minimal"` drops those two plus the roster pointer (the harness roster itself, if enabled, is still appended regardless of tier), and compresses everything else to its shortest behaviour-changing form: it still teaches delegation mechanics, that an undirected `zirv ctx send` is claimed by exactly one session while `--all` fans out, that durable facts persist via `zirv ctx remember`/`recall`, that substantial work starts a `zirv workflow`, the design-approval gate, and the review policy's hard stop after 2 fix rounds. It is `REPO_FORBIDDEN`: set it in `~/.zirv/ctx.toml` or with `ZIRV_CTX_PROMPT_VERBOSITY`. ```toml [prompt] verbosity = "verbose" ``` Pass `--simple` to any of the four verbs to start the agent with no zirv text at all, shipped default included. Supervision, pacing and hooks are unaffected. Whether a prompt was injected, and from which layers, is recorded in the decision log at every session start. ### Low-noise interactive, fail-closed unattended "Headless" here is a **launch mode** — whether a human is present to answer a permission prompt — not a spawn topology. `zirv chat`, `zirv ctx wrap` and a pane a human spawned from the dashboard's own overlay are *interactive*; `zirv ctx exec`, `zirv ctx loop`, `zirv agent` and any pane spawned by a request nobody vouched for are *headless*, however visible they are on screen. The permission rule is simple: **everyday and unknown commands run silently in an interactive session; a short list of genuinely dangerous commands prompts; a shorter irreversible, credential-exfiltrating, or zirv-self-destructive list is refused outright. Headless launches stay fail-closed because nobody is present to answer.** This is a launch-flag/hook layer, not injected instruction text, so `--simple` does not remove it—only `--no-supervise` (pure passthrough) or the explicit opt-out below do. - **Claude interactive:** `--permission-mode default`, native workspace/tool scoping, and a `Bash|PowerShell` `PreToolUse` safety hook attested on every Zirv launch through fingerprinted settings and immutable policy snapshots under `~/.zirv/runtime/` as the sole per-command gate. The hook evaluates both the launch snapshot and the policy resolved now, keeps the stricter verdict, and fails closed on a missing or tampered attestation. It emits an explicit `allow` for everyday and unclassified commands, `ask` only for the closed dangerous list, and `deny` for the shorter refusal list. Zirv ships conservative Design B: no blanket native `Bash(*)` allow. On macOS, Linux, and WSL2 the same launch layer requests Claude's OS sandbox in auto-allow mode to block common credential paths, and scrubs cloud credentials from subprocesses. On Linux the OS sandbox requires bubblewrap (`bwrap`) and `socat`; install them with `sudo apt install bubblewrap socat` on Debian/Ubuntu to enable it. When they are missing, Zirv still launches Claude Code, which prints "Sandbox disabled" and runs Bash without OS sandboxing; Zirv's permission mode, allowed/disallowed tools and `zirv ctx safety check` PreToolUse hook remain in force. macOS uses the built-in `sandbox-exec`. Native Windows has no OS sandbox and receives the hook and credential rules. The interactive `Edit(./**)`/ `Read(./**)` scope also covers Claude Code's own agent-worktree convention (`.claude/worktrees/**`) and any `--add-dir` grant this launch passes (a linked git worktree of the launch repo, issue #329), so a native subagent's Edit/Write inside a delegation worktree never falls through to a prompt. - **Claude headless:** `--permission-mode dontAsk`; ordinary allow rules are pre-approved and both deny and ask rules are disallowed, so no prompt can stall automation. - **`chat.claude_permission_mode`** (issue #504, operator-only, see [Trust boundary](#trust-boundary)): overrides the INTERACTIVE launch's own `--permission-mode` to `"acceptEdits"` or `"bypassPermissions"` instead of the shipped `"default"`. Claude Code's own `--permission-mode` flag outranks `permissions.defaultMode` in the operator's `~/.claude/ settings.json`, so before this key existed there was no way to quiet the interactive prompt volume from config at all — an operator running several native subagents delegating into worktrees got prompted for every Edit/Write and every unlisted compound command, with `bypassPermissions` reachable only via the live `/permissions` slash command each session. Headless is untouched either way, and the mode change never suppresses or widens the `--allowedTools`/`--disallowedTools` lists above — only the flag itself changes: ```toml [chat] claude_permission_mode = "acceptEdits" # or "bypassPermissions" ``` or `ZIRV_CTX_CHAT_CLAUDE_PERMISSION_MODE=acceptEdits`. Unset (the default) reproduces `"default"` exactly. `REPO_FORBIDDEN`: a repository checkout must not be able to widen its own session's permission posture. - **Codex interactive:** `--sandbox workspace-write --ask-for-approval on-request` when the installed CLI's own bounded capability probe documents it, otherwise `never`. When that CLI also advertises `--approve-for-me`, Zirv enables Codex's native security reviewer for boundary requests; older versions retain plain `on-request`. No zirv `[safety]` rule is projected per command. - **Codex headless:** `--sandbox workspace-write --ask-for-approval never`. `adapters::SHIPPED_POSTURE_ALLOW`/`_ASK`/`_DENY` are the shared source for the built-in classifier and Claude projection. Plain `curl`/`wget`, dependency installation, builds, commits, in-repo writes, read utilities, and commands zirv has never seen are not prompt-worthy merely because they mutate or are unknown. Force-push, hard reset, local ref/stash/reflog/worktree loss, recursive deletion, process termination, registry mutation, remote HTTP mutations, infrastructure destruction and device/partition tools ask interactively. Generated-directory cleanup, downloads, loopback requests and dry runs stay silent. Irreversible package or release publication/deletion, credential-store access, secret-file uploads, privilege escalation, download-to-shell pipelines, and attacks on zirv itself are denied. Local reads or copies of project secret files (`.env`, `.env.*`, `*.pem`, `*.key`) ask; `.env.example`, `.env.sample`, `.env.template`, `.env.dist`, and `.env.test` templates stay silent. The shipped allow set also covers `zirv report bug|feature`, absolute-path `find`, ordinary `git merge`/`pull`/`push`/`branch`, and the macOS SSH-agent setup `export SSH_AUTH_SOCK=$(launchctl getenv SSH_AUTH_SOCK)`. Only that specific `launchctl` environment key is allowed. The matching unsandboxed-retry rules keep those commands silent when the OS sandbox cannot reach the required repository, remote, user path, or agent socket. Destructive variants such as force/delete pushes, hard resets, and forced branch deletion still use the ask rules above. The structural/semantic result is identical for Unix, `cmd.exe`, and PowerShell spellings (including `.exe`/`.cmd` wrappers). This classifier is a tripwire layered with the harness sandbox, not a claim that finite command analysis can contain an arbitrary-code interpreter. An operator's own explicit `--sandbox`/`--ask-for-approval`/`--permission-mode`/ `--disallowedTools` (passed after `--`, or via `worker.claude`/`worker.codex`'s own trailing flags) always wins outright — zirv prepends nothing when the launch already pins one of these. To restore the pre-2026-08-22 behaviour (no zirv-applied flags at all; a launch's approval/sandbox posture comes entirely from the harness's own native config), set: ```toml [sandbox] enabled = false ``` or `ZIRV_CTX_SANDBOX=false`. `sandbox.enabled` is `REPO_FORBIDDEN` (see [Trust boundary](#trust-boundary) above): a checkout cannot turn its own sandboxing off, only the operator can. ### Command safety policy (issue #83) `[safety]` is zirv's harness-neutral shell-command classifier. Claude projects it through its native rule lists and `Bash|PowerShell` hook; codex currently has no verified per-command channel and relies on its sandbox/approval boundary instead. `gh` and `glab` (GitHub's and GitLab's CLIs) are both subcommand-based CLIs with the same ` ` shape; this classifier's own read-only carve-out recognizes a mutating `gh api` call specifically (a non-`GET` `--method`/`-X`, or a body flag such as `-f`/`-F`/`--input`) and classifies it the same way a destructive git or publish command is. `zirv ctx permissions`' separate classifier checks the equivalent mutating-vs-read shape for both `gh api` and `glab api`, using its own flag list (`-f`/`--field`/`--input`) — see [Permission auditing and safe-list proposals](#permission-auditing-and-safe-list-proposals-issue-178) below for turning repeated prompts on these two CLIs into either a standing operator allow or a proposed policy change: ```toml [safety] deny = ["terraform destroy*"] # additional deny patterns ask = ["kubectl delete*"] # additional ask patterns allow = ["just test*"] # operator-only, REPO-FORBIDDEN default = "ask" # operator-only, REPO-FORBIDDEN interactive_default = "allow" # operator-only, REPO-FORBIDDEN sql = "on" # operator-only, REPO-FORBIDDEN ``` Rules are glob patterns (`*` matches any run of characters); a command is matched deny-first, then ask, then allow, first match wins within a category. Unmatched commands use `interactive_default` for an interactive launch and `default` for a headless one. With SQL classification on, one provably read-only `SELECT`/`EXPLAIN`/`SHOW` through a recognized client runs silently; write-shaped, multi-statement, stdin/script-fed, malformed, or CTE input asks conservatively. With the default `interactive_default = "allow"`, the hook answers "allow" for commands no rule matches and suppresses the harness's own permission prompt, so enabling the hook widens what runs without a prompt; set `[safety] interactive_default = "ask"` in `~/.zirv/ctx.toml` to keep the harness's prompt for unmatched commands. The analyzer evaluates the most restrictive result across quote-aware compound segments (`;`, `&`, `&&`, `||`, pipes and newlines), nested `sh`/`bash`/`zsh`/`cmd`/PowerShell inline wrappers, `$()` and backtick command substitutions, and every semantic candidate it finds. The semantic layer recognizes SQL writes, remote network mutations, credential-file access, recursive deletion, infrastructure/service destruction, and irreversible package/release operations using case-folded executable basenames, so native Windows and Unix wrapper spellings reach the same verdict. Quoted command-looking text remains data. This tripwire is bounded against hostile input and deliberately does not decode obfuscation, expand variables, or read dynamically sourced scripts; the harness sandbox is the containment boundary beneath it. Each supervised Claude decision appends a privacy-preserving audit record under the platform state directory's `logs/safety-decisions/` UTC-day bucket. Records contain the verdict, a bounded command family (program plus a subcommand only for known dispatcher CLIs), matched rule/origin, launch/current policy fingerprints, attestation status and SHA-256 of the command—never the raw command, remaining arguments, source, paths, tokens, or shell secrets. `deny`/`ask` may be extended by a repo checkout (narrowing is always safe—both are checked before `allow`); `allow`, both defaults, and `sql` may not. A checkout cannot grant itself approval, loosen either launch posture, or turn off the conservative SQL narrowing. Three verbs work with the resolved policy directly: ```sh zirv ctx safety check --mode interactive -- rm -rf / # exits 0/1/2 for allow/ask/deny zirv ctx safety list # the effective merged policy, with each rule's origin zirv ctx safety explain --mode headless -- git push --force # rule plus launch consequence ``` `zirv ctx safety check` (with no trailing command) is also what `zirv setup apply` wires into claude's `PreToolUse` hook for `Bash` and `PowerShell` calls, so the same evaluator zirv's own CLI uses is what claude consults before running a command — see [Context Management](#context-management-zirv-ctx). ### Permission auditing and safe-list proposals (issue #178) `zirv ctx permissions` turns recent transcripts into a report on which commands kept needing a human, so a policy decision can be made about the *family* of command instead of clicking through the same prompt every session: ```bash zirv ctx permissions audit --agent codex --sessions 5 # read-only report zirv ctx permissions audit --agent claude --json zirv ctx permissions compile --agent codex --dry-run # preview eligible allows; writes nothing zirv ctx permissions propose --agent claude --dry-run # preview proposed issues; files/comments nothing ``` - **`audit`** is strictly read-only. It extracts every escalated/denied permission request from the sampled transcripts (codex: `require_escalated` exec requests; claude: headless `dontAsk` denials and interactive sandbox-escape asks), groups them by a normalized command family (`gh pr`, `cargo publish`, ...), and reports each group's sample command, cause, and a reusability verdict: whether a saved approval for this family would plausibly match the *next* equivalent invocation, or whether it collapses to a one-off (a long literal payload, or a pipe into `jq`/`grep`/`awk`/`sed` whose own argument is what varies). - **`compile`** runs the same audit, then *writes* eligible families as standing `[safety] allow` entries in the operator's own `~/.zirv/ctx.toml`. A family is eligible only when it is reusable, has at least two normalized tokens (a bare program name is too coarse — it would authorize whatever a future invocation is told to run), and is not a **protected** family: destructive git (`git push --force`, `git reset --hard`, `git rebase`, ...), a global binary/config install, a credential/secret command, a publish/release action, an interpreter/shell/remote-exec program, or a mutating `gh api`/`glab api` call always stay prompting regardless of reusability. `--agent codex` compiles are still written as real `[safety]` entries — claude reads them too — but a printed caveat makes clear this changes nothing about codex's own launch posture, since codex has no per-command approval hook yet for zirv to pin against. - **`propose`** is the mirror image: instead of escalated/denied requests, it looks at operator-**approved** prompts and classifies which are so clearly safe they should never have prompted at all — today, only a documented `gh`/`glab` collaboration verb (`SAFE_COLLABORATION_VERBS`: creating or updating a PR/issue, or commenting — never merge, close/reopen, delete, release, auth, or an arbitrary API call), matched at the exact `(program, resource, verb)` triple, unchained and unpiped. Evidence is grouped by family; a family with no open proposal issue yet files one, and a family whose evidence has changed since the last run gets one updated comment on its existing issue — a family already reported with unchanged evidence is skipped, so a re-run over an overlapping transcript window never re-comments the same evidence. `propose` is **disabled by default** — it auto-files issues on a public GitHub repository. Enable it explicitly, operator-side only: ```toml # ~/.zirv/.settings.toml [permissions] propose_enabled = true ``` Above a threshold of 5 total requests in one audit, `zirv ctx optimize`'s own friction pass surfaces the same summary as a finding, well before a session reaches the volume that originally motivated this feature. ## Supported harnesses and models zirv ships thirteen harness adapters — `claude`, `codex`, `gemini`, `qwen`, `opencode`, `pi`, `copilot`, `droid`, `grok`, `kimi`, `cursor-agent`, `goose`, and `muse` — each enabled or disabled per repo in [.settings.toml](#settingstoml). The model list below is what zirv *recognises*: it drives tier translation, pricing, review-model escalation, and context-window lookups. Each harness's own CLI still decides what actually launches — a model zirv does not recognise is passed through verbatim but gets no tier translation, since zirv never guesses. ### Models per harness | Harness | Models | Billing/notes | | --- | --- | --- | | `claude` | Anthropic ladder (see [Model catalogue](#model-catalogue)) | single vendor | | `codex` | OpenAI ladder | single vendor | | `gemini` | Google ladder | single vendor | | `qwen` | Qwen ladder | single vendor | | `opencode` | any catalogue vendor below | model is pinned as `provider/model` (e.g. `anthropic/claude-opus-5`); zirv resolves the vendor from that prefix and id; an unrecognised model falls back to the static `opencode` slug | | `pi` | any catalogue vendor below | model is pinned with `--model provider/id`; zirv resolves the vendor from that prefix and id; an unrecognised model falls back to `pi` | | `copilot` | any catalogue vendor below (ladder lookups only) | tier, strength and context window are looked up per model, but billing is always the operator's GitHub Copilot subscription (`github`), regardless of which model answered | | `droid` | any catalogue vendor below | bare model ids (no `provider/` prefix) resolve directly against the catalogue; `custom:` BYOK ids never match a vendor and fall back to the `factory` slug, with no tier translation | | `grok` | xAI ladder | single vendor; no verified row-level transcript schema, so rot-scoring reports no data (issue #390) | | `kimi` | Moonshot ladder | single vendor; no verified row-level transcript schema, so rot-scoring reports no data (issue #391) | | `cursor-agent` | none (proprietary) | no catalogue vendor and no verified headless model flag; no verified row-level transcript schema, so rot-scoring reports no data (issue #392) | | `goose` | any catalogue vendor below | model is paired with a `--provider` flag derived from the model's own vendor prefix; an unrecognised model falls back to the static `goose` slug; transcript is a single shared SQLite database whose schema is unverified, so rot-scoring reports no data (issue #393) | | `muse` | Meta ladder | single vendor; macOS/Linux only; even the transcript location is unverified, so rot-scoring reports no data (issue #394) | ### Model catalogue Every vendor's rungs, strongest first. Anthropic is the only vendor whose alias differs from its model id; every other vendor's alias and id are the same string, so the id is shown only where it differs. | Vendor | Model | Tier | | --- | --- | --- | | anthropic | `fable` (`claude-fable-5-1`) | — | | anthropic | `mythos` (`claude-mythos-5`) | — | | anthropic | `opus` (`claude-opus-5`) | Deep | | anthropic | `sonnet` (`claude-sonnet-5`) | Standard | | anthropic | `haiku` (`claude-haiku-5`) | Cheap | | openai | `gpt-6-astra` | — | | openai | `gpt-5.6-sol` | Deep | | openai | `gpt-5.6-terra` | Standard | | openai | `gpt-5.6-luna` | — | | openai | `gpt-5.4-mini` | Cheap | | google | `gemini-3.1-pro-preview` | Deep | | google | `gemini-3.7-flash` | Standard | | google | `gemini-3.5-flash-lite` | Cheap | | xai | `grok-4.6` | Deep | | xai | `grok-4.3` | Standard | | xai | `grok-build-0.1` | Cheap | | qwen | `qwen3.8-max` | Deep | | qwen | `qwen3-coder-plus` | Standard | | qwen | `qwen3.8-flash` | Cheap | | moonshot | `kimi-k3` | Deep | | moonshot | `kimi-k2.7-code` | Standard | | moonshot | `kimi-k2.6` | Cheap | | mistral | `devstral-2` | Deep | | mistral | `mistral-medium-3.5` | Standard | | mistral | `devstral-small-2` | Cheap | | deepseek | `deepseek-v4-pro` | Deep | | deepseek | `deepseek-v4-flash` | Cheap | | zhipu | `glm-5.3` | Deep | | zhipu | `glm-4.6` | Standard | | zhipu | `glm-4.7-flash` | Cheap | | minimax | `minimax-m2.7` | Standard | | meta | `muse-spark-1.2` | Standard | | meta | `llama-4-maverick` | Cheap | | amazon | `nova-premier` | Deep | | amazon | `nova-pro` | Standard | | amazon | `nova-lite` | Cheap | `deepseek` has no `Standard` rung, `minimax` has only its one `Standard` rung, and `meta` has no `Deep` rung — not every vendor fills all three tiers. Beyond the ladder, zirv also recognises `claude-fable-5` and the `[1m]` long-context variants `claude-fable-5[1m]`, `claude-fable-5-1[1m]`, `claude-mythos-5[1m]` and `claude-opus-5[1m]` on Anthropic, and `gpt-5-codex` on OpenAI; none of these six ids is a ladder rung in its own right. `gpt-6-astra` sits at the top of the OpenAI ladder alongside `gpt-5.6-sol`, and like `fable`/`mythos` carries no tier of its own but classifies as deep for cross-harness fallback. The catalogue for every vendor other than anthropic/openai is a survey dated 2026-09-07. See [Cross-harness fallback and handover](#cross-harness-fallback-and-handover) for how these tiers drive rerouting between harnesses. ## Supported Platforms - Windows (see the platform note under [Context Management](#context-management-zirv-ctx): `zirv ctx` supervision is unix only) - macOS - Linux Commands can target specific operating systems using the `operating_system` option in the script configuration. - `windows`: Windows OS - `linux`: Linux OS - `macos`: macOS ## Contribution Contributions are welcome! Please fork the repository and submit a pull request with your changes. For major changes, please open an issue first to discuss what you would like to change. ## License Licensed under the [MIT License](LICENSE). See [DISCLAIMER.md](DISCLAIMER.md) for autonomous supervision, binary integrity, and third-party harness notes. ## Contact Tweet [@Glubiz](https://twitter.com/Glubiz)