For an ask to, or a park on, a registered person with no connector binding, the Feed's detail now reads "Slack: not sent: this person has no connector binding, so they're asked on Steering and in the Feed only", and its route line says "answer on Steering; it wasn't sent to Slack", the wording quiet asks already use. A real Slack receipt still takes precedence, and a Slack-bound person's wording is unchanged. Wording and row selection only, local to the attention route.
routes/attention.ts +16/-5; one test cell.
A rebuild handover's receipt now says what happened. sideEffects.provenanceRecordWritten is measured: commit() reads the node's occupant tenure before and after recording the handover in its own transaction, and reports true when a new handover-kind tenure exists for the reserved generation; a swallowed mint failure reads false. A new sideEffects.departingProcess reports ended-graceful, ended-forced or not-touched, from the in-place termination the successor launch already performs. departingSessionKilled stays false, which is accurate: the tmux session is kept and the pane respawns. The CLI's human lines and the seat-continuity-and-handover skill now say whether the lineage record was written, how the old process ended, and that a bound seat whose record is missing shouldn't be handed over again.
7 files, +125/-20, most of it tests.
An environment seat's startup identity, selected role and context references, and text actions travel to the guest through a hash-checked first-input file, with a bounded initial prompt pointing to it; nothing is typed through the host viewer, so startup text can't reach the host shell if the viewer exits. Readiness is the guest execution's identity, generation and running state; only environment connect execs; the viewer does one live identity and generation lookup (driver 30 s, client 35 s) instead of serial file validation, while launch and the guest latch still validate. A failed owned delete reports its observed stage and error and whether dispatch is unknown, keeping the original intent, claim and same-operation reconciliation. An older guest helper's rejection of first input names the likely outdated image and the rebuild step.
Limits: a same-generation reconciled launch opens no viewer, and a non-text interactive startup action on an environment seat fails without host fallback.
313 lines added and 18 removed across 15 files, about two thirds of them tests.
The dsh example runner reads DSH_SEAT_BIN and DSH_SEAT_HOME, which a member's env: can set (OPENRIG_* stays reserved for OpenRig's own per-launch values and is still read first when exported). It doesn't read dsh's own DSH_HOME: a seat's shell runs the person's startup files after the seat's env:, so an exported DSH_HOME would rewrite their personal dsh profile and skip the uploads-off patch; any inherited DSH_HOME is still replaced with the seat's home. The launch record's producer takes dsh's version from dsh --version, killed with SIGKILL at a 5-second timeout so a wrapper that ignores SIGTERM can't hold the seat's start, falling back to the handshake's version, labelled as such. Not verified: a real dsh's --version output and whether it makes a network call.
Runner +23/-6; README, one preset limit line and the dsh example test.
Any registered person in a shared workspace settles another person's ask as themselves, from the Feed, Steering, the TUI or Slack, and the record keeps who answered whose ask. The first committed answer is the answer on every surface and in either order: a later answer from someone else gets 409 qitem_already_terminal with answeredBy and records nothing, a click honours an answer already committed (on a pending ask or a park's current question), an interrupted close finishes as the first answerer, and a typed reply routed to a person who isn't the asker stays correspondence. A person may have no connector binding (rig gateway human add without --binding): they load through every registry reader, are reached in the Feed and Steering only, get no Slack post, and their asks never read never-posted or undelivered. The terminal's primary-user act is recorded as human@host, unattributed, whoever is registered.
Known limit: a click's option written before its answer row (and, on an ask with several questions, partial clicks completed or closed by another person) can stand beside or inside the closer's answer without the first clicker's authorship.
Source +210/-90, tests +455/-119.
The kernel hook-review reader now recognises a managed Codex seat whose pane reports the launcher shell: OpenRig runs managed Codex through a staged `/bin/sh` script, so the pane command reads `sh` and the reader's `codex` check never passed. For a non-`codex` pane it now requires the existing `verifyCodexPaneProcess` proof before and after the screen capture, with the same pane PID and lineage fingerprint, and keeps every binding, session, pane and dead check; an unverified chain stays unknown and neither raises nor clears an escalation. The direct `codex` path is unchanged. Nothing approves, edits or trusts hooks. The scheduler's tmux dependency gains `getPanePid`. Source proof, not a native pass.
Daemon +14/-5, tests +65.
For You shows Approve only on an ask with no structured questions, or a runner permission request, where a close means permitted. For an ask with questions it says to answer in the TUI (`rig tui`) or tell the agent in Chat, so the request stays open; an ask from a remote host names that host and doesn't suggest Chat. "Has questions" is a non-empty `humanQuestions`, the rule the daemon's attention view already uses, and the card's payload now carries the ask's questions and tags from the start. This is a UI guard only: the action API still accepts approve.
Reported in #2406. Contributed by @w4rc0n. UI +29/-7, tests +86/-1.
The web UI's maintenance notice now renders in its own row above the header rather than over it, so the header's name, host label and Theme control are visible on first visit; dismissing it gives the space back. The notice and its existing dismissal storage move out of `App.tsx` into `UiMaintenanceNotice`, and the shell, explorer, shared detail drawer and proof image viewer take the notice's height into account on phones, and the phone rail's links stay above the bottom navigation and scroll.
Contributed by @w4rc0n. UI +68/-68, tests +38/-6.
The Dashboard's kernel card uses the UI's surface token instead of a fixed white background, so it follows the dark theme and its heading and status text stay readable.
Contributed by @w4rc0n. UI +3/-3, tests +9.
The Topology table no longer auto-resets its page index when its data changes (`autoResetPageIndex: false`). The table has no pagination, and each reset queued a state change that re-rendered, rebuilt the data and reset again, so one keypress in "Filter agents" kept the tab busy. Filtering, clearing and sorting while filtered now settle. A regression test types into the filter and checks that it settles.
Contributed by @w4rc0n. UI +3, tests +44.
A manual `rig compact` whose idle wait expires now reports `map_complete_seat_never_idle` when a completed restore map exists and `preparation_incomplete` when no completed map remains, matching the automatic preparation-expiry path. Other transport failures, the idle requirement, the timeout budget and command delivery are unchanged. HTTP-route tests cover a complete map, an absent map (the published and `.tmp` paths both removed) and a non-timeout refusal.
Credit to Seán McNamara (@seanmcn) for the timeout-reason mapping in his fork PR #3. Enforcer +5/-1, tests +28.
The OpenShell README's actor-attribution example now uses `operator-agent@kernel`, the kernel operator's actual address (as the Slack CLI's default already names it), in place of `operator@kernel`, which isn't a kernel seat. The surrounding instruction to use the actual acting member's address is unchanged.
Docs +1/-1.
Fact-only corrections so the in-repo docs and `--help` match what 0.7.8 ships: the CLI reference gains the `rig environment` section and the `host edit`, `host remove` and `terminal attach` rows; both contracts link the in-tree presets; the guest listener's permission routes, port and host defaults are stated as they are; status labels say what 0.7.8 ships for grants, the answered permission path and `rig environment`, keeping "no harness end to end" and "native verdict pending"; `rig provider switch --help` says a stopped seat isn't relaunched (it prints the `rig launch` that resumes it) and that the switch asks to keep the running effort, using the saved effort when it can't, unless the seat chose its own; `rig host` help says add, edit and remove rewrite hosts.yaml from the known fields, dropping comments and unknown keys; the capability map's verb count reads 91; and `/abort` and the shipped dsh preset's resume behaviour are described as they are. Section 7's text is unchanged.
Docs and help text +70/-28; no behaviour change.
A newly created default kernel starts in `<workspace.root>/kernel` instead of `workspace.root`, through both creation paths (daemon start and the TUI's first setup), so a Claude kernel seat's managed `CLAUDE.md` no longer lands above the default projects folder and isn't loaded by every project seat. If the folder can't be prepared, the kernel falls back to `workspace.root` with a warning; nothing refuses a boot. Existing kernels keep their stored working folder and restored conversations; nothing is moved or cleaned up. The operator role's working-directory line follows the new folder, and `docs/reference/agent-startup-guide.md` explains the optional move for existing users: a fresh launch starts a new native conversation, custom text in a shared `CLAUDE.md` is kept, and step 3 waits until no running rig's seat still works in the old folder, as `rig down` checks.
Reported by @ch-davidriott in #2390.
Daemon +24/-11, guidance and docs +28/-1, tests +59/-6.
An assistant connected through `rig mcp serve --http --grant-service` can now claim, update and park the rows it owns and see its team, with the scope that show, list and create already apply to external callers: claim and update responses go through `projectForExternal`, so a blocker, closure target or waiting detail the caller can't read stays out; an external owner needs both the exact destination and the existing item scope, so a known id outside the grant gets `out_of_scope` and stays unchanged; an external park must name a `blockedOn` the caller can read, so a bare re-park can't restore or reveal the blocker saved at claim; and `rig_team`'s `lastWake` for a hosted member counts only rows in the caller's item scope. Local and token-grant behaviour and the error codes are unchanged; refusals use the existing `out_of_scope`. `docs/reference/hosted-members.md` describes the loop.
MCP server +51/-16, docs +32, tests +329.
Pi's stream heartbeat (#2336, by @porterchild: one 5 s tick, a beat once 5 s pass since the runner's last post, stopping when streaming clears or the runner exits) now posts the runner contract's working beat (section 4): `active` / `liveness` with the open run's `turnId` and `livenessSeconds: 30`, in place of `active` / `streaming`. The Pi runner now posts `UserPromptSubmit` / `accepted` with the next `turnId` for each prompt it gives Pi: a prompt or `/followup` sent into an open run is posted as it's sent, and a prompt sent while idle is posted when its run opens, so a command Pi handles without a run (such as `/model`) opens no turn. `/abort` opens no turn, and OMP is unchanged.
Named limit: after a daemon restart in the middle of a run, the run's beats are ignored until the next prompt.
Pi runner +30/-4, tests +145/-19.
docs/reference/slack-app-setup.md's channel-map section said missed-message recovery covers the default channel only. Since #1536 it recovers the default channel and every mapped channel, except a channel relayed to another machine, as the guide's reconnect-recovery sections already say. The historical 0.6.12 release note is unchanged.
Docs +1/-1. Reported by @ziadeh.
The dsh example runner now posts the runner contract's working beat (section 4): while a turn runs and nothing waits on a person, it posts `active` / `liveness` every 10 s with the turn's `turnId`, `livenessSeconds: 30` and its rising `sequence` and `occurredAt`, and stops at the turn's `Stop` and at dsh's exit. It pauses from a permission ask until the agent has its answer, and posts `active` / `permission_resolved` with the turn and `askId` once no ask waits and none is being submitted, never after a refusal, a cancel, a permit with no ask open, or dsh's exit. The agent's own reports go out only when a beat could.
The conformance check's cases 4b and 7 now count only the non-exit `Stop`s posted after the turn's accepted prompt, ordered as OpenRig orders them (`occurredAt` when it reads as a time, else arrival; then a positive `sequence`), so a runner's idle report from before the turn is no longer a false failure. The check still doesn't validate those two fields' types, and it is stricter than OpenRig for a `Stop` that precedes a later post that arrived first.
Example and check +69/-16, tests +213/-16.
A restore whose snapshot has no saved startup context reaches #2338's existing hook recovery only for a legacy (non-pod) seat whose live runtime still matches the snapshot; a changed runtime or a pod-aware seat gets no new hooks write, and the restore itself is unchanged. Every restore step that uses the seat's directory now takes the launch directory (the snapshot's cwd, else the live node's, as the launcher does): the projected hooks, the startup replay's plan and binding, the legacy resume's managed launch and read policy, and both saved-guidance checks. A snapshot with no cwd therefore no longer writes the context collector or settings into the daemon's own folder, or prepares a resume at the filesystem root. Seats whose snapshot has a cwd are unchanged.
Restore orchestrator +48/-12, tests +232/-6.
The queue list route accepts a sort by last update, and the TUI uses it for JUST FINISHED on Pulse and RECENTLY FINISHED on an agent's page, so those lanes hold the items closed most recently instead of those created most recently. It is a read order only: no queue state, write or delivery path changes, and the default list order is unchanged.
Source +13/-10, tests +33/-8.
The transcript store's forward reader keeps an unfinished line as fragments until a newline arrives, instead of prepending it to each new 64 KiB chunk and splitting the whole result again. A very long captured line is therefore scanned once rather than once per chunk, so `rig transcript --grep` holds the daemon for less time. The lines it yields, and so the grep output, are unchanged.
Transcript store +11/-5, tests +14/-0.
`rig tui`'s Pulse shows each JUST FINISHED row's clock in the TUI's configured time zone (`ui.timezone`), through the existing display-time helper, rather than as UTC with no zone mark. Other Pulse lanes and the stored timestamps are unchanged.
TUI +12/-16, tests +23/-2.
The workspace frontmatter validator reports an opened but unclosed frontmatter block, and a YAML sequence instead of a metadata mapping, as a parse-error gap, including when no workspace kind is selected. An unclosed block is recognized as present rather than silently treated as body text. The report path, gap kinds and HTTP response are unchanged, so `rig workspace validate` now exits 1 for such a file through its existing gap exit status; nothing else gates on it.
Validator +4/-3, tests +50/-0.
`rig scope ... progress` edits only the tasks a reader sees. `--set` no longer changes or counts example checkboxes inside fenced code blocks, and a text found only inside an example takes the existing no-match error, leaving the example untouched. `--add` ignores example headings and rows when it picks a section, and inserts before an unfinished trailing fence rather than inside it. The fence rules match the progress indexer's.
Command +36/-7, tests +42/-0.
`rig down` removes OpenRig's managed blocks from the guidance file of every seat runtime that writes one, now including Pi and omp (AGENTS.md), with the same target used for teardown, live-file protection, exact resume and node inventory. Once a working folder has no live Claude seat of any rig, down also removes OpenRig's own Claude additions there: the collector status line and the relay hook groups in .claude/settings.local.json (matched exactly as launch claims them, keeping other hooks and fields), and .openrig/context-collector.cjs when its bytes still match the shipped asset. Empty .openrig and .claude folders are removed without recursion, and settings.local.json is kept as {}. Edited, unreadable and still-shared files, including targets another live seat reaches through a symlink or hardlink, are kept and reported. An exact resume puts its activity hooks back.
Source +211/-64, tests +248/-8, docs +4/-2.
The /broadcast route adds its unsigned-sender notice only when the broadcast reached at least one recipient, counting sends a recipient holds for later delivery (sent + retained > 0). When every recipient holds the text, the notice begins "Held for delivery" rather than "Delivered"; when it reached nobody, there is no notice. The CLI prints the route's warning as before.
Source +9/-4, tests +31/-0.
A harness runner may post `active` / `liveness` beats while a prompt is outstanding and the agent it started is alive, so a long tool or model call keeps the seat reading working. A beat counts only when it carries the seat's live generation, names the latest accepted turn that hasn't stopped, carries a sequence above every earlier counted post, and the seat has no attention outstanding; otherwise it is ignored, and it never starts or extends a turn or clears attention. After delivering an answer or permit, a runner posts `active` / `permission_resolved`, which clears attention. Section 4 of the runner contract documents both from 0.7.9; section 7 is unchanged. The conformance checker gains the beat cells. A runner that sends no beats behaves as before.
Source +181/-8, contract and conformance kit +100/-2, tests +313/-7.
`rig seat set-runtime`'s refusal for a seat with no bound tmux pane now says what to do: start a stopped seat first, then switch it; for a seat that never started, declare the other runtime for the member in its rig spec and bring the team up again from it, which makes a new generation without the seat's edges, settings or queued work. `rig seat set-runtime --help` says the same. The existing refusal passes one guidance line; nothing changes in how a seat switches or starts.
Source +11/-2 (text and help), tests +6.
`rig queue create`, `handoff` and `handoff-and-complete` lowercase a documented --priority (routine, urgent, critical) given in another case, so `--priority Critical` files as critical. Any other value is kept exactly as given, with one note on stderr that it is outside the documented choices. create still defaults to routine, and a handoff with no --priority still sends none, so it inherits. --tier is unchanged.
Command +14/-3, tests +56/-3.
`rig queue list --state` warns on stderr about any name that isn't a queue state, naming the supported states, then sends the query exactly as before. A mixed filter still returns its known-state matches; an all-unknown filter still returns nothing, and its warning says an empty result doesn't mean an empty queue. Stdout, --json, the empty and default filters and the HTTP API are unchanged.
Command +13/-0, tests +55/-0.
`rig export <rig>` with no -o no longer replaces an existing rig.yaml. It claims the next unused sibling (rig-export.yaml, then rig-export-2.yaml and so on) by creating it exclusively, writes the export there, and prints the path only after the write succeeds. An explicit -o keeps its replacement behaviour, including -o rig.yaml, and filesystem or export failures still exit nonzero.
Command +39/-5, tests +79/-1.
0.7.7 is published, so main's CHANGELOG records it: the [0.7.7] section reads published 2026-11-02 as @openrig/[email protected], built from the commit tagged v0.7.7, and the 0.7.7 status paragraph inside [0.7.8] no longer says it isn't published. [0.7.8] itself stays not yet published. docs/reference/whats-new.md, versions and pins stay as main has them.
CHANGELOG.md +2/-1.
When a person answers a runner's permission ask in words that are neither a choice nor a plain yes or no, the read result (`GET /api/activity/permission-requests/<askId>`) now carries those words, whole, as `said`, beside `permitted` (closed without a choice) or `refused` / `no_allow_offered`. The words come from the row that holds the answer, so a colleague's words count when the ask was routed to them. A plain yes or no keeps today's mapping with no `said`, and words are never read as a no.
The harness runner contract's section 7 gains two passages describing `said` and how a runner delivers it: queued for the agent as an ordinary prompt, after the current turn and behind any prompt already waiting, never as the answer. A runner that drops them still keeps the contract. The dsh example runner and the minimal ACP conformance fixture deliver the outcome as before and queue the words; the conformance check covers it.
10 files: runtime +20/-4; the dsh example runner and the conformance fixture +18/-2; the contract, the dsh README, the conformance README and check +34/-6; tests +61/-7.
The OpenShell guest reader opened `/` before walking to the selected workspace, which a sandbox that grants only the workspace refuses, so a worker never started. It now anchors descriptor traversal at the selected root, with no ancestor opens, and keeps the selected-path symlink checks and `O_NOFOLLOW` reads beneath it.
When a resource is created but projection fails before a generation exists, `rig environment` can now delete that owned resource by its selected, immutable resource id, with no generation or node binding required. Attached resources are refused before any mutation, the one-dispatch journal and dependent cleanup are kept, and the selection is checked again after the driver's final identity call. Uncertain or lost replies still require observed absence.
The example `rig.yaml` gains its required pod label and a pod-local delegation edge, and validates through the normal parser and plan path. The CLI help and the OpenShell README describe cleanup by resource id, `OPENRIG_SESSION_NAME` as attribution rather than authentication, and guest-reachable listener addressing.
Runtime +30/-14, tests +165/-2 across five files. No new error code, dependency, migration or policy.
Carries the 0.7.8 section of `CHANGELOG.md` from `release/0.7.8-prep` (16121548) onto main byte for byte, adding only a "Not yet published" line, plus the 0.7.7 status paragraph from its release source. Neither version is dated or marked shipped; each label changes when that version is published. `docs/reference/whats-new.md`, versions and pins stay as main has them.
CHANGELOG.md +167/-0.
`rig specs ls`, `show` and `preview` accept any --kind. For a value other than rig, agent or workflow, the command prints one stderr warning naming the supported kinds, still queries the daemon with the value URL-encoded, and keeps only the entries whose kind matches exactly. A known kind trusts the daemon's filtered list as before, so `rig up`'s library lookup is unchanged. When a --kind filter matches nothing, `specs ls` prints "No <kind> specs found" instead of the empty-library message. The warning goes only to stderr, so stdout and --json stay parseable.
Command +35/-10, tests +105/-1.
Launching seats that share a working directory made its `CLAUDE.md` and `AGENTS.md` grow by one byte per managed block per launch. When `mergeManagedBlock` replaced a block already in the file, it wrote the result plus a newline even when the result already ended in the previous merge's newline. Now it adds the newline only when it's missing, so a file is the same size from the second merge on. Every byte outside OpenRig's blocks is kept, including blank lines a person left after the last block, so a file that already grew isn't trimmed. A first write, an append and the replaced-id trim are unchanged; a CRLF file is left alone on re-merge, and `rig down` still gives back a file's exact bytes after one start or two.
Source +4/-2 (one expression plus comments), tests +46 new and two reworded comments. Reported in #2372.
A Claude seat's rebuild handover could stop at "Waiting to confirm the new Claude process in this pane" although the new Claude was running, when its argv[0] was spelled through the native installer's symlink (`~/.local/bin/claude`) rather than the versioned file it points to. A managed launch freezes the realpath, and readiness compared a path-shaped argv[0] only with that frozen path; the OS executable path was read only for other row shapes.
Now, when the exact-path match finds nothing for a launch that froze an executable, readiness admits a foreground process whose argv[0] is an absolute path named `claude` and whose OS executable path (on Linux, `/proc/<pid>/exe`) is exactly the frozen file, absolute with no `.` or `..` segments. The process must still name this launch's own conversation, be the only candidate, look the same in two samples under the same pane, and its first match is pinned. Only such a process has its OS path read: an exact launch, run with exactly the frozen path, is proved without it, as before, so a slow path read can't change its pinned fingerprint. Another install's binary, an unreadable or relative OS path, another conversation and a process outside the foreground group are still refused. The `#!` wrapper rule from #2354 is unchanged.
Runtime +25/-8, tests +64/-1. Refs #2341.
When the daemon answers `rig workspace validate` with an error status (400 or above), the command now prints the daemon's message as one line on stderr, falling back to its error code and then to "Validate failed (HTTP <status>)", instead of the raw JSON response. Every error this route returns carries an error code and a message, such as `invalid_workspace_kind` for an unknown `--kind`, so the line is that refusal's own text. Under `--json` the command prints the same JSON as before, and the exit code is still 1. The daemon remains the only place that lists the workspace kinds; the CLI adds no check of its own.
Command +11/-2, tests +81.
The runner conformance check (`docs/reference/runner-conformance/check-runner.mjs`), which runner authors run on their own machines, could falsely fail a correct runner on a busy machine at case 4b's read cells. After a permission read that OpenRig answers with an unlisted status (such as 500) or `activity_grant_stale`, it waited a fixed `scale(4000)` for the agent's outcome, tighter than section 7, which sets no time limit after a read. That wait is now the check's own per-step observation budget, `opts.waitMs` (8 seconds by default, scaled by `--wait-ms`). The unreachable-read cell keeps `scale(10000)`, since a runner may retry a read that got no answer. A dropped or wrong outcome still fails the same exact checks. The test double gains a `--read-retries <n>` mode, and a test cell runs case 4b with it.
Check +3/-2, test double and test +17/-1.
A Pi seat mid-turn now keeps reading `working` through a long, quiet model request (local inference, a slow provider) instead of flipping to idle or unknown after 15 seconds. The Pi runner already had an idle heartbeat; it now also posts an `active` beat while its `streaming` flag says a turn is in flight. The heartbeat timer ticks every 5 seconds and a beat goes out once 5 seconds have passed since the runner's last post, real or beat, so a working hook stays inside the seat-activity oracle's 15-second `HOOK_AUTHORITY_WINDOW_MS` on ordinary on-time ticks. The beat stops when `streaming` clears at the turn's end, an abort or error, and with the runner's exit; frequent real events postpone it, and the idle heartbeat's three-minute gate is unchanged. OMP keeps its no-op timer. `main()` starts the same exported timer the new fake-timer tests drive.
Runner +55/-13, tests +167/-4.
Runner contract section 8 now names ACP's `session/resume` (capability `sessionCapabilities.resume`) beside `session/load`, which is what dsh `0.2.1-rc.2` advertises, and marks open question 1 answered. It also says how a runner tells a missing conversation from one that won't reopen, since an agent may answer one error for both: missing comes only from the agent's own not-found (`-32002`), anything else is unconfirmed, and an agent's `session/list` is positive evidence only. An unlisted conversation isn't missing unless the agent guarantees its list is complete, which dsh doesn't.
The dsh example runner reads the list first when dsh advertises it, following any string cursor unchanged (the empty string included) for up to 50 pages; a non-string cursor, an entry without a string `sessionId`, or a failed list decides nothing. It always tries the reopen: a reopen that works resumes, `-32002` is missing unless the list named the conversation, and any other error, `-32602` included, is unconfirmed. It never starts a new conversation. The dsh README and `dsh.yaml` limits say the same, and the runner keeps its executable mode.
Runner +24/-2, contract and dsh docs +30/-7, tests +82/-1.
A clean install of the CLI couldn't start: bare `rig` and `rig --help` exited 1 with `ERR_MODULE_NOT_FOUND` for `@modelcontextprotocol/sdk`. `packages/cli/src/commands/environment.ts` imports the SDK client at module load and `index.ts` loads the environment command statically, but the SDK was declared only in the CLI's `devDependencies`, so an install without development dependencies didn't have it. The SDK (`^1.32.2`) is now a production dependency of `packages/cli`; the lockfile update is metadata only (its dev flags), with no package added, removed or re-versioned. `scripts/cli-runtime-dependencies.test.mjs` walks the import closure from the CLI's entry points and fails if any external package it reaches isn't a production dependency, and the testbed image now runs `rig --help` after `rig --version`.
4 files, +77/-87.
The capability map said a typed reply to a question parked on its own person resumes it but isn't recorded as the answer. Since #2293 it is: that person's typed reply resumes the question and is kept the same way as on an unparked ask, in the same transaction. A reply whose order against the posted question can't be confirmed resumes it with the words in context and isn't recorded. Only that sentence changes.
Text only. 1 file, +3/-2.
The runner conformance check (`docs/reference/runner-conformance/check-runner.mjs`), which runner authors run on their own machines, could fail a correct runner when the machine was busy. Case 1 waited 800 ms after seeing `session/new` and took one sample, so if the check's own timers ran late it called a correctly written launch record early; it now samples the record against the agent's log for the whole unanswered window, which is at least as strict. Case 4b required the cancelled or ended turn's `Stop` to arrive after the withdraw, an order section 7 doesn't promise; the turn-end checks now count only the `Stop` that follows the agent's own `session/prompt` reply with its stop reason, on either side of the withdraw, so an early idle `Stop` can't stand in for a missing end. Check names, the contract text and the kit's README are unchanged.
Check +28/-6, tests and fixtures +46/-4.
Main's CHANGELOG gains the `## [0.7.7]` section, byte for byte as on `release/0.7.7-prep`, directly above `## [0.7.6]`: 0.7.7 is 0.7.6 with three reliability fixes for managed Claude seats (the "Update installed" footer reads idle, managed compaction doesn't type into an unsent draft, and a Claude seat behind a non-exec wrapper rebuilds, with a failed handover keeping its resume target), with its upgrade step, known limits and thanks. Nothing else changes.
Text only. 1 file, +60/-0.
A provider switch on a Claude seat (`rig provider switch --seat <seat>`) no longer refuses with `effort_would_change` when the seat runs with an `--effort` that differs from its saved, non-explicit effort. The relaunch keeps the running effort, so a person recovering from a limit or an outage doesn't have to set effort and switch again. An explicit `rig seat set-effort` choice still wins: restore's launch-effort seam reads the seat at launch, uses the choice when one exists (latest wins) and the kept effort only when none does, and reports what it selected on every keep-pane relaunch. The kept effort is never written to the seat. The switch's `effort` line is worded from that report; without one it says so, and a relaunch that doesn't report resuming says what was asked and the rule the recovery command follows, not a value read before the switch. An unreadable running effort proceeds as before and is disclosed. #2344's token-reference restore, pre-stop and unknown-effect handling are unchanged.
Follows #2344. Source +102/-25, tests +196/-27.
Runner docs that still described the first, refuse-every-request version of permissions now describe the answered path. The harness runner contract's four "0.7.7" labels for the answered path read "0.7.8", the release that ships it; section 7's text is otherwise unchanged. `author-contracts.md` gives the answered recipe instead of "refusal-first", `community-harness-presets.md` notes the runner-preset exception to "relays none", the conformance README no longer says a runner refuses another person's click or typed words, and the OpenShell guest README lists the three permission routes beside the hooks route. This is the same change as the docs commit on the 0.7.8 release preparation branch, so main stops describing the old behavior.
Text only. 5 files, +9/-9.
A managed Claude seat whose `claude` is a `#!` script that starts the real Claude without `exec` can be rebuilt again. `rig seat handover --source rebuild` on such a seat stopped after 30 seconds at "Waiting to confirm the new Claude process in this pane", though the new Claude was running: the wrapper keeps running the script as the pane's foreground process, with Claude as its child, and no process passed the script rule. Readiness now also admits the direct child of the process running the frozen script, in the pane's foreground group, when its arguments name this launch's own conversation token; the token match, single candidate, two stable samples and pane PID checks are unchanged, and every other process is refused as before.
A failed handover no longer moves the seat's resume target to the new, unprimed conversation while the seat keeps its previous occupant. A Claude `session_identity` hook carrying an occupant generation that isn't current, isn't registered for the seat, and arrives while the seat has a current tenure now answers `tokenPersisted: false` with `generation_uncommitted`; a successful handover still writes the launch token at commit. Codex is unchanged. Limit: a seat whose current generation was never registered (an adopted Claude carrying another generation, or a managed launch whose tenure record failed to write) keeps its first token, and later hooks don't move it.
Refs #2341. Source +33/-1, tests +138/-1.
`docs/reference/community-harness-examples/dsh/` adds a preset (`dsh.yaml`), an example runner (`openrig-dsh-runner.mjs`) and a README showing a runner author the runner contract end to end for dsh, a harness that speaks ACP: launch, activity, input, cancel and permissions. Permission requests follow section 7 of the runner contract: the runner submits the agent's live request, delivers the person's answer once, permits with the agent's allow option when nobody can be asked, and refuses and holds attention only on the seat's `--deny` or when no allow option is offered. `docs/reference/community-harness-presets.md` points to the example. A real dsh hasn't run it yet. One limit is named: the runner keeps one hold per seat, so a second request in the same turn can release an earlier one's hold. Docs and example +658, tests +447.