pstack model choices are provider-qualified descriptors:
<provider>:<model>@<effort>
| Family | Upstream pstack choice | Provider | Model | Default effort | Selectable efforts | Claude-native agent stem | First-run active |
|---|---|---|---|---|---|---|---|
| fable | - | claude | fable | max | low medium high xhigh max | fable | no |
| sol | gpt-5.6-sol-max | codex | gpt-5.6-sol | max | low medium high xhigh max | - | yes |
| grok | grok-4.7-xhigh-fast | grok | grok-4.7 | xhigh | low medium high xhigh max | - | yes |
| opus | claude-opus-5-5-max | claude | opus | max | low medium high xhigh max | opus | yes |
| sonnet | - | claude | sonnet | high | low medium high xhigh max | sonnet | no |
| astra | - | codex | gpt-6-astra | high | low medium high xhigh max | - | no |
| luna | - | codex | gpt-5.6-luna | high | low medium high xhigh max | - | no |
| terra | - | codex | gpt-5.6-terra | high | low medium high xhigh max | - | no |
For the model matrix, the allowed effort universe is exactly low, medium, high, xhigh, max. A - in Upstream pstack choice means the portable build added that family. First run activates only rows whose First-run active cell is yes, in matrix order, and uses each active row's Default effort. Later runs derive the active family set from the non-alias descriptors in the normalized final role map. No separate active-family setting exists. A Claude-native agent stem of - means the family has no Claude-native agent. Otherwise the shipped agent name is pstack-<stem>-<effort>.
The matrix records recommended defaults and effort guidance. It is not an allowlist. Use the provider's current model listing where available and probe the exact selected model and effort. A listing alone does not prove account access. Claude and Codex models can be checked through the current host's capabilities and actual execution without matching a matrix row.
A runner receipt keeps the requested model in model and the provider's report in reportedModel. For the legacy rolling aliases fable, opus, and sonnet, verification requires a numeric revision from the same family. Other IDs can match the provider report directly or with a hyphenated revision suffix, and new aliases can match a numeric claude-<requested-family>-<revision> report without a package update. A report naming a different family fails verification. Exact IDs outside the legacy migration rules pass through unchanged. Claude's report is the single message.model on the main assistant stream events (explicit parent_tool_use_id: null in the result's session); helper-owned events, modelUsage, init.model, and argv never verify the model.
Devin is an external provider from either parent, not a parent harness. These opt-in families do not change the three-model default panel.
| Family | Provider | Model | Default effort | Selectable efforts | CLI model UID |
|---|---|---|---|---|---|
| swe-2 | devin | swe-2 | high | medium high max | swe-2- |
| swe-1.6 | devin | swe-1.6 | default | default | swe-1-6 |
Use descriptors such as devin:swe-2@high or devin:swe-1.6@default. default records that SWE-1.6 has no selectable effort; it is also used for Cursor slugs without a separate effort flag. An explicit or stored SWE-2 descriptor with unsupported low or xhigh is invalid. Never clamp it during dispatch or after a failed probe. Setup may propose a supported effort below a user-requested budget ceiling for an already valid descriptor, but must show the change and obtain confirmation before saving. The runner maps these legacy descriptors to the exact CLI UID shown above.
For any other selected model, save devin:<uid>@default. Discover exact UIDs in families[].variants[].model_uid from devin models list --format json. The runner passes the selected UID to --model unchanged. Effort is part of that UID, so a non-default effort on an unmapped ID is invalid. devin:swe-2@default passes swe-2 itself. Do not guess variant suffixes. If discovery is unavailable or its output format changes, retain an explicitly selected ID and let the probe establish whether it runs.
Install and sign in to Devin CLI. Inspect devin models list --format json and probe each selected pair: listing a model does not prove the account can execute it. An upgrade-required response is an unavailable-model failure, never permission to substitute another model.
The runner uses --print and a private temporary --config file, deleted after completion or failure. It disables recursive subagents and imports from other tools, and denies MCP and fetch calls. Read-only lanes deny edit, write, and shell execution; use them for file inspection, not test execution. They also disable exec so the model cannot select the denied tool. Writer lanes leave exec enabled. Writer lanes use --sandbox in the dedicated worktree and perform edits and tests through sandboxed exec. Direct edit/write tools are denied and removed from the tool inventory in both modes: they run outside Devin's OS sandbox and can require interactive confirmation even with scoped write grants. Revisit this choice once unattended scoped edits work alongside sandboxed commands (CognitionAI/devin-cli#24). The adapter prefixes each writer task with these execution constraints in a private prompt copy, preserving the assigned prompt and its receipt path. File-tool-only prompts are unsupported. Devin's own project configuration, rules, plugins, and hooks can still load; this is not a clean-room execution environment. Do not assign an untrusted checkout or rely on this adapter to isolate startup hooks. Verify effective permission behavior in the target CLI before release. The runner removes inherited DEVIN_REFUSAL_FALLBACK from Devin child environments so that this ambient setting cannot enable provider-side model substitution. The parent environment and other providers are unchanged.
The temporary config marks shell onboarding complete. The runner requests a private ATIF-v1.7 conversation export and accepts only a final agent step with a nonempty message and no tool calls. A progress message followed by denied or pending tools cannot count as completion, even if Devin exits zero without stderr. For an incomplete export, the adapter recognizes only a final direct write call whose single tool observation has the matching call ID and exact provider-owned write-access denial for the requested path. It records a verified permission cause under the fixed code devin_atif_write_permission_denied; other incomplete exports remain malformed. A delivered final answer takes precedence. Only the final message is returned; system context, reasoning, tool arguments, and observations are never copied into output or receipts. The export directory is private and removed after every outcome. The adapter continues to use pinned-argv model evidence rather than treating the export's display model name as verified identity; model-report, session, usage, and cost fields remain null.
Cursor is an external provider from both parents. Install and authenticate cursor-agent, run cursor-agent models, and choose an exact available slug as cursor:<slug>@default (for example, cursor:composer-2.5@default when listed). default means no separate effort flag is available; select any reasoning variant by its exact model slug. Do not translate Claude/Codex/Grok slugs into Cursor slugs or use Cursor's auto selector. Model availability and subscription limits remain Cursor's responsibility. Adding a Cursor lane does not alter the three baseline families.
OpenCode is an external worker provider from both parents. It is not a parent app in this distribution. The adapter supports two incompatible native contracts, selected exactly once from the opencode --version banner by stable semver major: restored V1 (>=1.18.29 <2) and stable V2 (>=2.0.0 <3). Bare (1.18.32) and opencode v-prefixed (opencode v2.0.27) banners select the same contract. Compatible patch and minor releases pass through the same required capability checks; tested versions (1.18.32, and 2.0.0, 2.0.25, 2.0.26, 2.0.27) are evidence, never an allowlist or a minimum based only on the tested build. Prereleases, unknown majors, malformed output, and trailing noise are rejected before route-specific configuration inspection or inference. Save an exact opencode:<provider>/<model>@default assignment. Additional model path segments are preserved. auto, missing or empty segments, and non-default efforts are invalid. The runner omits --variant because unavailable variants may silently use defaults. Native opencode debug models can expose exact IDs after the registry is ready; cold empty lists are not access or entitlement evidence. The existing default panels and saved assignments do not change.
Ordinary native authentication requires explicit apiSpend: approved authorization on both contracts. OpenCode can route through several upstream providers and credentials, so this general route cannot prove subscription-only billing. Both deny and omitted legacy authorization block before CLI startup on this route. Never read or copy credential files or infer spending permission from authentication, a subscription, or a model listing. Setup probes each selected exact model after authorization.
An explicitly requested ChatGPT subscription attempt passes --opencode-chatgpt-subscription --api-spend deny with exactly openai/gpt-6.1-sol at default effort, on either contract. The opt-in applies only to this attempt. The environment allowlist preserves the native HOME and data location for OpenCode's own existing login while dropping inherited API keys, tokens, proxy controls, route overrides, preload settings, and all inherited OPENCODE_* values. No credential files are inspected, copied, or exported. On V2, the private config selects opencode.provider.openai and opencode.config.provider, and configures https://chatgpt.com/backend-api/codex with HTTP transport; this is a configured endpoint, not an attestation of the final resolved route. On V1, the private config keeps an empty provider section because the native builtin OpenAI OAuth plugin loads under --pure and uses its own builtin endpoint, so receipts truthfully record no configured endpoint.
Subscription preflight runs after private configuration validation, with the form selected by contract. The CLI must expose credential-method metadata for this mode. Native V2 2.0.0 omits that metadata and is rejected before inference; upgrade to a CLI that exposes it. This capability requirement does not change the general supported version range. On V2, opencode auth list --standalone --format json must return exactly one OpenAI integration and exactly one stored OAuth connection; keys, environment or external connections, duplicate connections, and additional authenticated integrations, including OpenCode Console, block before model execution. On V1, opencode --pure auth list prints a human-readable credentials block parsed as a whole known structure: a Credentials <path> header, exactly one OpenAI oauth entry, and a 1 credential[s] footer, each line optionally prefixed by native frame glyphs after recognized ANSI sequences are stripped. Extra entries, API or well-known credentials, environment sections, duplicate or split blocks, unknown lines, bad counts, and control noise all block before model execution. The private configuration is rechecked after preflight and immediately before invocation on both contracts. Receipts record passed OAuth evidence separately from funding intent; rejected attempts retain apiSpend: deny and no authentication claim. Raw native authentication output, connection IDs, labels, credential paths, and account metadata are withheld. Native startup may refresh its own OAuth login or fetch account configuration before this check returns.
This mode trusts a normal OpenCode ChatGPT login or native legacy migration on either contract. Native auth listing exposes credential type, but not its login-method ID or hidden metadata. Deliberately modified OAuth metadata can override the configured route, and another process can change the native credential store between commands. The mode blocks ordinary visible key, environment, Console, and configuration takeovers; it does not attest a hostile credential store or guarantee network egress. No login loop, implicit timeout, or billing-enabled fallback is added.
Each attempt creates an exclusive <receipt>.opencode-config directory with mode 0700 before the version probe, using a sanitized environment with private configuration discovery and no contract-specific config pointers; native login locations remain available. After the banner selects a contract, the private files are written per contract with mode 0600. V2 uses OPENCODE_CONFIG_DIR pointing at a private config/ directory containing opencode.json and service.json; the latter disables the shared service. V1 uses OPENCODE_CONFIG pointing at a single private opencode.json at the root. Both clear inherited OPENCODE_* overrides and disable project discovery and CLI updates; V2 additionally pins cwd and PWD, disables file watching, and selects a positive builtin plugin list after -*, while V1 disables external skills, Claude Code compatibility, autocompaction, and LSP downloads. Approved native authentication preserves the real home and native data location while isolating configuration discovery. Guarded OpenCode provider and policy plugins survive removals on V2; V1 external plugins stay disabled and its provider section stays empty. Ambient instructions, compatibility, skills, browser, web, MCP, and recursive-worker registration are excluded. Shell registration is restricted to sandboxed writers. The V2 config uses model objects, plural agents, and ordered action/resource/effect permission rules; the V1 config uses model strings, a singular agent, and an ordered permission map. Sharing, snapshots or warming (V2), LSP, formatting, and compaction are disabled. A fixed title avoids automatic title generation.
Preflight shares the caller's deadline and cancellation handling. After the version check selects the contract, opencode debug config (--pure debug config on V1) verifies the private configuration. On V2 the output must be exactly the private document and directory: native objects are compared structurally, rule and plugin array order remains significant, and foreign, pathless, duplicate, missing, or additional sources fail. On V1 the merged effective configuration must carry the pinned model, small model, agent, and ordered permission map exactly, keep provider overrides empty, and enable no MCP server. On V2, unexpected supplemental files or symlinks in the private config directory also fail. Both contracts reject a replaced or modified private config file. The expected config and environment are immutable for the attempt. Raw configuration output is withheld from receipts. A passed receipt says configuration or effective policy verified with the detected contract and version; this proves neither authentication, model access, nor active runtime permission enforcement. Only a completed model response establishes execution.
Read-only permits read, glob, and grep. Isolated-write additionally permits edits, requires the real Git worktree root as cwd, and on V2 also registers write and denies edits to .git metadata. The restored V1 permission policy does not add an explicit .git edit deny. Both modes start with wildcard denial and deny external directories, recursive tasks, web tools, skills, questions, and MCP calls. Read-only denies shell. Writers enable V1 Bash or V2 shell through a required Anthropic sandbox-runtime wrapper, so they run their own suitable checks. The wrapper and policy live in an exclusive temporary control directory outside the worktree, are checked before invocation, and are removed after the attempt settles. V1 pins SHELL; V2 additionally pins the native shell setting. A sandboxed startup probe must pass before inference. Missing dependencies or startup failure stop execution without an unsandboxed retry. Supported shell hosts are macOS and Linux with sandbox-runtime dependencies available. On macOS, an enclosing Seatbelt sandbox prevents a second sandbox profile from starting. The parent needs an already authorized execution surface outside that enclosing sandbox; otherwise the writer remains unavailable. The worker sandbox stays required.
Shell writes are allowed in the worktree and private temporary/cache directories, with the control directory and .git pointer denied. The runtime also permits its standard system paths, /tmp/claude, ~/.npm/_logs, and ~/.claude/debug. Network domains, local binding, and arbitrary Unix sockets are denied; weaker nested sandboxing is disabled. Read access follows the runtime defaults. Direct file-edit tools retain their application permissions, as with external Claude writers. This is shell confinement, not whole-provider isolation or strict Git ownership. Pre-existing hard links, trusted runtime files, native shell-selection fallback after external removal of the wrapper, and filesystem races remain limitations. Worker guidance requests foreground commands. V2 shell accepts timeout 0 to disable its command timer. V1 Bash requires a positive timeout and retains its native 120-second default when omitted; the adapter does not invent a replacement value. Neither the runner nor sandbox wrapper adds an attempt timeout. Final conversation completion does not prove that a command passed. These lexical tool permissions are not an OS sandbox, and allowed paths can reach symlink targets outside the worktree. Use trusted workspaces, symlinks, and installations. Ordinary native-auth startup may load wellknown or account configuration before preflight rejects extra sources. The check cannot undo startup effects or guarantee plugin activation and settings remain unchanged between processes.
Invocation derives from the same frozen contract as configuration and preflight. V2 uses opencode run --standalone --format json --model <provider/model> --agent <attempt-agent> --title pstack-worker; V1 uses opencode run --pure --format json --model <provider/model> --agent <attempt-agent> --dir <cwd> --title pstack-worker, with every V1 call passing --pure. The prompt goes to stdin. A separate JSONL inspection validates session and assistant identities, including text-only reconciliation and a missing final step_finish. On V2, after the workload settles, one opencode session export --standalone <sessionID> command runs with the same executable, frozen environment, cwd, cancellation, and absolute optional deadline. Standalone export reads persisted state without resuming inference; it supplies no prompt and never replays, continues, forks, or starts a managed service. V1 has no session export and never attempts one.
V2 delivery requires a successful workload and export process, one matching fresh session and prompt, the expected cwd, agent, and configured model, all observed assistants in order, and the exact final observed assistant completed with a tool-free stop. Session outcome and the trailing idle record must both be succeeded. Earlier tools must be settled. Truncated, stale, mixed, conflicting, unknown, or incomplete records fail closed. Final text and usage come only from export, once; the runner never synthesizes a finish. V1 delivery instead requires the strict terminal stream directly: matching session and assistant identities, a tool-free final step_finish with reason stop, and nonempty final text. Stored model selection remains pinned-argv, with reportedModel: null and modelVerified: false. A completed export can preserve terminalSuccess after nonzero workload exit, malformed handoff, or delivery failure to prevent replay, while those outcomes still fail and produce no output. Cancellation suppresses that proof; deadline expiry preserves only proof already obtained. Strict streamed completion remains an additional conservative replay guard if export fails.
Native tool identity is partID on V2 and id on V1; both are admitted without treating two conflicting identities as interchangeable. Pre-session errors with an empty session ID remain errors. Unknown quota errors, including generic 429s, never authorize fallback. Receipts retain bounded error type and message, never raw exports or private response bodies. Owned private files are removed after all subprocesses settle on completion, failure, explicit timeout, or handled cancellation; preexisting paths are preserved.
The native configuration and event contracts were inspected in OpenCode V2 source at 5e73d5c, including schema/src/config.ts, core/src/config/discovery.ts, core/src/plugin/internal.ts, core/src/plugin/provider/opencode.ts, and cli/src/run/noninteractive.ts, and in the released V1 adapter contract restored from v1.13.0. Installed-parent runtime validation across the tested versions is a separate release gate.
Antigravity is an opt-in external worker from either parent. Install and sign in to agy, inspect agy models, and use an exact listed slug such as antigravity:gemini-3.1-pro-high@default. The slug already contains the available reasoning variant; the runner does not pass --effort, and auto is invalid. Hosted Claude models remain Antigravity routes for access, receipts, and exhaustion grouping. It does not change the default panel. Every Antigravity attempt requires an explicit saved apiSpend choice, including on legacy sheets.
The model sheet is the only persisted routing source. Each line assigns one or more role names before the first : and the seat list after it. Role names themselves contain commas (feature, refactoring, why investigators, synthesizer, reflect tooling, judgment, divergent, synthesizer), so the header is matched against the documented role list before the right-hand side is split.
On the right-hand side, commas and arrows have different meanings and are never interchangeable:
,separates independent panel seats. Each seat runs one candidate concurrently; the seat count is the fan-out count and repeated entries are not deduplicated.->separates ordered attempts within one seat:primary -> fallback(one or two fallbacks, for exampleprimary -> fallback1 -> fallback2). A single descriptor is a one-attempt chain and authorizes no fallback.
feature, refactoring: grok:grok-4.7@xhigh -> codex:gpt-5.6-sol@high
how explorer: grok:grok-4.7@xhigh
A chain is bounded at three attempts. Duplicates and a second attempt on the same provider are invalid: each provider route uses that CLI's current account, so another model on the same provider cannot recover exhausted capacity. An alias and a descriptor on the parent provider use that same parent route. Why and Reflect rows keep every attempt on inherit-parent or auto because they require the parent's live MCP surface. Legacy single-attempt rows remain valid and authorize exactly one attempt.
# access: <JSON> comment lines record one access fact per provider: provider, funding (included, metered, unknown), capacity (standard, high, unknown), apiSpend (deny, approved), provenance (user, provider), plus optional display-only plan, note, and observedAt strings. Funding, capacity, and the optional fields explain recommendations; they are advisory and never treated as durable entitlement. apiSpend is binding authorization: pass its exact saved deny or approved value on every policy-enabled external attempt. The sheet contains no credentials or account selector. The provider name conservatively groups exhaustion under the CLI's current account.
The installed helper skills/poteto-mode/scripts/model-policy/pstack-model-policy is the policy boundary. It parses and decides; it never probes providers, reads credentials, or launches anything. Setup validates the complete candidate, including authorization for every row:
pstack-model-policy validate --sheet <candidate-file> --parent <claude|codex>
At runtime, first copy the active sheet's exact bytes to a unique run-local file. Keep that frozen copy and the original parent value for the whole run; never reread the live sheet between attempts. resolve gives an inspection view of a role's lane chains:
pstack-model-policy resolve --sheet <frozen-sheet> --role "<role>" --parent <claude|codex>
A missing sheet or a missing role in a legacy sheet returns status: "unconfigured": use the calling skill’s documented default as one attempt, with no fallback permission. A sheet containing access metadata, an ordered chain, a # fallback declaration, or a # continuation declaration is policy-enabled; a missing requested role is an error, so defaults cannot bypass its saved permissions. Unreadable or malformed sheets also fail instead of selecting defaults.
Before every attempt, including each primary, ask next for the decision for that zero-based lane:
pstack-model-policy next --sheet <frozen-sheet> --role "<role>" --parent <claude|codex> --state <lane-state.json> --lane 0
The parent owns <lane-state.json> with this exact shape:
{"events":[{"attemptIndex":0,"status":"usage-exhausted","processStarted":false}],"exhaustedGroups":["grok"],"access":"read-only"}events retains every earlier attempt for that lane in order. Both attemptIndex and --lane are zero-based; event status is one of complete, usage-exhausted, route-unavailable, terminal-failure, deadline-exceeded, or failed (failed covers every outcome the saved policy cannot advance, including cancellation and billing blocks), processStarted is included when known, receiptPath preserves the runner receipt, and a writer event may carry inspection:{"state","evidenceRef"} recording the parent's verdict. Events normally come from pstack-model-policy normalize, which reads the actual receipt and checks it against the frozen sheet attempt. exhaustedGroups is the run's accumulated provider list and must be carried into every lane state. access is read-only or isolated-write. A launch decision supplies the only authorized attempt to route. A stop decision ends the lane; in particular, an unauthorized route stops and is never skipped to reach a later descriptor. An inspect decision preserves a writer for review. After each terminal attempt, retain its receipt or native transcript, append its event, add a provider only after supported exhaustion evidence, and call next again. The caller also passes the selected provider's saved apiSpend value to every external launch and retains all histories and group failures. Skills must not duplicate this decision logic by splitting model-sheet prose themselves.
Normalize configured descriptors before matching them to the matrix or choosing a route. If a provider-qualified Claude model starts with claude-fable-, claude-opus-, or claude-sonnet- and its remaining revision contains only digits and hyphens, replace that model component in memory with fable, opus, or sonnet. Preserve provider, effort, role, and lane order. Use only the normalized descriptor for native dispatch or runner argv. Never pass the versioned predecessor to Claude.
This read-time rule makes an older installed sheet use the latest family revision immediately without writing user files. Once per parent run, report that the persisted sheet is stale and that /setup-pstack will rewrite it after its normal probes and confirmation. Versioned Claude IDs outside the three migration families are ordinary exact IDs and pass through unchanged. The external runner rejects a missed Fable, Opus, or Sonnet version pin instead of silently executing it.
fast is part of Cursor's Grok selector, not a Grok Build CLI model or effort flag. Upstream's current Grok default grok-4.7-xhigh-fast names that selector; the portable Grok route pins grok-4.7, as advertised by Grok Build CLI. A model listing does not prove authenticated execution; setup still probes every selected route. The first-run Grok effort is xhigh. Update the pin only when grok CLI reports a newer model.
The top-level harness freezes the sheet once and asks the helper for each attempt decision. A child receives an assigned provider, model, effort, access mode, prompt, working directory, and output path. A child never detects the harness, chooses a provider, or launches another model. Environment markers may corroborate the top-level harness before fan-out, but nested processes inherit parent markers and must not use them for routing.
| Parent | claude:* |
codex:* |
grok:* |
devin:* |
cursor:* |
antigravity:* |
opencode:* |
|---|---|---|---|---|---|---|---|
| Claude Code | native Agent when a shipped lane covers it, else external runner |
external runner | external runner | external runner | external runner | external runner | external runner |
| Codex | external runner | native spawn_agent |
external runner | external runner | external runner | external runner | external runner |
inherit-parent and auto remain aliases. They use the parent's current model and effort through its native subagent primitive. In a panel they still consume one lane, but they reduce provider diversity; say so in the synthesis record.
Native dispatch avoids a second CLI startup and its base context.
- Claude Code: the shipped
agents/pstack-*.mddefinitions are the native capability. Aclaude:<model>@<effort>descriptor is native exactly when a shipped agent's frontmattermodelandeffortmatch it; dispatch that lane through the matchingpstack-<stem>-<effort>agent, which selects the model alias, effort, andbackground: true.pstack-fable-maxandpstack-opus-xhighremain in that set. Any other Claude model on a Claude parent — an unlisted family alias or an exact ID — uses the external runner with the model passed unchanged. Pass the complete task, grounding paths, access mode, and unique output location in theAgentprompt. Retain the task handle and drain it only after fan-out. - Codex: call
spawn_agentwith the descriptor's model andreasoning_effort, the complete task, grounding paths, access mode, and unique output location. Use an isolated worktree for a writer. Codex subagents already run concurrently.
Before either native call, run pstack-worker-contract prepare --parent <claude|codex> --provider <provider> --route native --mode <read-only|isolated-write> --contract legacy and prepend its returned instructions to the task prompt. This is mandatory for read-only and writer assignments. Give read-only lanes a distinct parent-assigned output artifact; they may write that artifact but not edit the checkout they inspect. The legacy contract states that the parent owns Git mutations and final-response handoffs; it is prompt guidance, not hard confinement. A strict request returns unsupported and ends the assignment without trying another provider.
Send a same-provider descriptor to the external runner only when no shipped native lane covers that exact model and effort; the runner rejects a same-provider call a native lane covers because that route is cheaper and already available. A Codex parent always covers codex:* natively. Policy resolution and runner validation share this route decision. An external route still requires successful authorization, authentication, and execution. Choose the route before launching the attempt; do not retry a failed native attempt through the external CLI.
The launcher lives at skills/poteto-mode/scripts/runner/pstack-runner under the installed plugin. The parent writes the complete candidate prompt to a unique file, creates a unique output directory or worktree, and invokes the launcher directly. Do not put another agent in front of it.
pstack-runner \
--parent <claude|codex> \
--provider <claude|codex|grok|devin|cursor|antigravity|opencode> \
--model <real CLI model> \
--effort <low|medium|high|xhigh|max|default> \
--mode <read-only|isolated-write> \
--prompt <unique prompt file> \
--cwd <repository or dedicated worktree> \
--output <unique final-response file> \
--receipt <unique receipt file> \
[--execution-id <parent-assigned id>] \
[--contract <legacy|strict>] \
[--progress <unique progress file>] \
[--api-spend <deny|approved>] [--opencode-chatgpt-subscription] \
[--timeout <seconds>]
Pass arguments as an argv array or quote every path. Never interpolate prompt text into a shell command. For every external attempt resolved from a policy-enabled sheet, pass that provider's saved apiSpend value; omit --api-spend only when executing an untouched legacy sheet with no access metadata and no fallback chains. The launcher prepends the shared ownership contract to every external prompt, including read-only lanes. It checks the assigned CLI and preflights authentication where supported (Claude defers authentication to invocation), invokes the model exactly once, disables recursive agents and ambient skill dispatch where the CLI supports it, restricts the built-in tool surface, and records the exact provider/model/effort flags. External lanes do not receive the parent's MCP surface. Keep MCP-dependent Why and Reflect roles on inherit-parent or auto. The launcher never falls back.
legacy is the default for existing assignments. Its ownership rule is prompt guidance. strict requires live proof that file edits remain possible while repository Git metadata, alternate gitdirs, metadata pointers and hardlinks, shell descendants, and authenticated remote mutation stay blocked. No current native or external route has that complete proof. prepare --contract strict returns unsupported; the runner writes a schema 2 unsupported-capability preflight receipt with processStarted:false and exit 79. This is a terminal capability stop, not a route failure that can advance a saved fallback. CLI help flags, tool allowlists, sandbox labels, and an inherited full-access parent do not establish the boundary.
Claude has no separate authentication preflight on any platform. Do not run claude auth status during setup or worker dispatch: its short-lived process can consume a refresh token and exit before saving the replacement (upstream issue 95822). The actual task handles authentication. Read-only receipts record preflight.status: not-run. Writers first run claude --version and require a stable version of at least 2.1.285, the first version validated with the writer sandbox profile. Old or malformed versions produce unavailable-cli before model execution. A passing version probe does not establish authentication or billing. Known API environment checks remain, but Claude's account billing type is not asserted. See the exception and conditions for revisiting it.
Grok authentication preflight has one bounded retry. If the first grok models result would be classified as unauthenticated, the runner waits five seconds and tries the same preflight once more. A second failure is terminal. The delay and second attempt share the runner's absolute deadline and cancellation latch, and the receipt keeps evidence from both attempts. Model execution is never retried.
The parent tool sandbox still governs whether a subscribed child CLI can reach its credentials and network. Run setup's live probe from the actual parent profile. A blocked external CLI is a loud dropout, not a reason to elevate permissions or substitute a model silently.
The parent invocation must itself be resumable background work:
- Claude Code: call the launcher through a Bash tool invocation with
run_in_background: trueand retain its task ID. A foreground Bash tool call has an automatic ten-minute ceiling even when the runner's own timeout is longer. Shelling out with&and losing the task handle is not equivalent. - Codex: run the launcher in a persistent exec session that returns a session ID, then wait or poll that handle. Do not hold one foreground tool call open for the model's full runtime.
Start the background process, continue launching the other lanes, then drain their handles. Native and external lanes belong in the same fan-out phase.
Pass a unique --progress path per external lane when the parent wants lifecycle visibility before the terminal receipt exists. The launcher reserves it exclusively alongside output and receipt, writes a bounded private snapshot (schema version, derived phase, last-observed direct-child state, byte counts and the timestamp of last observed output, cancellation facts, and a terminal marker), and replaces it atomically. Keep the parent-owned artifact paths unchanged until the lane exits. An observed replacement disables further progress writes; the identity check and rename are not a lock against external path replacement. Progress writes are telemetry only: a write failure disables them silently and never changes the lane's outcome.
Read lanes with the runner's own reader:
pstack-runner status --progress <progress file> --receipt <receipt file> \
[--progress <progress file> --receipt <receipt file> ...] [--json]
- Claude Code: a short foreground
Bashcall while the launcher'srun_in_backgroundtask handles stay retained. - Codex: a short
execbetween polls of the retained exec session handles.
Process-identity probes run asynchronously beside the worker; pending or unavailable identity remains unknown. They never delay prompt delivery or determine the worker deadline. status is read-only. JSON includes updatedAt and seq so callers can identify an old observation without treating its age as failure. It merges the explicit progress path, the explicit receipt path, and a process-start probe (ps -o lstart, with the platform’s timestamp precision), never a path stored inside the snapshot. A lane is terminal only when the receipt itself parses to a known status — a finished progress marker without a valid receipt reads as unknown, never active. interrupted means the recorded launcher pid is absent or was reused; it is explicitly not a failure verdict. launcher identity unverified means the platform gave no usable process-start token (the local probe is bounded to one second, independently of the worker); read it as unknown, never alive or dead. Cancellation renders as requested, then as confirmed only after the direct child settles and the cancelled receipt lands; nothing claims provider descendants stopped. Output activity is byte counts and a timestamp only: a quiet worker can be healthy, busy output does not prove useful backend work, and neither authorizes retry or fallback.
Surface a lane update to the user only when its rendered changeKey differs from the value last reported for that lane; changeKey covers phase, child state, cancellation, launcher verdict, and terminal fields, and deliberately excludes elapsed time and byte counts. There is no periodic unchanged-state reporting and no per-event message. Cancellation still goes through the retained task handle, and policy decisions still read only the receipt through normalize.
The runner and its preflight have no implicit timeout. Do not invent a duration from role, mode, or a convenient round number; real implementation lanes can run for 90 minutes or much longer. Pass --timeout only when the user, an external service deadline, or a measured task contract supplies a real bound. That value starts at wrapper entry, before module loading and argument parsing, and remains one absolute deadline across setup, preflight, model execution, and output capture. It is never a fresh allowance per child, and long waits are armed in runtime-safe chunks without shortening the supplied deadline. Otherwise supervise liveness through the retained background task/session handle and cancel manually only on evidence that the run is dead. Cancel through that retained handle so the runner receives SIGINT or SIGTERM, sends it to an active child when one remains, stops waiting on inherited output pipes, removes the empty output reservation, and writes a cancelled receipt. Preserve that receipt; a retry is a new attempt with new unique output and receipt paths. Unchanged running state is not a dropout, and Claude's ten-minute foreground ceiling is never a reason to terminate a healthy lane.
Read-only mode maps to Claude plan mode with an explicit tool list and project-only settings, or empty setting sources under apiSpend: deny. It maps to Codex's read-only sandbox and Grok plan mode plus its read-only sandbox and read-oriented tool list. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under /tmp, /var/tmp, or the host's temporary directory. isolated-write maps to Claude acceptEdits, Codex workspace-write, and Grok acceptEdits plus its workspace sandbox and write-capable tool list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout.
External Claude writers receive session --settings that enable sandboxed Bash and autoAllowBashIfSandboxed, require failIfUnavailable, disable allowUnsandboxedCommands, leave excludedCommands empty, keep filesystem isolation enabled, and allow no network domains. Writer setting sources are empty for every spending policy, so project sandbox exclusions cannot widen command access. Managed policies still apply. Read-only lanes receive no sandbox settings because automatic Bash approval could permit writes. A missing sandbox dependency or unsupported platform fails loudly; the runner never retries without isolation or grants unrestricted Bash. Checks that need outside-directory caches or network access remain blocked and require parent inspection. This shell profile does not establish strict Git confinement. See Claude's sandbox contract.
Cursor uses the explicit cursor-agent executable, not agent (which can name another CLI). For stored OAuth credentials, preflight requires cursor-agent status --format json to return isAuthenticated: true. With a nonblank CURSOR_API_KEY, preflight checks cursor-agent --version only and records that authentication is deferred to the real model invocation; Cursor status does not report API-key authentication. The key stays in the environment. Authentication failures from execution fail the lane. Setup always performs a real model probe because preflight alone does not prove model access. The runner sends the prompt through stdin, pins --model, requests a successful JSON terminal result, and uses --workspace with --sandbox enabled. Read-only adds --mode ask and denies Write(**) and Shell(*); it cannot run shell-based tests. Writers use the sandbox in their dedicated workspace without --force or --yolo.
Each Cursor attempt exclusively creates a private <receipt>.cursor-config directory (0700) containing cli-config.json (0600), sets CURSOR_CONFIG_DIR for preflight and execution, and removes that directory on completion, failure, timeout, or handled cancellation. Existing paths are never overwritten or removed. The temporary permissions deny Mcp(*:*) and WebFetch(*); no user configuration is edited and no credentials are copied. Cursor's authentication storage is separate from this configuration directory. Before overriding the directory, the runner reads the original global configuration (CURSOR_CONFIG_DIR, otherwise XDG_CONFIG_HOME/cursor, otherwise ~/.cursor) and preserves only a boolean network.useHttp1ForAgent, needed by some proxies. It never copies permissions, hooks, credentials, endpoints, or other settings. An unreadable or malformed existing configuration fails the lane instead of silently losing its transport setting.
Cursor's CLI currently exposes no supported switch to disable recursive subagents, project rules, skills, plugins, or hooks. The runner passes --trust for the assigned workspace so headless execution can start; this may authorize project startup hooks. Use only trusted workspaces and keep the parent-owned assignment in the prompt. These settings are not a clean-room guarantee, and the receipt does not prove that project hooks or every descendant were sandboxed. Cursor's native child tools also differ from the parent's tools; MCP-dependent work must stay native. See Cursor's configuration, permissions, and output format contracts.
Antigravity preflights with agy models; a listed slug proves discovery, while authentication and execution remain subject to the real attempt. The runner sends one stream-json user message over stdin, pins --model and a unique --agent, enables --sandbox, and audits the final stream. Read-only launches from an exclusively created private directory beside the receipt, adds the assigned checkout with --add-dir, and declares only view_file, list_dir, and grep_search. Writers launch from their assigned dedicated worktree, pin --project default-cli-project, and use --mode accept-edits. They retain the read and direct file-edit tools and gain run_command and manage_task under commandExecutionPolicy: sandbox. --disable-slash-commands remains read-only because combining it with --mode disables the requested mode. Web, MCP, recursive agents, sandbox bypass and external terminals remain unavailable.
Writer preparation exclusively creates a temporary agent definition and .agents/hooks.json, refusing existing hooks and symlinked policy ancestry. Fixed approval code, captured policy and bounded session state live in a unique directory under the real home, outside the worktree and native writable temp/cache roots. The hook approves arbitrary nonempty command text without matching shell words; it validates command options, canonical cwd and conversation-local task selectors. Direct file tools cannot alter the generated agent or hook. Prefer foreground checks; collect native exit results through manage_task or completion notifications when background work is needed. A failing check permits repair and rerun. Hook denial or failure ends the lane even if the conversational result says SUCCESS. Stop continues while native tasks are not fully idle. Delivery does not independently prove that worker-selected checks passed. The receipt does not prove all descendants were terminated.
Writers require established safe default-project configuration. Preparation inspects CLI settings, shared and default-project configuration, global hooks, both documented user plugin roots and workspace plugins without reading credentials. Harmless trusted-workspace lists and billing-route settings remain supported; execution-policy customizations, competing hooks, active plugins, extra project resources, unknown policy and relevant execution environment overrides are refused with source/key diagnostics. Only the Codex marketplace catalog named open-pstack is accepted as data; other or unknown catalogs are refused. No user setting is changed or restored. Captured source fingerprints are rechecked before launch, by hooks and after settlement to detect changes; they do not prevent concurrent mutation. Cleanup removes only owned unchanged files and reports replacements while preserving them.
Writer preflight first requires a bare stable numeric agy --version result in 1.x, then performs model discovery. Unknown majors, prereleases and malformed version output stop before model invocation. The compatible contract is agy 1.x with native sandbox, startup-captured workspace hooks, command/task tools, bound hook metadata and fail-closed hook execution. The observed checkpoint is 1.3.5, not an exact-version allowlist. Installed-candidate verification must exercise these capabilities; a missing or incompatible capability blocks that lane. The native sandbox may allow its own temp/cache writes, existing hardlinks and loopback socket binding; this is neither whole-provider isolation nor strict Git enforcement. Trusted startup remains required. Receipts contain bounded denial/provenance outcomes, never command logs or raw hook input.
Under apiSpend: deny, Antigravity blocks known environment billing routes (GEMINI_API_KEY, GOOGLE_GEMINI_BASE_URL, AGY_ADC_AUTH, and set AGY_GATEWAY_* / AGY_LLM_GATEWAY_* keys), an unreadable or malformed ~/.gemini/antigravity-cli/settings.json, and settings containing modelProvider or modelConfigOverrides. Only key names appear in receipts. This is a route guard, not a claim that provider account overage cannot occur. apiSpend: approved permits the selected account route.
Every concurrent external lane needs distinct prompt, output, receipt, and progress paths. The launcher reserves output, receipt, and progress paths exclusively, rejects progress paths that alias the prompt, working directory, output, receipt, or provider sidecar files, and refuses to overwrite any of them.
Success requires all of these:
- Exit status
0. - Receipt status
complete. - Either
modelVerified: truewithmodelEvidence: "provider-report", or a Codex, Devin, Cursor, or OpenCode receipt withreportedModel: null,modelVerified: false, andmodelEvidence: "pinned-argv", or an Antigravity receipt with its requested slug echoed inreportedModel,modelVerified: false, andmodelEvidence: "pinned-argv". Antigravity'sinit.modelmust exactly match the request but merely echoes the CLI override; it does not verify the backend model. Claude model-report matching follows the alias and exact-ID rules in Model matrix. For pinned-argv evidence, verify the receipt provider, model, and effort match the assignment and its argv pins the expected CLI model. Codex, Cursor, and OpenCode pin the assigned model directly; Cursor, Antigravity, and OpenCode requireeffort: "default"; Devin uses the exact UID mapping in Optional Devin models (for example,swe-2@highpins--model swe-2-high). - A non-empty output file.
The receipt also carries elapsed time, token usage when the CLI exposes it, and cost when available. Cursor returns session IDs and may return token usage; missing model identity, usage, or cost stays null. Keep it with the arena or review artifacts so parent-harness comparisons are evidence-based.
Any missing CLI, failed login, unavailable model, explicit timeout, cancellation, catchable post-reservation launcher failure, non-zero child exit, malformed result, or model mismatch is a receipt-bearing dropout. Record it and apply the calling skill's existing dropout policy. A cancelled receipt proves that the runner received the signal; its signal field is non-null only when the runner sent that signal to a still-active direct CLI child, and remains null when cancellation only stopped a post-exit pipe drain. The provider CLI owns any processes it starts beneath that direct child; the receipt does not claim a process-tree kill. Do not delete or overwrite the receipt. Never substitute the parent model, retry another provider, or reinterpret an external descriptor as a native model slug — the only permitted second attempt is the saved chain below.
Start native and external lanes in the same fan-out phase, then wait for all of them before judging. A judge must not read candidate paths while their owners are still writing.
The parent owns the assigned repository's index, refs, configuration, Git metadata, and remote writes. Every native or external worker receives this rule in its prepared prompt. Workers may edit ordinary assigned files and run allowed checks; tests may create disposable fixture repositories only when the task allows them. Read-only tool settings alone do not remove every route's shell or network authority, and the legacy prompt is not an enforcement claim.
Writer assignments require suitable local checks, repair of failures caused by their edits, and another affected check run. The parent gives provider-neutral tasks; each external invocation supplies guidance for its configured tools. Devin writers use sandboxed exec for edits and checks. OpenCode writers use their file tools and sandboxed shell for checks. Antigravity writers use direct file tools and native sandboxed commands, collecting task completion before delivery. Native workers use permitted inherited host tools without an enforcement promise. A failing assertion permits repair; a denied tool ends the worker turn without retry, workaround, or escalation. Final responses name actual commands, observed results, and checks left unverified. Worker reports and worker-written logs are advisory. Provider tool observations establish worker execution; separate parent acceptance checks establish the reviewed candidate's behavior. A complete receipt proves final delivery, not independently verified checks.
A worker that needs a parent-only operation may end its ordinary final response with exactly one pstack-handoff fenced JSON object containing task, checkpoint, operation, files, checks, and summary. operation is commit-checkpoint or run-checks. A delivered handoff is not task completion. Schema 2 receipts use status needs-parent-operation; an ordinary final response uses complete. Truncated, repeated, or malformed reserved markers fail closed. Older schema 1 readers reject schema 2 rather than interpreting the handoff as completed work. A verified permission refusal is classified from provider-owned evidence; worker prose alone cannot prove it, and an earlier nonessential refusal cannot replay a delivered completion.
The parent can inspect a response with pstack-worker-contract interpret --response <final-file>. Before acting on a handoff, run:
pstack-worker-contract op-validate --response <final-file> --repo <canonical-worktree> --task <assigned-task> --checkpoint <assigned-checkpoint> --expected-head <sha> --file <allowed-relative-file> [--file ...] --check <allowed-check-id> [--check ...]
This validates exact task/checkpoint scope, the expected HEAD, canonical Git metadata and allowed regular files, including symlink and hardlink escapes. It returns a SHA-256 checkpoint digest over HEAD, operation, checks, and framed file paths, modes and contents. Recheck the digest and HEAD immediately before the parent's fixed operation. The worker request never grants permission or supplies shell text. The parent stages only approved paths with --, or runs only a parent-owned check ID. A check that executes worker-modified code must use an appropriate sandbox or reviewed trust boundary; run-checks is not permission to run arbitrary worker-authored code with unrestricted Git or network access.
Reserve the operation with pstack-worker-contract op-record --ledger <file> --id <unique-id> --task <task> --checkpoint <checkpoint> --kind <commit-checkpoint|run-checks> --state pending --expected <json>. Only after the pending record is durable and the parent has revalidated preconditions may it perform its fixed operation and record --state complete --result <json> or --state rejected. op-status --ledger <file> exposes pending records. A pending record after a crash is ambiguous and requires reconciliation; do not mark it rejected merely to replay the operation. Rejected operations retain their task/checkpoint/kind identity; a new operation needs a separately reviewed checkpoint identity. The ledger serializes concurrent CLI writes and atomically replaces its file. Recording an operation is not authorization to execute it.
Failure receipts retain launcher-authored diagnostics, canonical quota codes, selected terminal fields, and structural line and JSON-event counts. The runner uses full captures transiently for parsing, quota classification, and terminal-success assessment. Preflight captures and stderr contribute counts only. Postprocessing retains terminal detail only from a typed provider error; invocation failures project the final candidate envelope, using OpenCode's shared parser to select its error. Provider denial records retain a fixed code rather than denied tool arguments. Each retained text field and error message is capped at 500 characters, and each rendered evidence block at 4,000. Intermediate prompts, reasoning, tool input/output, narration, unknown envelope fields, and malformed lines are omitted. Selected terminal result/error text can itself quote task content; this boundary does not redact that text.
The runner and native tool envelopes classify failures; the helper owns every routing decision. A sheet may declare one saved recovery policy:
# fallback: {"on":["usage-exhausted","route-unavailable","terminal-failure","deadline-exceeded"]}
The on list is a closed union over exactly those four policy outcomes, with no duplicates and no unknown keys; a second # fallback: line anywhere in the sheet (preamble, between rows, or footer) is malformed and rejected. An explicit # fallback: {"on":[]} disables automatic fallback while retaining any saved chains. Removing the line does not disable fallback. A sheet without the line keeps quota-only behavior — {"on":["usage-exhausted"]} — so existing sheets advance only on proven exhaustion. Declaring the line makes the sheet policy-enabled in the same sense as saved access facts and chains: required role rows must be present rather than defaulting, and every configured route needs an access fact. The policy declares which terminal outcomes may advance; it never adds an attempt, a provider, a model, an effort, or an apiSpend approval beyond what the saved chain already authorizes. Setup may recommend a broader policy on a new sheet and must show its effect in the candidate; it never edits an existing sheet's policy without explicit acceptance.
The receipt boundary is deterministic: bun scripts/model-policy/pstack-model-policy normalize --sheet <path> --role <role> --parent <p> --lane <n> --attempt <i> --receipt <path> --mode <read-only|isolated-write> [--contract <legacy|strict>] reads the actual runner receipt, checks that its parent/provider/model/effort match the frozen sheet's attempt i on lane n, that its recorded access mode equals the requested --mode, its contract equals --contract (legacy by default), and its recorded apiSpend equals the attempt's saved access fact, checks internal consistency (a not-started receipt can only fail in preflight, a preflight failure cannot be started, postprocess requires a clean exit), and prints the policy AttemptOutcome (plus the receipt path as evidence) or exits nonzero. Agents never reimplement the mapping. The total mapping over receipt statuses:
| Receipt status | Policy outcome | Gate |
|---|---|---|
complete |
complete |
— |
needs-parent-operation |
needs-parent-operation |
schema 2 delivered final response with one validated handoff; transport success does not finish the task |
usage-exhausted |
usage-exhausted |
— |
unavailable-cli, unavailable-model, unauthenticated |
route-unavailable |
— |
child-failed, malformed-output |
terminal-failure |
direct child settled (recorded exitCode or signal) and terminalSuccess: false recorded by the runner |
timed-out |
deadline-exceeded |
receipt carries an explicit timeoutMs and a settled owned process |
cancelled, billing-policy-blocked, unsupported-capability |
failed |
always terminal; never advances |
| provider-owned permission denial | permission-blocked |
verified concrete permission cause only; unknown and safety causes stop |
| any started failure with no settled child evidence (launcher/programming catch, including a missing quota adapter) | failed |
not a backend failure |
any started or unknown-start nonquota failure with no recorded terminalSuccess: false assessment |
failed |
missing evidence cannot authorize broader recovery; proved-not-started preflight route or deadline failures need no workload-success assessment |
non-handoff failure with terminalSuccess: true |
failed |
a valid final success is never replayed as a failure |
timeoutMs on a receipt records only an explicit --timeout deadline; absent and null never qualify. A terminalSuccess receipt flag records the runner's verdict on whether a provider-protocol final success (a real terminal success envelope, not a bare is_error:false object) was observed even when the process exited nonzero or stderr disagreed. It vetoes replay as a failure. A delivered handoff can have transport success while its task remains open for the parent operation and any authorized same-route continuation. A completed worker whose output reports failing project tests is completed work, not a backend failure. Live silence is never terminal: without a terminal receipt there is no event and no fallback.
Missing executables and failed authentication/model preflights can produce route-unavailable. After a workload starts, unproven authentication or model wording in stdout or stderr is a child-failed outcome, governed by terminal-failure; generated task prose and quoted logs cannot authorize a route-only fallback. Source-backed quota classification and terminal-success precedence still apply.
An explicit inspection.state: "unsafe" vetoes continuation regardless of access mode or whether the workload started. The started-writer rule determines when inspection is required; it never overrides an unsafe verdict already recorded by the parent.
Native lanes have no runner receipt. A native event may be recorded only from explicit host terminal metadata (for example a structured tool-envelope capacity failure); there is no helper that verifies host-side records, so the parent attests them and normalize refuses native aliases and native-routed descriptors rather than parsing generated prose.
Writer inspection: an isolated-write attempt whose receipt cannot prove processStarted: false returns inspect instead of advancing, because external side effects cannot be ruled out. The parent then autonomously inspects the diff, output, and tool effects — never a user prompt — and records inspection: {"state":"clear","evidenceRef":"<what was inspected>"} or {"state":"unsafe","evidenceRef":"<reason>"} on the event before calling next again. clear permits only the next authorized descriptor; unsafe stops the lane; a missing or malformed inspection cannot bypass the check and evidenceRef is the parent's attestation, not something the helper verified. An approved later writer attempt runs in a fresh worktree from a reviewed immutable continuation snapshot that preserves completed safe work; the failed worktree and receipt are preserved untouched and writers never overlap.
Same-route continuation is separate from provider fallback. A sheet may opt in with # continuation: {"on":["permission","handoff"],"max":2}. The only causes are a verified concrete permission interruption and a delivered handoff whose matching parent operation completed. max bounds total executions of the same descriptor from 1 through 4; sheets without this line keep one execution. Safety guards, authentication and model failures, spending restrictions, unknown tool denial, cancellation, a completed task, and unsupported strict capability cannot use this policy. Neither policy changes model, effort, access, contract or saved apiSpend.
Before each continuation, including the first, the parent records a clear inspection of partial files and external side effects, the concrete corrected blockage, a reviewed immutable snapshot reference and SHA-256 digest, evidence that old writers and descendants stopped, and a prepared fresh execution ID, workspace, output and receipt path. The recovery object on the blocked event carries correction, blockageId, snapshotRef, snapshotDigest, partialWorkRef, sideEffectsRef, stoppedWritersRef, and nextExecution. execution on the event names the prior run. The helper checks the prepared nextExecution against the frozen descriptor, access, spending and contract and refuses reused or aliased identities. The next event must match that identity derived from its runner receipt or explicit native host metadata. A repeated unresolved blockage or unchanged correction cannot automatically replay. The continue decision returns the prepared execution identity; it never chooses another provider.
Every registered provider implements a typed quota adapter (scripts/runner/provider-failure.ts): the registry is total over PROVIDERS, so a new provider without an adapter fails compilation, and a runtime lookup with no implementation throws an explicit error instead of silently disabling fallback. usage-exhausted is emitted only for these implemented external protocols:
- Claude: the last stdout result envelope — or the trusted terminal envelope behind a typed parse error — carries
type: "result",is_error: true,terminal_reason: "api_error",api_error_status: 429, and aresultstring beginning with a canonical rejected-limit diagnostic:You've hit your session limit,You've hit your weekly limit,You've hit your Opus limit,You've hit your Sonnet limit,You've hit your Fable limit,You've hit your usage credit limit,You've hit your usage limit,You've hit your limit,You've hit your team's shared budget,You've hit your monthly spend limit,You've hit your org's monthly spend limit,You've hit your org's monthly usage limit, orYou're out of usage credits. The diagnostic must end the message or continue at the·separator the composer appends; arbitrary continuations fail closed. The complete errored api_error frame is required:subtype: "success"alone or a bare 429 never classify, and a later successful result supersedes an earlier error. - Codex: the final stdout protocol outcome is a terminal
turn.failed.error.messageor unrecoverableerror.messagebeginning with a pinned canonical diagnostic:You've hit your usage limit,Your workspace is out of credits.,You hit your spend cap set, orQuota exceeded. Check your plan and billing details.The diagnostic must end the message or continue at.or. Only the provenstream disconnected after retries:retry wrapper may precede it; anAuthentication required:, rate-limit, model, or any other unproven{context}:prefix never exposes a diagnostic. A plan-upgrade/UsageNotIncluded message is an entitlement failure, not exhaustion. Stderr cannot override a later successful stdoutturn.completed. Codex output acceptance also requires a completed final turn with a nonempty final agent message, valid JSONL objects, and consistent available thread and turn framing. Earlier text or completion cannot supply a later unfinished turn. Intermediate errors can recover when a later turn completes. The last protocol terminal outcome remains independent of output acceptance and process exit, so completed work still vetoes replay when those checks reject it. - Grok: the last terminal JSON record has
type: "result",subtype: "error_during_execution",is_error: true, and anerrorsarray containing exactlyYou’ve reached your free Grok Build usage limit for now. Get SuperGrok for much higher limits, or try again later: https://grok.com/supergrok?referrer=grok-build. A later terminal success supersedes an earlier error. - Devin: a nonzero exit plus exactly one stderr
Error:frame whose top-level Display isQuota exhausted: <canonical>or a bare canonical cap sentence —You've reached your monthly usage limit. Wait for the limit to reset next month.,Your organization has reached its monthly usage limit. Ask an account admin to raise it, or wait for the limit to reset next month., orusage quota has been exhausted. AQuota exhausted:prefix with unproven detail, a canonical sentence nested inside anotherError:Display (authentication, rate-limit, transport), more than oneError:frame,Rate limited:/Authentication required:/An admin paused usage on your accountshapes, model errors, and agent stdout prose all fail closed. A trusted successful terminal envelope vetoes the stderr diagnostic; Devin has no proven stdout result protocol — the ATIF transcript is a separate export file — so an envelope can only veto, never prove quota, and there is no postprocess quota path. - OpenCode: no proven quota diagnostic is mapped. All errors remain unknown to the quota classifier. A shared transcript parser detects terminal success; a generic 429 or quota words never authorize fallback.
- Cursor: a nonzero exit, no successful stdout result envelope, and exactly one stderr
<Class>Error:frame that isActionRequiredError:whose message beginsYou've hit your usage limitat a separator boundary (end of line, space, or period). Usage-limit, rate-limit, login, payment, and pro-only errors share the ActionRequiredError class, so the class alone is not proof; multiple error frames are contradictory and fail closed, and ANSI styling/CRLF endings are normalized on the frame only. A trusted successful terminal envelope vetoes; errored envelopes carry no proven quota channel. Do not invent structured error codes the CLI does not emit. - Antigravity: no canonical terminal quota envelope is proven. Its adapter returns UNKNOWN even for quota-sounding text or HTTP 429; no
usage-exhaustedreceipt is emitted until captured protocol evidence supports one. A finalSUCCESSwith a nonempty response and nodenied_actionsstill takes precedence over a conflicting exit code so the completed attempt is never replayed.
Diagnostics hidden behind unresolved build constants, entitlement or seat-tier messages, unproven error shapes, and every failure outside these contracts are not quota: they get the normal receipt status and saved-policy handling above, so an authorized broad policy can still recover them while quota-only sheets treat them as terminal. A native lane may classify only an explicit capacity failure from the parent host's structured tool envelope. Never infer exhaustion from model prose, generated output, or a human-readable transcript. Generic 429 or 403 errors, resource_exhausted, authentication or model-access failures, task failures, timeouts, malformed output, and unrecognized provider shapes are not exhaustion; cancellations and billing-policy blocks stay terminal failed under every policy.
Classification evidence comes from read-only inspection of installed first-party artifacts and upstream sources; no credential files were read. "Observed" means exercised live or captured from a real run; "derived" means read from source/artifacts but not yet captured live.
| Provider | Evidence | Observed vs derived |
|---|---|---|
| Claude | ~/.local/share/claude/versions/2.1.281 SHA256 a922981f6f3b55a251ef9f9dbaa0621a5f99cbcb5ca67f8a797476ccfc83f626; the composer renders You've hit your ${name}${tail} and You're out of usage credits${tail} with tails empty or beginning ·; the canonical name set above is source-proven. |
Verified against a real captured exhaustion: {"type":"result","subtype":"success","is_error":true,"api_error_status":429,"terminal_reason":"api_error","result":"You've hit your session limit · resets 4pm (America/New_York)"} at exit 1. |
| Devin | ~/.local/share/devin/cli/_versions/current/bin/devin SHA256 f3fb3868c38c83826951ce71b803d5ad7d7d402eaa56f6dc9bca7a14cbfa5de7: error Display templates carry the exact Quota exhausted: prefix distinct from Rate limited: and Authentication required: , and the cap sentences appear verbatim in the binary. |
Source-derived, not captured live; only the Error: <display> stderr framing was exercised (via Error: Unknown model: ...). |
| Cursor | ~/.local/share/cursor-agent/versions/2026.09.23-86fc751/ index.js SHA256 6daeddfe3327a7efd21f1e69ef7b970ad90869ca785e2089e1d32f4de2de62f6 and 711.index.js SHA256 141bae738b2128eba6a5f5e468f41ff16c5d5aaaf7a7c5407719221274b0c95b: FREE_USER_USAGE_LIMIT (9) and PRO_USER_USAGE_LIMIT (10) map into ActionRequiredError alongside rate-limit, login, payment, and pro-only errors; the headless catch prints String(error) to stderr and exits 1; success writes {type:"result",subtype:"success",is_error:false}. |
Source-derived, not captured live; the displayed cap form is corroborated by Cursor forum reports. |
| Codex | codex-rs/exec/src/exec_events.rs @f8c6026c (terminal turn.failed/error events carry message only — no machine code) and codex-rs/protocol/src/error.rs @9d8de196 (UsageLimitReached/QuotaExceeded Display strings; UsageNotIncluded is an entitlement gate, RateLimitExceeded retryable, ServerOverloaded/SessionBudgetExceeded not account quota). |
Source-derived. |
| Grok | xai-org/grok-build @f0e3be11 crates/codegen/xai-grok-shell/src/sampling/error.rs (backend subscription:free-usage-exhausted maps uniquely to the canonical message) and crates/codegen/xai-grok-pager/src/headless/reducer/messages/mod.rs (the terminal {type:"result",subtype:"error_during_execution",is_error:true,errors:[...]} shape). |
Source-derived. |
New provider or vendor output shapes fail closed until their own evidence lands.
The receipt's additive failurePhase (preflight, invocation, postprocess) and processStarted fields record how far the attempt got. processStarted: false proves no model or assigned workload invocation launched; authentication preflight may already have invoked the provider CLI. Anything else means the workload may have begun.
Before replacing a failed worker, publish a brief progress notice in user-visible message text, not only in private thinking or tool logs. The notice must name the exact failed descriptor, the observed cause, and the next approved descriptor or action. For a writer, announce inspection and preservation first, then announce continuation after a clear verdict. Do not ask the user to select a replacement or approve an already authorized backup. Keep unrelated healthy lanes running. Summarize the observed cause without copying sensitive logs.
The parent loop, applied per seat through next:
- Call
nextwith the frozen sheet and current lane state. Launch only itslaunchdecision, with a fresh output/receipt path and the savedapiSpendpolicy.stopends the seat;inspectpauses a writer for review. - On success, retain the evidence and finish the seat.
- On a terminal outcome the sheet's saved policy authorizes, retain the event (normalize the actual receipt through the boundary above; native lanes need explicit host terminal metadata), add its provider to the run's exhausted groups only for
usage-exhausted, and callnextagain. The helper may skip an attempt only because that provider group is already exhausted, and it enforces the skipped route's authorization before skipping. Chains remain finite at three descriptors; an explicitly declared continuation policy can allow multiple bounded executions of one descriptor. - On any outcome the policy does not authorize, retain the evidence and apply the calling skill's existing dropout rule. Do not advance to a fallback.
completeends the seat;failedis always terminal. - When the chain is exhausted, checkpoint only the blocked lane; unrelated healthy lanes and independent parent work continue.
- Report the configured primary, every attempted or exhaustion-skipped descriptor and its observed cause (for example "authentication check failed" — describe only what the receipt shows, never invented account state), the actual successful descriptor, and actual provider diversity. A recovered success supersedes earlier attempt errors in the final report. Never describe a fallback's output as the primary's.
Writer lanes are stricter: an isolated-write attempt whose receipt cannot prove processStarted: false stops for autonomous parent inspection — never a failure-time permission question — because external side effects cannot be ruled out. A proved-not-started writer can advance directly; a started writer continues automatically only after the parent records inspection.state: "clear" with an evidenceRef covering the diff, output, and tool effects. An unsafe verdict stops the lane; missing inspection keeps it in inspect and cannot advance. Each later attempt is a fresh writer in a new worktree from a reviewed immutable continuation snapshot that preserves completed safe work, and every failed worktree and receipt is retained untouched. Read-only attempts still need fresh exclusive output and receipt paths but may reuse the checkout.
A fallback not present in the confirmed sheet requires user approval through setup; nothing searches for a closer model, retries in place, or lets a child reroute itself. If the parent account itself exhausts, the controller cannot recover itself; checkpoint and report instead of pretending worker fallback keeps the parent alive.
Provider-observed facts (provenance: "provider") do not authorize dispatch. Binding funding and API-spend choices require operator confirmation and provenance: "user"; setup retains provider plan details as advisory metadata.
The runner accepts --api-spend <deny|approved> and records it in the receipt. approved explicitly authorizes a metered API route. deny performs bounded route checks and writes a billing-policy-blocked receipt (exit 78, failurePhase: "preflight", processStarted: false) when a known API route is present or required subscription-auth evidence cannot be established. Every external attempt from a policy-enabled sheet passes the exact saved value; omission preserves legacy behavior for preexisting providers only on an untouched sheet with no access metadata and no chains. OpenCode has no legacy exemption.
The bounded check covers known environment inputs without printing or persisting their values: nonblank Claude API/auth variables, OPENAI_API_KEY, XAI_API_KEY, GROK_CODE_XAI_API_KEY, DEVIN_API_KEY, and CURSOR_API_KEY, DEVIN_API_URL and CURSOR_API_ENDPOINT route overrides, plus enabled Claude Bedrock, Vertex, Foundry, base-URL, and custom-header controls. Claude does not inspect auth-status method/provider fields; its billing route remains unverified. Codex uses the exact known codex login status form and gives API evidence precedence over ChatGPT login evidence. Devin and Cursor have environment-guard coverage plus their ordinary account login checks and isolated runner configuration. Grok is blocked under deny: its models login banner cannot establish subscription routing because per-model BYOK takes precedence over OAuth. Claude uses an empty settings-source list for invocation under deny, preventing user/project/local settings from injecting API credentials; managed policies still apply. Ordinary OpenCode native authentication blocks deny and omitted legacy policy before CLI startup because subscription routing is unproven. The explicit ChatGPT subscription mode is defined in Optional OpenCode models. The guard reads no credential files and changes no ambient authentication.
This is a narrow route guard, not a zero-charge promise. Provider-managed on-demand credits or overage can still apply under OAuth, and unsupported authentication arrangements can remain unknown. Use the provider account's billing controls for hard limits.