DocsSupervision (zirv ctx)
Harness proxy
The harness proxy is an opt-in intake decision made once, before a session
launches: from the request text alone it decides intent, complexity, risk,
execution mode (Direct/Bounded/Orchestrated), a workflow to start (or none),
the orchestrator harness+model, and the worker tier, then hands that decision
to zirv chat. It implements part of the execution-profile seam tracked in
issue #537; the deterministic
classifier, team compiler, workflow definitions and gates it sits in front of
are unchanged.
Decider chain. decide() computes the deterministic baseline first. With
decider = "typesafe" and a credential present, one metadata-only Jev call
(coarse counts, classes and flags; never request text, repository names or
paths) may advise clarification and its category; with no Jev credential the
helper-model chokepoint runs, then the baseline. The
baseline measures no diff at intake, so it floors complexity by the request's
own size: 120+ words or 3+ enumerated items is at least bounded, 300+ words
or 8+ items at least substantial (never architectural) -- without it every
multi-part spec landed on the cheap seat. The eleven-question classification
schema and live battery below describe the helper's merge behavior. The historical Jev path
and current helper model answer eleven
questions — intent, complexity, risk, workflow, needs_clarification,
and six additive domain tags (security, data, docs_only, devops,
architecture, frontend) — never execution, seat tier or worker tier
directly: a live battery found those unreliable, and any many-option seat/
tier question never cleared the confidence floor. A field whose model answer
is not decisive keeps the baseline value instead, with a recorded reason,
except complexity/risk when it is the margin alone that fell short:
these resolve to the higher of the two most probable levels, and that level
is taken only when it is above the baseline. This keeps a near-tie
deterministic whichever level wins, and avoids the 2026-09-21 case, when an
exhaustive multi-system investigation landed on a single haiku seat because
a bounded/substantial near-tie (confidence 0.57, margin 0.14) fell to the
text-only baseline. A confidence below min_confidence still keeps the
baseline: that is the model having no opinion rather than a tie between two
candidates, and escalating it over-sized the live battery's bump-timeout
and ambiguous cases from trivial/direct/cheap into bounded work on a
standard seat. The recorded reason names what actually happened — resolved upward to <label> or kept baseline. A model answer is decisive when BOTH
its confidence is at or above min_confidence AND its
margin (the gap between its top and runner-up probability; for a yes/no
question, distance from the maximally uncertain 0.5, doubled) is at or above
min_margin. A completed 2026-09-18 measurement (497 live calls across the
intake battery) found flipped intent/workflow/architecture answers
topped out at margin 0.14, while their own stable answers sat at 0.17 or
higher — confidence alone missed this, since a flipped answer's confidence
was not noticeably lower than a stable one's. The gate is a determinism
tool, not an accuracy tool: one stable complexity answer for
perf-investigation sits at margin 0.18-0.24 and is simply wrong, and one
genuinely ambiguous prompt still flipped at margin 0.54 — the same request
body giving the same answer twice is the property this floor buys, not
correctness. The shared Jev client backs that further with its own decision
cache: an identical request body (hashed with SHA-256) served twice within
[jev] cache_ttl_secs (default a week, 0 disables it) returns the exact
same stored answer instead of asking Jev again, at zero additional cost.
complexity and
risk are always merged with max(model, baseline) — a monotonic floor, so
a model decision can raise them but never lower them — and validation is
recomputed from the merged complexity and risk, so a raise always
propagates. execution, seat tier, worker tier and seat role then derive
entirely from that merged complexity: Trivial → direct/standard/single seat,
Bounded → bounded/standard/single seat, Substantial or Architectural →
orchestrated/orchestrator seat — floored upward when risk reaches
High (at least Bounded) or the request names a security surface, and a
Direct execution always clears any chosen workflow back to none. execution
alone never routes to the cheap seat tier any more (cheap-seat overhead fix,
wrapper-overhead benchmark 2026-09-24): a live 85-run replay found Direct
sending 16/28 runs to haiku, which then took 2-5x the turns of a sonnet run
on the same code task for +133% wall time and no cost saving, so Direct
now maps to the same Standard seat tier Bounded already used; worker_tier
is unaffected. An orchestrated seat runs the frontier tier only for
Architectural complexity or High-or-worse risk; a Substantial one stays on
the standard tier. The baseline's own deterministic pick of a workflow to
start (selection::select_definition) fires only at Substantial or
Architectural complexity — the same 2026-09-24 benchmark found intake
starting a workflow for every Bounded-complexity headless run even though
headless agents never read it, adding ~11s/run; an explicit "start a
<name> workflow" request still starts one at any complexity, exactly as
before. When a workflow starts, the proxy layer names its id so the seat can
run zirv workflow status and follow the current step. The
winning decision is then validated against the live roster (an unready
harness falls back to the baseline harness, its model re-derived for the
decision's own seat tier; an unknown workflow id falls back to the baseline)
before it is applied. The committed tests/fixtures/proxy/jev-battery.json
documents the expected ruling (execution, complexity, workflow, seat tier)
per request class, verified against the real API; TYPESAFE_API_KEY=... cargo nextest run --run-ignored only jev_live_battery replays it against Jev
directly (it is #[ignore]d by default, like every other live provider
contract), running each case TWICE and asserting the two merged decisions are
identical to each other as well as to the recorded ruling — a flip between
the two runs is reported as an instability, distinct from an outright
mismatch.
Domain tags. A substring keyword match (the deterministic classifier's
own security-domain detection) misses phrasing that never uses one of its
fixed keywords — "rotate the shared token" names none of security/auth/
permission/credential/secret — so a model decider is also asked
directly, one yes/no question per tag. A true (>= 0.5) answer that also
clears the margin floor (see "Decider chain" above; a yes/no question has no
separate confidence to check, so only its margin gates it) adds that tag to
decision.domains; tags only ever accumulate, and a confident security tag
applies the same risk/execution floor the keyword trigger already does (risk
at least High, execution at least Bounded). Shown
in zirv ctx proxy's human/--json output, the proxy: announce line, and
the [zirv proxy] prompt layer as domains: security, data — omitted
everywhere when empty.
Clarification. needs_clarification always keeps the model's raw
yes/no reading, but a consumer only acts on it when it is ALSO decisive at
min_margin (a confident-looking but thin-margin "ambiguous" reading must
not interrupt a launch on its own). When the winning decision's
needs_clarification is at or above 0.5 and decisive, the intake view (see
"The intake plan card" below) shows one reworded question in a box
(Which part should change? Name a file, a module or a screen. for the
target category, and similarly for behavior/constraint/generic) —
⏎ answers it, Tab skips and plans anyway. An empty or skipped answer
leaves the decision as is; a non-empty one is appended to the request
(separated by a blank line) and decide() runs exactly once more — never a
second round, however ambiguous the new decision still looks. zirv ctx proxy itself never prompts; it keeps printing needs_clarification as a
plain field, same as every other value. A launch that never got the chance to
ask (a dashboard pane, a resumed session) still carries a clarify: ask the user one precise question before acting line in its [zirv proxy] prompt
layer when the same threshold-and-decisive condition holds. The Typesafe
metadata intake asks only when it can also name what is missing: without a
decisive target/behavior/constraint category (confidence at least 0.7),
the decision is marked not decisive and nothing asks.
When it takes over. Bare zirv and zirv chat open the proxy's intake
view first only when [proxy] enabled = true, the configured decider has a
usable model (typesafe needs a non-empty model and the environment
variable named by credential_env set; helper needs a resolvable default
adapter; deterministic never takes over), and the terminal can actually
render it (an interactive stdin, VT output enabled, and stderr itself a
terminal — the region is inline ratatui, drawn on the plain screen before the
harness starts). Otherwise the launch proceeds exactly as it does today — the
full orchestrator harness, keeping whatever task text was already typed —
after one zirv ▸ advisory line carrying the reason, for example proxy: enabled but TYPESAFE_API_KEY is unset; starting the orchestrator harness or
proxy: this terminal cannot render the intake view; starting the orchestrator harness.
The intake plan card#
When the proxy is active, zirv chat/bare zirv opens a short inline view
— on the normal screen, before the harness's own full-screen start, so it
survives that switch (issue #701) — instead of the harness starting
immediately:
-
A boxed prompt (
⏎ plan and start · shift+⏎ new line · esc start without a plan) reads the task description;Alt+⏎/Ctrl+Jalso insert a newline. Long lines soft-wrap inside the box, which grows with the content (capped at the inline region's own height, scrolling to keep the cursor's own line visible once content exceeds that). While the box is empty the hint instead reads⏎/esc start the full orchestrator · shift+⏎ new line— Enter on nothing typed starts the harness the same way Esc always has. -
✻ Sizing the task… Ns · esc to start without a plan— a spinner whiledecide()runs on a worker thread; Esc or Ctrl+C abandons it (the thread's result is then just discarded) and starts the harness without a plan, keeping whatever was typed. -
The clarify box above, only when the decision asks.
-
A plan card in plain words — no seat tiers, decider names, domain scores or confidence numbers, ever;
zirv ctx proxy --jsonstill has those:╭─ Plan ───────────────────────────────────────────────────────────────╮ │ A bounded bug fix. │ │ │ │ Model Sonnet 5, working alone │ │ Workflow bugfix · 7 steps, starting at reproduce │ │ Why one area, low risk │ │ │ │ ❯ 1. Start │ │ 2. Start with a different model │ │ 3. Start without a workflow │ │ 4. Edit the task │ ╰──────────────────────────────────────────────────────────────────────╯ ↑↓ choose · ⏎ confirm · esc start without a planAn orchestrated decision shows
Lead/Helpersinstead ofModel.↑↓/digits1-4choose,⏎confirms — every plan waits for Enter, no countdown. Choice 2 opens a short list of the models every enabled-and- ready harness's own tier ladder offers (cheap through frontier, minus the already-planned seat) — the harness can change too, not only the model; picking one replaces the whole seat. Omitted (and the rest renumbered) when there is nothing else to offer. Choice 3 starts no workflow; choice 4 returns to the prompt with the typed text kept. Esc or Ctrl+C at any point — the prompt, the clarify box, the plan card, the model list — starts the harness without a plan. -
The decided workflow starts only once choice 1 or 2 is confirmed (choice 3 starts none) — not, as before, ahead of the operator ever seeing a plan.
-
When the configured decider failed or timed out, a yellow
⚠line above the card names it in plain words (Typesafe did not answer within 10s. The plan below uses local rules.), withzirv ctx proxy --jsonfor details. -
Confirming clears the region and leaves exactly one line in scrollback, e.g.:
✻ zirv planned in 3s · Sonnet 5 alone · bugfix workflow w-3f2a
What zirv chat does with it. The decided harness and --model replace
the resolved adapter and cfg.chat.model before launch (a picked model from
choice 2 replaces orchestrator.model the same way); the decision's workflow
starts with the request text as the first prompt when the repo has no
workflow already active and the plan was confirmed with choice 1 or 2.
Single seat vs. orchestrator seat. A Direct or Bounded decision launches
the harness as a single seat (PromptRole::Single): it gets none of the
orchestrator's own conventions — no HARNESS_PROMPT, no derived harness
roster, no "delegate everything" coaching — and never reads the operator's
orchestrator system-prompt.md; editing repository files directly is
allowed (only an orchestrator seat is technically blocked from that), and
the seat gets its own optional ~/.zirv/system-prompt.single.md
(SINGLE_PROMPT_FILE) layer instead, if one exists. An Orchestrated decision
keeps today's orchestrator seat and every one of its existing conventions
unchanged.
zirv ctx proxy [--json] [--headless] [REQUEST] decides and prints
without launching anything. REQUEST is read from stdin when omitted and
stdin is not a tty; human output is the announce line plus one line per
field with its source and confidence, then reasons and fallbacks; --json
prints the full decision. zirv ctx chat --proxy / --no-proxy overrides
cfg.proxy.enabled for one launch; --resume and --simple always skip the
proxy.
Operator overrides. zirv ctx chat and zirv ctx proxy accept
--execution direct|bounded|orchestrated, --tier cheap|standard|deep|frontier
and --validation review,test,security (or none) to override the derived
profile for that one launch. They are flags only, never config keys, so a
checkout cannot set them. Raising is always honoured; lowering stops at the hard
floor (a high-risk or security request cannot go below bounded, cannot drop
the review or security gate, and cannot lower the tier below its derived value),
and each refusal is named in the decision's reasons. A headless launch still
ends on a single seat. The applied override is recorded as operator_override
on the proxy-decisions.jsonl row.
Single-seat validation. A single seat has no reviewer or tester seat, so when
its profile asks for review, test or security validation the [zirv proxy]
layer carries one validate: line telling it to run the targeted tests, re-read
its own diff and flag security assumptions before reporting done. This is
advice only; no hook blocks completion. Bounded bug fixes and small features
stay single-seat; workflow.auto_start = detect can still start a workflow for
them.
Per-seat profile store. The final decision (with its override and started
workflow) is also written to <state>/proxy-profile/<seat short id>.json. A
rollover or handover successor keeps the seat's short id, so the successor's
handoff prompt re-renders the same [zirv proxy] layer from that file
(unless its base prompt already carries one). A missing, corrupt or old-format
file is skipped.
zirv ctx proxy stats [--json] is read-only. It groups the stored
profiles by intent/complexity and reports, in tokens only (no dollars), the
median seat input, output and cache tokens from the seat transcript, the
tokens of zirv agent delegations whose parent is that seat, and the
verified-pass rate of the last attributable build/test run in the seat
transcript. A class with no verification record prints quality as unavailable.
Headless single seat. A headless launch works unattended — nobody is
watching the seat, so it must never be told it is an orchestrator: zirv's own
rule for a worker is "runs unattended and must not delegate further". Pass
--headless, or leave it unset and let zirv ctx proxy fall back to reading
ZIRV_CTX_HEADLESS=1 (adapters::HEADLESS_ENV, the same marker zirv itself
sets on a headless launch's own process environment) from the calling
process's environment — either is enough. Either one forces execution/
seat_role to their single-seat equivalents (Orchestrated/Orchestrator
downgrades to Bounded/Single; a decision already Direct/Bounded is a
no-op) for BOTH the deterministic decider and a Jev/helper-assisted decision
alike — applied last, after everything else has run. complexity, risk,
seat_tier, worker_tier, and workflow are left exactly as the rest of
the pipeline decided; only which seat executes the request changes. The
rendered [zirv proxy] prompt layer then carries the same "You are the
single seat for this request: do the work here yourself; do not delegate."
line an interactive single-seat decision already gets. An interactive launch
(zirv chat, a dashboard pane) never passes this and is unaffected. An
external harness that launches zirv headlessly (a benchmark, CI, or any
caller outside a session) should always pass --headless.
Disabled by default:
# ~/.zirv/ctx.toml
[proxy]
enabled = false # ZIRV_CTX_PROXY_ENABLED
decider = "typesafe" # typesafe | helper | deterministic; ZIRV_CTX_PROXY_DECIDER
min_confidence = 0.5 # ZIRV_CTX_PROXY_MIN_CONFIDENCE
min_margin = 0.2 # ZIRV_CTX_PROXY_MIN_MARGIN -- see "Decider chain" above
request_max_bytes = 16384 # ZIRV_CTX_PROXY_REQUEST_MAX_BYTES
validation_gate = false # ZIRV_CTX_PROXY_VALIDATION_GATE
[proxy.typesafe]
base_url = "https://api.typesafe.ai/v1" # ZIRV_CTX_PROXY_TYPESAFE_BASE_URL
credential_env = "TYPESAFE_API_KEY" # ZIRV_CTX_PROXY_TYPESAFE_CREDENTIAL_ENV
model = "jev-1.13.0" # ZIRV_CTX_PROXY_TYPESAFE_MODEL -- pinned; see below
timeout_secs = 10 # ZIRV_CTX_PROXY_TYPESAFE_TIMEOUT_SECSValidation gate. validation_gate = true makes the Stop hook block (instead of only advising) a seat whose stored proxy profile requires tests (validation.independent_test, the condition behind the single seat's validate: line) while code changed and verification evidence is stale. It reuses the [verify_on_stop] nudge, so it needs verify_on_stop.enabled and stops blocking after max_nudges; it never blocks a Stop that is already a continuation. Operator-only: a repo layer that sets it is a hard error.
Pinned model. model defaults to a specific Jev release (jev-1.13.0,
what jev-latest itself resolves to today) rather than the jev-latest
moving alias: the pin is for reproducibility across FUTURE alias moves, not
because today's alias is wrong -- an alias that can change underneath a
deployed config would confound Jev's own answer-to-answer instability (see
"Decider chain" above) with an actual model upgrade, making either one
impossible to diagnose from the outside. Set model = "jev-latest"
explicitly to opt back into automatic upgrades, or to a newer pinned version
once you've verified it against tests/fixtures/proxy/jev-battery.json.
Privacy. The state sent to a model decider carries the request text
(truncated to request_max_bytes) plus the repository name and the
registered workflow ids/descriptions — never file contents, secrets, or any
live-measured repository fact (uncommitted/branch changes, the active
workflow, primary file extensions all used to ride along here too; stripping
them changed no answer's accuracy in the 2026-09-18 replay, and it means the
request body now depends only on the request text, the workflow registry and
the policy, never on anything that can drift between two calls for the same
request). The harness/model catalogue (names, readiness, headroom, prices)
used to ride along as well; it was dropped for the same reason (no question
ever read it, and TypeSafe's own guidance is that irrelevant state degrades
answer accuracy) — the harness roster is still policed against the live
roster directly when the decision is applied, never through this state. The
TypeSafe credential is read only from the environment variable named by
credential_env.
Price. TypeSafe Jev is priced through the catalogue's typesafe vendor
at $0.042 per MTok input, output free — see Model
catalogue — and costs at most one bounded call per launch.
The native runtime applies the same decision behind its existing gate: the
decided seat role (PromptRole::Single/Orchestrator) always applies, and
the decided model applies too when it names a configured, policy-allowed
native [route] directly, or — the common case, since the proxy's
harness-CLI-style aliases and an operator's own route names are independent
vocabularies — resolves to the same catalogue model as one of them (an
operator who named a route cheap with model = "sonnet" still gets it
picked for a decided sonnet; several matching routes prefer the role's own
default route, else the first by route id). No match at all leaves the pane
on its role's own default route rather than failing the launch. This feature
does not change the coming-soon gate itself.
The HTTP call to Jev is a shared client, not proxy-specific code; other
advisory sites it may back (memory ranking, the supervisor judge, dispatch
tiering, review triage, gate reclassification) are listed under their own
[jev] key, each still spending through this same
[proxy.typesafe] connection.