Adds transport.draft_delivery (deliver or refuse; default deliver) to the CLI config store, the daemon's settings store and the TUI config model, with OPENRIG_TRANSPORT_DRAFT_DELIVERY taken first when it is exactly deliver or refuse. The daemon reads it at each send that meets a person's draft: only a readable refuse opts out, and a value or read that fails falls back to deliver. Under deliver, SessionTransport pastes the message at the current cursor into the draft and sends Enter, so the combined input is submitted. Under refuse, the send is refused as before and a work notice is retried once the prompt is clear. Menus, permission prompts and hook-reported requests still refuse, a per-seat typing guard still holds, and managed compaction still waits for an empty prompt. rig config help describes both values and that a draft OpenRig can't detect isn't mentioned in the result.
Adds a relay connector to the gateway: an HTTP loopback transport to an operator-owned bridge process, configured per connector from an absolute secrets file (OPENRIG_RELAY_URL, OPENRIG_RELAY_OUTBOUND_TOKEN, OPENRIG_RELAY_INBOUND_TOKEN), with no environment fallback. Outbound, each decision freezes its complete relayEnvelope on its DispatchBuffer entry before the first attempt and posts it whole, once per attempt, with deliveryId set to the decision ID; a 2xx records relay-owner-notification-accepted and drains the entry, a failure records relay-owner-notification-transport-failed and keeps it for replay of the same bytes, and a stop mid-attempt records neither. Inbound, POST /api/gateway/relay/inbound requires the connector's bearer token from a direct loopback socket peer (forwarding headers are ignored), attributes the reply by sender.handle against that connector's registered people, and commits the answer or resolve with its audit record through the Mission Control write contract, deduplicating an exact replay of the same eventId. Inbound size limits are a documented protocol contract, outbound references are fixed-width digests that the inbound contract accepts whole, and a ledger whose state can't be read returns a scoped retryable 503. Readiness warns, rather than going unready, on group or other bits on a readable secrets file and on one token used for both directions within a connector. docs/reference/gateway-relay.md describes the topology, custody, configuration and wire protocol.
An external (non-tmux) self-attach resolves its session name in SelfAttachService.resolveContext. The explicit name (an external_cli context's sessionName, or else --display-name) is kept exactly when it already contains @ or is already saved for the same node in sessions; any other explicit bare name becomes name@rig. With no explicit name, the derived default stays logicalId-with-dashes@rig, as before. A tmux context passes through unchanged. The attach command's --display-name help says new bare names gain @rig. A member already bound under a bare name keeps it on an ordinary re-attach, and so stays unaddressable by the queue until an explicit repair.
A whole-file rig config reset now names the keys whose effective value still comes from an environment variable, and keeps the existing message when there are none. If reading the settings back after the reset fails, the reset is still reported as successful (exit 0) and the read-back problem is noted on stderr. Single-key reset is unchanged. Contributed by dajiaohuang; reported in #2528.
rig acp serve runs OpenRig as an ACP agent over stdio. A prompt becomes a queue row to the receiver named by --to, attributed to the caller (--as, the seat environment, or the default person) with the session's follow-up lineage in its body; the turn follows the row and its handoffs, including handoff-and-complete's done/handed_off_to form, and ends with the team's result, naming the closer from the row's closing transition. The row ID is chosen before the create: when a create gets no answer, a readback that finds the row carries on, and one that doesn't keeps the ID as unconfirmed in the session's lineage; an answered 4xx is a plain refusal and a 5xx stays unknown. The JSON-RPC peer splits into the existing client end and a new stream end over stdin and stdout, with no protocol SDK dependency. The queue is the only store, and a 2 s poll follows open turns.
The terminal broker's attach snapshot and scroll-back window now request tmux captures with escape sequences, so colour, bold and dim survive the first paint. The painter keeps tmux's carried styling across rows instead of resetting each row, resets once before clearing the screen and once after the last painted row, and replays the styling of rows above the first painted row when a height crop or the scroll-back window drops them. The captures feed only the websocket painter; activity, input-box and prompt detection keep their own plain captures. Reported by niklasbaldi in #2598.
daemon-http-clients.md no longer says a token never gets past the browser boundary's 403 refusals. A valid configured bearer token in the Authorization header can pass the target-name check for a well-formed but unaccepted Host; it cannot pass a malformed Host or a refused Origin. The sentence links Browser access for the full rules. Docs only. Contributed by dajiaohuang; refs #2486, where the correction was reported.
bin-wrapper enables Node's compile cache except for a bare invocation with stdin or stdout piped or redirected, which only prints usage. That case loaded the command tree only to print usage, and with a cold cache the deferred cache writes near exit could push it past a caller's deadline after the usage was already printed. Any script argument, including piped --help and --version, and a bare run with both streams on a terminal, keep the existing cache attempt, matching the front door's own predicate. A new bin-wrapper test covers the piped and terminal cases. Contributed by seanmcn; reported in #2642.
rig provider switch --account-home and --clear-account-home select or clear an existing Pi seat's account home through a confirmed successor with a fresh conversation and handover; an unconfirmed or failed switch leaves the old account and state intact, and the selection survives restart, resume, restore and daemon restart (migration 151 keeps switch history and creates or copies no credentials). The chosen directory must already hold a readable auth.json, which the runner links into the seat's managed agent directory; env-only accounts and wrappers that create credentials later are outside this path, and an existing seat-local login or unmanaged auth link is preserved through the confirmed switch. Selected accounts load a Node preload that points only their auth-lock operations at the chosen account's auth.json.lock pathname, even when that auth.json is a symlink, and removes its own NODE_OPTIONS entry before Pi starts tools; outside the managed target it does nothing. A Node-hosted launch wrapper that execs Pi can consume the preload, so that Pi gets no shared-auth lock and keeps its native per-seat lock; compiled Pi and wrappers that discard NODE_OPTIONS are likewise not covered. Contributed by seanmcn; requested in #2629.
leavePaneMode passes the seat's session name to cancelPaneMode, as the text and Enter writes do. The guard resolves the seat and writes to its bound pane; the mode reads stay on the bound pane. Before this, the cancel addressed the raw pane ID, and when a stale binding in another rig named the same reused ID, resolveGuardTarget matched two bindings and refused the cancel (guard_target_unknown), so the send failed pane_in_mode with no text sent. A new session-transport cell covers the stale same-pane binding, asserting delivery and submission, the cancel's target, the reads on the pane, and resolveGuardTarget on both forms. Not changed: reconciling stale bindings, and a person scrolling again between leaving the mode and the Enter.
Claude kernel and team launches pass the named policy kernel or team to the PreToolUse hook instead of the base64 policy, shrinking the argument from 988 to 6 characters for kernel seats and from 1,640 to 4 for team seats. The three policy lists move unchanged into a shared CommonJS asset with TypeScript declarations, read by both the launcher and the hook, so every allow and ask decision is unchanged; legacy encoded arguments still decode, and the missing-hook fallback is kept. rig-spec.md documents that named-policy sessions read the installed lists on each later Bash hook call while native inline allowances stay as launched, and that rolling back in place to a pre-name hook gives no hook decision, so the lifecycle asks can disappear under the native Bash(rig:*) allowance until affected Claude seats are relaunched. Contributed by seanmcn; requested in #2639.
Managed Codex seats now pass Codex -c allow_login_shell=false, so tool commands run in a non-login shell and inherit the environment their pane's shell set up, OpenRig's command folder included; plain rig works however OpenRig was installed, with a Codex that supports allow_login_shell. Inherited setup isn't re-run for each command, so setup that lives only in login or rc files, and shell functions and aliases, may be missing from tool commands, and a model request for login: true gets Codex's own refusal. rig config set launch.codex_login_shell true omits OpenRig's override, deferring to Codex's own setting, at each seat's next launch; a restart in place keeps the running choice. Fresh launch, fork, resume, legacy resume, restart in place and the launch-wrapper path each pass the setting exactly once, or not at all when it's on. getting-started.md and the config help describe the trade-off.
When OS policy prevents a reliable process or port check, daemon startup now reports the uncertainty instead of treating a blocked probe as proof that an address is free. Missing state and preflight cases warn. The auto-start stop is narrowed to the shared PID path for a reused or uninspectable PID. Stop refuses unknown process liveness before probing or signalling and preserves the state file. A denied signal probe yields unknown even when the process state is readable, and a confirmed zombie remains dead. A blocked explicit non-loopback OPENRIG_URL stays on the blocked-status path instead of starting a local daemon (rig up and rig start); loopback targets, including localhost, [::1] and 127.x addresses, keep the existing startup behaviour. Refs #275 and #1650. Contributed by dajiaohuang.
Adds --task-destination <member@rig> to rig mcp serve --http --grant-service. For an external caller, rig_queue_create's destination becomes optional; an omitted destination takes the operator-configured receiver. The receiver comes only from that flag, never from caller input, topology, names or history. It is checked against the request's grant with the same team predicate as an explicit destination, before any lookup, and re-resolved on each omitted call to exactly one existing local node; a missing, ambiguous, removed or out-of-grant receiver returns task_destination_unavailable with no write. Explicit destinations, and local and member calls, keep their existing behaviour and still require a destination where they did. The external instructions now teach the create, show, revise and artifact loop. The new option is refused only when misused: it needs --http --grant-service, and takes one local member@rig seat.
Adds an opt-in on_restore policy to resources.skills[] entries in agent specs. keep_saved, the default and what an absent policy means, keeps #2337's behaviour: a restored seat keeps its saved skill copy. follow_source refreshes the seat's copy of that skill from its source when the seat is restored, for Claude and Codex seats, keeping the native conversation. Copies edited, linked or not owned by OpenRig in the seat keep their existing protections. An unrecognized on_restore value adds an advisory that names the resource, the value and the two supported spellings, and is normalized to keep_saved rather than invalidating the spec. On a no-op restore a keep_saved skill reads only the projection record, and the following bookkeeping is best effort, so it can't fail a launch. Documented in docs/reference/agent-spec.md. Requested by seanmcn in #2625.
Kept Herdr viewer journal entries and follow-view saved tiles store the whole tile attach argv. #2614 changed the writable tile script, and #2464 changed both scripts before it, so views kept from 0.7.5-0.7.9 failed the strict comparison after upgrading: a kept viewer read unconfirmed and a kept follow view stopped adding and closing tiles. earlierTileAttachment accepts a kept argv only when it exactly equals what 0.7.9's or 0.7.5-0.7.8's generator makes for the current session, tmux, server and recorded read-only intent; the entry then moves to the current command. A different target, a changed read-only intent, or any other script (including one sharing the old prefix) still mismatches as before. The earlier generators live in src/domain/terminal/earlier-tile-attach.ts, shared with the tests. The approach follows natefaerber's #2467. No new refusal, error code or setting.
Makes the daemon test fixtures hold on macOS paths and under startup contention, without weakening any assertion: readiness waits instead of fixed sleeps, temp-root resolution through the real path, a script-file shell for long commands, serialized mutations where tests raced, a long-path command, and the shared hermetic test setup. The refocus-overlap cell still asserts the lost pendingOn race, now through a deterministic handshake (the time budget only scales timeouts). The existing Linux CI job that covers /proc seat environments also runs pi-shell-launch-native.test.ts, and the Linux long-command session tokens stay within their limit. Test and setup only; no product code changes. Reported by seanmcn in #2627.
On the first inherited tmux server selection with persistence on, tmux-server-selection creates the home directory (recursively, best effort) before writing the record, so the recorded home is the canonical path a later boot or a later switch to isolation reads it back by, even when the home sits under a symlinked parent. If the home can't be created, inheritance carries on without it as before; inheritance has no writable-home prerequisite. Homes already recorded are unchanged. Reported by seanmcn in #2632. No new refusal, error code or setting.
For You cards for local asks with humanQuestions show the questions with the card's existing controls: one option per question, or an answer in the person's own words, recorded through POST /api/mission-control/action with the ask's revision, then sent once every question has an answer. The card names who OpenRig records as answering (human@host (unattributed), as the TUI does). Live attention keeps priority in the 50-item window, with answered receipts using spare room, and saved session snapshots are bounded with it. A receipt says "answered and returned" only on the answer outcome or reply evidence; a plain close shows as done with the recorded choices, and no closing-note text is read as an answer. Finished asks without questions move to Shipped again. The shared action hook keeps remote_action_failed detail and status. Cards from a paired remote host keep the TUI hint; CHAT, asks without questions and runner permission asks are unchanged.
ROADMAP.md's released section moves from 0.7.8 to 0.7.9: the heading, the release link, and the published 0.7.9 notes' first two sentences with "OpenRig 0.7.9" written as "it", as the 0.7.8 update did. The pointer to the release notes for what shipped, known issues, upgrade instructions and contributor credits is unchanged. Text only.
The two unverified-custody messages a manual close gets (a successor named on another host, or no local row with the source's lineage) now name the route that makes the handoff verifiable: rig queue handoff or handoff-and-complete with --to <seat@rig>, and --host <registered-host> for a successor on another host. That route creates the successor there first and closes with the derived key the stuck sweep already trusts. The sweep, its custody rule and the CLI command definition are unchanged: a registered-host name alone is still not proof that the work was handed on. No new refusal, error code or setting.
Adds an optional member-level launch_wrapper for local built-in Pi and Codex seats: an argv prefix applied at native launch and carried through restore, handover, restart and runtime switch (migration 150 stores it). For Pi the wrapper wraps the child process the runner spawns, inside the unwrapped runner; flat specs carry the field. Once selected, a wrapper is never silently skipped. rig-spec.md says retention applies to existing seats, not newly imported nodes. Native cells cover the wrapped paths with an rc PATH export, a readiness marker and wrapper execution markers. Sandbox, memory and tooling remain the operator's choice. Reported by smota in D#2328.
drainPendingWakeIntents skips a failed or indeterminate wake intent while its seat has no receiver: the destination is unroutable, or the seat's current session, resolved through physicalSessionName, is exited or detached. The check runs at every claim, including grouped and correlated claims, so a seat that comes back during the pass gets its later wakes, and only notifications nothing in the pass attempted are counted as waiting. A never-tried intent always gets its first attempt; running, unknown and non-pane-bound seats take the old path, and an error in the check fails open. The skipped rows stay pending and owed, with no write, and the start-up log line adds waiting=N and names the seats. The wake ladder and stuck sweep are unchanged. No new refusal, error code or setting.
A timed-out or unreadable rig stream emit can still land late. Its error now names the retry that can't add a second item: with --id, repeat the same emit with that --id (the daemon keeps one item per stream item ID) or check first with rig stream show <id>; without --id, check with rig stream list --source <recorded seat> --since <five minutes before the send>, using the seat the daemon records, and pass a stable --id on later emits. queue create is unchanged; its existing --id already makes a retry safe, and the openrig-user skill now says so for both in one paragraph (skill digest updated). No new refusal, error code or setting.
tileAttachArgv chose ignore-size for a writable tile whenever another normal client was attached to the session, and under window-size latest such a tile could never size the pane, so the pane kept an older client's dimensions. Writable initial, fallback and replacement attaches, and the writable recovery command, now attach without ignore-size; read-only attaches keep -r and are byte-identical to before. This covers writable rig, pod and seat views, saved writable members through composeView, and interactive task viewers, locally, over inherited SSH and through the remote helper. The tmux control client's no-output,ignore-size and the CLI's attach-flag recognizer are unchanged. A real tmux test with a competing older client fails on the previous code (14x18 where 101x32 is expected) and passes now (101x32, then 121x43 on resize), with a larger read-only observer keeping its size. Limits: different simultaneous sizes on one shared pane follow tmux's latest-client rule; clients attached before this change keep their flags until re-attached.
validateAgentSpec adds an advisory for any key under a profile's uses that isn't one of the five known categories, naming them, on the existing advisory channel, so it reaches the person at rig up, restore, retry and rig agent validate. It never makes a spec invalid, and hooks stays a validation error with no second advisory. The advisories for shapes that normalization drops now end "; ignored", and a non-string or blank list entry, which is kept and fails resolution when its profile is selected, now ends "; a member using this profile fails to launch". The agent spec reference says so in one sentence. No new validation error, refusal or setting.
CHANGELOG.md gains the [0.7.9] section from the v0.7.9 tag (ddb9d25b), placed above [0.7.8], with the status line "Published 2026-11-06 as @openrig/[email protected], built from the commit tagged v0.7.9 (ddb9d25b)." Where the tag's section and the published release notes differ, it uses the published wording: the summary (including its credit to the people who reported or contributed), the OpenShell worker section, and the Known limits, including "What we ran end to end under the runner contract". The OpenShell heading is "The OpenShell worker in 0.7.9" so its anchor is unique in the file. Nothing outside [0.7.9] changes; whats-new.md stays as it is, since main's package test pins its heading to main's package version.
A profile's uses.<category> written as a non-blank scalar validated but was silently dropped, so an agent could start without the selected skill. normalizeUses now turns a non-blank string into a one-item list for all five categories, and validation adds an advisory suggesting list syntax. A uses value that isn't an object, a category that isn't an array, and a blank or non-string entry are advisories, not new validation errors, so a malformed unused profile can't stop a spec; arrays reach resolution unchanged. When a profile is selected, its coerced scalar resolves exactly like a one-item list: a resource the pool doesn't hold fails that member's resolution with "not found in resource pool", and a non-string skill ID now returns "has a non-string skill ID" before discovery instead of throwing. The advisories travel with the spec's existing advisory channel. Limits: no trimming of scalar values (as with list entries); unknown uses keys are not yet advisories.
scripts/fixtures/packaged-plugins-last-release.json now names release 0.7.9, its published archive (sha256 668b9305...) and the openrig-core 0.1.25 payload it shipped (f1838261...), byte-equal to a --snapshot of the published npm tarball and to the v0.7.9 tag's plugin tree. openrig-core moves from 0.1.25 to 0.1.26 in both harness manifests: its files changed on main after 0.7.9 (permission-stop descriptor, refocus and the trace script), and check-packaged-plugin-versions.mjs requires a greater version when packaged bytes change, so existing installs don't keep old content. The check fails at 0.1.25 and passes at 0.1.26; nothing else pins 0.1.25. No other file.
The rigs skill's Install step offered only npm install -g @openrig/cli. When npm's global prefix isn't writable (often a system Node with prefix /usr), an installing agent improvised a project-local install, and Codex seats then reported rig: command not found. The step now has the agent first point npm's prefix at a folder the person owns whose bin is on their login PATH (for example npm config set prefix ~/.local when ~/.local/bin is on it), saying first that this changes their npm settings and, if needed, their shell profile; use the Node of a version manager such as nvm instead, since nvm refuses an npm prefix setting; and install inside the project only if neither is possible. It links getting-started's "Before you start" for setting and checking the login PATH. Both copies, skills/rigs/SKILL.md and packages/daemon/assets/skills/rigs/SKILL.md, stay identical. Text only; no code, command or gate. Limits: it makes the project-local fallback rarer but doesn't fix why managed Codex seats lose OpenRig's command folder after one (separate work); not run with a live agent install.
The post-commit notification drain in EventBus read the whole remaining event tail into one array. It now reads ordered SQL pages of at most 500 rows (seq after the cursor, ordered by seq) and keeps paging until a read comes back empty. The per-row cursor, subscriber fanout, nested standalone and envelope emits, and poison recording are unchanged; replayAll(), SSE replay and client buffering, and event retention are untouched. One test commits a tail larger than two pages through real SQLite, with subscriber emits at page boundaries and a malformed row, and checks the same order for two subscribers, one poison notification, no redelivery, an uncapped replayAll(), and that no SQL read returns more than 500 rows. No new refusal, error code or setting. Limits: this bounds rows per query, not bytes; the drain still runs synchronously to quiescence; no latency change on a live install is claimed.
validateAgentSpec already produced non-blocking advisories, but loadSpec dropped them, so only rig agent validate --json and the HTTP route ever showed them. loadSpec now carries them on ResolvedAgentSpec.advisories, rigPreflight turns each base or imported spec's advisory into a warning naming the member, the spec and its path, beside the existing import-collision warning, and those warnings ride the existing preflightWarnings that rig up prints. A recipe-backed fresh restore (launchExistingAgentMember) and a successful first-start retry now return the same advisories through their existing returned warnings, once per member, spec and advisory, without doubling where preflight warnings are already merged. Human rig agent validate prints one agent spec advisory line per item, valid or not; --json is unchanged. No new refusal, error code or setting. Limits: a native resume that uses saved context and doesn't reload a spec shows none; unknown uses keys are not yet advisories (a follow-up after #2585).
docs/reference/telemetry.md gains a boundary: OpenRig's telemetry guarantee ends at the finite HTTP/CLI read envelope, and any projection of it is owned by an external consumer. It describes the separately maintained openrig-telemetry exporter's mapping and limits as that exporter's choices (sampling, observed state intervals, no native execution or trace attribution, downstream freshness), says arbitratedNeedsInput is true, false or null with null meaning no evidence either way, and links the exporter's repository, which documents its installation and supported versions; OpenRig doesn't test or guarantee it. It states OpenRig's own fact that the finite reads are versioned by schemaVersion 1 while the cached rig and node inventory endpoints are operational projections. finite-telemetry.test.ts adds one contract cell through the production telemetry routes: the envelope and limits, decimal IDs above JavaScript's safe-integer range across the archive and active tables, the note-free row shape, an opaque cursor that round-trips, page counters, the cap reason and a retained sequence gap. No exporter code, configuration or tests are included, and no daemon or CLI behaviour changes. Limits: the contract cell pins the envelope strictly, so an additive field under schemaVersion 1 needs the cell updated; the exporter itself wasn't reviewed or run here.
The agent-spec reference described plugin skills as always projected into the seat's skill folder. Since the duplicate-loading fix related to #2426, a Claude seat that selects the vendored openrig-core gets that plugin loaded directly, skills and hooks included, and resolveNodeConfig skips projecting its skills; other selected plugins, Codex seats and non-vendored copies still project. The skills section and the plugin table now say so, and that a seat that didn't select openrig-core doesn't get its hooks this way. Docs only. Limits: one sentence says the daemon loads the plugin through CLAUDE_CODE_PLUGIN_DIRS; the daemon sets that variable and Claude loads the plugin, as the table row says. The direct load depends on a Claude Code version that reads that variable, which refocus-channel.md states for the hooks.
agent-spec.md's fragment section now says a fragment only adds tables to config.toml, so it can't install a named Codex profile in either form: Codex reads a profile from its own <CODEX_HOME>/<name>.config.toml with top-level keys and won't load one while a legacy [profiles.<name>] table remains; it points to creating the file and selecting it with codex_config_profile, and notes that the profile preflight checks it before a launch or restore and names the file for a legacy table. getting-started.md says builtin:yolo drops the whole profile, not only its sandbox and approval settings, and how to keep a profile: full access inside the profile with permission_policy absent, or a workspace-write or read-only codex_permissions envelope, which keeps -p and adds its flags; full access isn't an envelope. rig-spec.md's codex_config_profile row says the same in one sentence. Docs only; no behaviour or contract change.
Podman registers its native /libpod routes only under a version prefix, so the observer's unversioned /libpod container list and inspect calls got the router's plain-text Not Found, which the reader then failed to parse as JSON; an owned delete stopped unconfirmed at the cleanup inventory. The observer now uses the compatible /containers/json?all=true and /containers/<id>/json routes, which Podman serves with or without a prefix and whose Id, Names, Labels and Mounts fields the existing selection already reads. Volume absence no longer comes from a per-volume 404: one /volumes listing must be a 200 with an array of named Volumes and no Warnings, and presence is name membership in it; anything else is environment_cleanup_unconfirmed with the HTTP status. The reader parses JSON only for a 200, and malformed 200 inventory is a typed unconfirmed error. Socket identity, workload and supervisor selection, ownership and lifecycle are unchanged, and nothing is sent to the engine but GETs. Limits: container rows are still checked only for status and array shape, so a 200 of [{}] with an empty volume list would read as absent (unchanged here; Podman emits full records or an error); a thrown cleanup error still omits its stage and dispatch status; this is a snapshot, not an atomic inventory; maintained native owned-delete isn't requalified by this change.
rig exec run --wait installed its SIGINT listener only when polling began, after it had printed that the run had started, so a Ctrl-C landing between the two met the default action and killed the CLI with no exit code (the run itself, in its own session, was never affected). armWait now installs the listener before each path announces the run: a run the row already has, a caller that lost the claim to another, and a fresh start right after the connector is spawned, where the start-wait loop also stops when the waiter leaves. report removes the listener in a finally, and waitForEnd takes the waiter instead of arming its own. A tests-only afterAnnounce seam stalls the CLI just after the announcement; the new cell sends a Ctrl-C to the whole process group into that stall and expects exit 3, the stopped-waiting line and the run completing. Limits: a Ctrl-C before the connector is spawned still meets the default; an exception before report would skip the listener cleanup, though the CLI exits on it; the pre-announcement path is traced at source, not separately exercised; a run that has already ended still exits 0 or 2 by its outcome.
The OpenShell guest's launch builds the Claude child's environment from scratch and copied only ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY, so a supervisor-supplied CA never reached the worker and its TLS failed with SELF_SIGNED_CERT_IN_CHAIN. The same explicit list now also carries NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, REQUESTS_CA_BUNDLE and CURL_CA_BUNDLE, each copied only if the guest's exec environment has it; when they're absent the launch is as before. No broad environment copy, no TLS bypass, no credential, and provider references, argv, latch and lifecycle are unchanged. Limits: NODE_EXTRA_CA_CERTS is the demonstrated repair for Claude; the other three are the CA selectors this exec supplies, kept for the agent's own OpenSSL-based tools, and no record isolates each as needed. This preserves the supplied references; it isn't a claim that the maintained OpenShell worker is qualified end to end (export, finite completion and exact restore are unproven).
resolveCrossHostTarget returns localHost: true only from its self branch, when the qualifier is the daemon's own host ID, and only after the existing check that refuses an explicit --host naming a different host. rig send then leaves opts.host unset for that target, so a persisted remote selection no longer catches a send that names this host; the documented order (explicit --host, then target sugar, then the persisted selection) holds again. The early --context refusal now covers only fan-out, and the remote --context refusal runs after host resolution, so explicit, persisted and @remote sends with --context are still refused before any remote dispatch, now after the local health read. Limits: when the self-host ID can't be read, the qualifier isn't recognized as local and the selected host may still be used, as before; rig capture and rig transcript don't pass the self ID, so @<self> isn't stripped there (unchanged); the tests use a local HTTP fixture, not a real second host.
rig exec runs a queue row's task on Claude Code or Codex through their maintained ACP adapters, detached: a hidden rig exec connector process owns the run and outlives the calling CLI, the run file under $OPENRIG_HOME/acp/runs records its process identity and typed state (requested, started, finished, interrupted), and the queue row keeps custody and acceptance and is never closed by the run. rig exec harnesses and run --plan show the adapter and native versions and how each harness answers. A row gets one run: claims are published once per generation with a create-exclusive hard link, a retry after an uncertain submit returns the existing run, and --again starts another only after it ended. Each run's start and end are separate slots, each published once, so a reconciler and the connector can't both settle the same transition. --wait only polls and survives Ctrl-C; its exit follows the outcome (0 completed, 2 otherwise, 3 if the waiter leaves). A dead or reused process, or a connector that never recorded itself, reads interrupted, outcome unknown. Cancel is acknowledged, not settled; usage and cost carry their source, and unavailable cost reads unknown. rig acp run stays synchronous and adds only a usage line. Limit: the crash and cancel guarantees are rig exec's; rig acp cancel given an exec run's id while it's requested can signal the requester rather than the connector, so use rig exec cancel.
QueueRepository.isRegisteredHumanUpdate(row) is true only for a pending row with human_intent update whose destination resolves to a currently registered person; it reads false for an agent, an unregistered destination or an unavailable registry. The waiting view treats such a row as informational correspondence with no claim backstop, and the stuck sweep skips it as an unclaimed obligation, except when its quiet-hours hold has been orphaned (no live release job), which is still reported as before. The new resolution reason closes only the sweep's own unclaimed-obligation finding, never while the row is quiet-held or has a delivery outcome. A shared recovery for such an update can be relabelled to an undelivered human notice instead of being closed, so a later delivery failure stays visible. Posted and on-Steering rows keep today's paths. This covers the informational-update part of #514, not the rest of it.
With mouse mode on, scrolling a seat's pane puts it in copy mode, and tmux stacks modes: copy mode can sit under run-shell output (view mode), a chooser, customize or clock mode. A single send-keys -X cancel leaves only the top copy or view mode, so a pane with two modes stayed in a mode, and its sends and wakes were refused with pane_in_mode until someone left the mode by hand. cancelPaneMode now runs tmux copy-mode -q, which leaves every mode the pane is in and types nothing on a pane in no mode; when that fails it falls back to send-keys -X cancel, checking the input guard again first, as the Enter fallback does, so input that arrived meanwhile stops it. leavePaneMode reads the result and adds the reason to its refusal. Behaviour change: a send or wake now also leaves a chooser, customize or clock mode a person has open in a seat's pane, where it was refused before; #912 already leaves scrollback.
docs/reference/acp-agents.md adds "Claude Code and Codex, through their ACP adapters", checked on 2026-11-06 with OpenRig 0.7.8 on one small file task per case. It covers the install for @agentclientprotocol/claude-agent-acp 0.101.0 and @agentclientprotocol/codex-acp 2.5.0 and the native versions each runs (Claude through CLAUDE_CODE_EXECUTABLE when set, otherwise the SDK's platform binary, unchecked; installing with --omit=optional leaves that binary out, and without CLAUDE_CODE_EXECUTABLE the adapter then refuses to start rather than falling back to a claude on PATH). Each agent keeps its own permission mode; the settings that make each one ask are the caller's choice, not a recommended default. Claude's reject lets the turn continue, while Codex's only refusal, cancel, ends it. A missing session is refused by both with -32002 or -32603, and neither code alone means a missing session. A completed turn isn't finished work: check the work before closing the row; beyond one observed case, long and background work weren't checked. rig acp cancel was acknowledged in 20-30 ms for both, sent while the agent waited on a permission answer; a cancel mid-generation or mid-tool wasn't timed, and an acknowledgement isn't a settled shutdown or zero further spend. Docs only.
docs/reference/community-harness-examples/dsh/dsh.yaml's first limit now names the two stacks a real dsh 0.2.1-rc.2 has run on with this runner: dsh's Claude route with a Claude Haiku model in 0.7.8's release testing (an allow-once and a reject-once), and dsh's pi-ai OpenRouter chat-completions profile with deepseek/deepseek-v4-flash on OpenRig 0.7.8 (queue work, activity through a quiet tool call, an allow-once and a reject-once, and cancel). In both, the permission answers came from a test answerer registered as a person; dsh's DeepSeek Messages adapter via OpenRouter failed with HTTP 400, cause not established. Its resume limit says what was observed (a real dsh reopened and recalled a conversation with resume added to a copy of the preset; a missing conversation's refusal wasn't run) and resume stays undeclared. harness-runner-contract.md adds a "Since 0.7.8, on one stack" paragraph after its not-shown list. CHANGELOG.md's [0.7.8] Known limits line that said the dsh example was checked only against stand-ins is corrected in place, with the old sentence struck. Docs only.
rig seat set-runtime's handover read the seat's authored recap with authoredRecapResolver(ctx.seatRef), the logical ID (for example dev.impl), while recap folders follow the bound canonical session's member (dev-impl), so a pod seat's switch missed its own recap. It now passes the bound sessionName. A test covers a seat whose logical ID and member differ. Limit: the ordinary handover path still resolves recaps from its own input.seatRef, which already accepts logical aliases; that path is unchanged here.
A queue listing asks findQueueRecovery once per row, and each ask ran json_each over every recovery-tagged row. withRecoveryIndex(db, fn) now opens a scope for one synchronous listing; inside it, the first lookup builds an index with the same json_each, the same recovery tag filter and order and text values only, and later lookups read it, while a compact listing that never asks builds nothing. Outside the scope every lookup is the original per-row query, and nested calls share the scope. QueueRepository.list wraps its existing body. Limits: the index assumes recovery data doesn't change during the listing; nothing a listing calls changes recovery membership, state or its ordering time, although the activity reader it calls can write other events, and a callee made async would simply fall back to the per-row query. Equivalence was traced in source and checked against an offline copy of a real queue; it wasn't measured on a live daemon, and malformed Unicode tag values weren't compared.
The openrig-core refocus hook marks a `rig queue whoami` lookup that ended in ETIMEDOUT and passes `--work-unknown-timed-out` with `--work-unknown`. With `--unresolved-work-advisory` (passed since #2582), trace-to-root.py prints "TRACE GAP — current work node UNKNOWN: queue whoami failed: spawnSync rig ETIMEDOUT" plus the project-level chain, labelled a broad orientation and not evidence of a current mission, and doesn't fail `--check`, so the timeout no longer counts toward "Refocus failed on N consecutive attempts". Non-zero exits, other spawn errors, unreadable answers, no remaining budget and a run without the advisory flag stay strict. A timed-out refocus now clears the pending post-compaction retry, as #2582's advisory does, so a slow daemon isn't queried again on every prompt. The skill digest is regenerated. Refs #2581: the part #2582 left open.
docs/reference/harness-runner-contract.md replaced "No harness has been run end to end under this contract yet, and the native verdict is pending" with what has run natively in 0.7.8's release testing, against a Claude Haiku model: dsh-seat 0.3.0 (dsh 0.2.1-rc.2 over ACP) for launch and first input, queue work, a multiline message with an /abort line as one prompt, working through a 52-second quiet tool interval, cancel by a bare /abort, the same conversation after rig down --snapshot and rig up, and the runner and child gone after the managed down; and the dsh example runner, whose native permission request reached a person, with allow-once taking effect and reject-once not. It also lists what wasn't shown natively: a DeepSeek model, the example runner's own resume, the default permit when nobody can be asked, a typed or Slack answer or a second person's answer, reboot recovery, and the model OpenRig observes. Any other runner, provider or path stays unqualified until it's shown. The status line above now matches. docs/reference/author-contracts.md's dsh/ACP row says the same in one sentence. Docs only.
rig roster show and find can see one seat through more than one route, for example two host aliases. They now build each route's activity (state, reason, sampled time) and compare them. When the routes agree on state and reason, the activity is kept, with the latest time if every route gave a comparable one and no time otherwise. When they disagree, or one route has no activity, the seat reads unknown with the reason "Conflicting activity observations" and no time, whatever the route order. A seat seen through one route reads exactly as before. Only the CLI's roster enrichment changes; the daemon is unchanged.
The openrig-core refocus hook passes a new --unresolved-work-advisory flag to the refocusing trace script. With that flag, when the work basis says typed work did not resolve (no matching node, or a mission or slice that resolves to more than one directory), the trace prints "TRACE GAP — no single current work node" with the reason, then the project-level chain labelled as broad orientation, not evidence of a current mission. The hook can then tell that advisory from an operational trace failure. Without the flag, and for conflicting tags, several fully resolved nodes, or an unreadable queue response, the script stays strict and exits nonzero as before. The skill's edge digest is regenerated. Refs #2581; the separate queue whoami timeout case is unchanged.