Compare commits

...
Author SHA1 Message Date
dev-driver 63a7375d2b docs(release): 0.7.11 notes: a tile's first keys on tmux before 3.5
The CHANGELOG and whats-new name the one behaviour a writable tile has
with a tmux server older than 3.5 (#2821): its first keys go to the seat
before the tile attaches, so a tmux prefix typed as the very first key
reaches the seat instead of acting as the prefix. Typing into a tile
reaches the seat on those servers again; on earlier 0.7.11 candidates
it didn't.
2026-11-09 10:50:55 +00:00
Mike Schwarz 2cea5cc34c fix(daemon): typing into a writable tile reaches the seat on tmux before 3.5 (#2821)
Just before a writable tile sends its first typed keys, it reads the running tmux server's version (display-message '#{version}'). On 3.5 and later the keys go through the attaching client (send-keys -K, unchanged); below 3.5, or when the version can't be read, they go to the pane and then the tile attaches. tmux 3.3a has no -K and 3.4 doesn't deliver keys sent that way, so typing never reached the seat there; reading the server rather than the client keeps a newer client attached to an older server on the working path. View-only tiles make no extra call. Below 3.5, a tmux prefix typed as the very first input reaches the seat instead of acting as the prefix. Pastes were unaffected. The fix and root-cause trace are w4rc0n's, posted in #2816.
2026-11-09 10:49:54 +00:00
Mike Schwarz 78c10eac00 fix(daemon): a paste during a tile's attach stays one paste (#2795)
The writable tile's attach script no longer writes \033[?2004l after the first key and before tmux attaches. tmux 3.4 and later turn bracketed paste back on only when the client starts, so a paste in that gap was sent unframed and each line reached the agent as Enter. The no-attach return still restores the terminal, and detach is unchanged. The tile-size helper's paste-framing sample is back to its original place after the key, and a generated-script cell asserts exactly one framing-off write, on the no-attach return.
2026-11-09 02:58:41 +00:00
dev60-review 5f7e5f8ac4 docs: say when a keep_saved restore can't put a skill back
agent-spec.md's restore paragraph now says keep_saved puts back a missing copy only while its recorded source still has the saved bytes: if rig down removed an unchanged copy and the source has changed since, the skill stays missing and the restore names it. The 0.7.11 CHANGELOG and whats-new add the same as a known limit; an edited seat copy is kept. Docs only.
2026-11-08 23:38:42 +00:00
dev60-review 70c6143a1a docs(release): 0.7.11 notes: a Pi seat with a declared account home starts again
The CHANGELOG and whats-new say a Pi seat whose spec declares its account home starts, resumes and switches accounts again (#2782): on the first 0.7.11 candidate the staged command began with shell setup exec can't run. The provider switch example now names --to-account and --reason, which the command requires with --account-home.
2026-11-08 23:38:42 +00:00
dev60-review b5ce309f3d docs(openshell): a restored guest reads its startup files again
The OpenShell README's exact-restore paragraph now says saved startup actions aren't resent, but the restored guest's first input asks it to read the projected CLAUDE.md and its startup files again, so strict recall after a restore isn't met. This is #2780's README hunk only.
2026-11-08 23:38:08 +00:00
Mike Schwarz a63e940d6a fix(daemon): a Pi seat with an account home starts again (#2782)
A Pi seat whose spec declares an account home composed its launch, resume and account-switch commands as 'unset VARS; /usr/bin/env ...', and the tmux staging script runs every command as 'exec <command>', so /bin/sh tried to exec 'unset' and the seat never started (exec: unset: not found). The account step now passes the same variables to /usr/bin/env as -u options, making the command a single simple command that exec replaces the staging shell with, so the agent keeps the staging shell's process. The scrub list, the selected account home, clear semantics and the failure message are unchanged; Pi commands without an account, and Claude, Codex and community-harness commands, compose exactly as before.
2026-11-08 23:38:08 +00:00
dev60-review 13d639680c docs(release): 0.7.11 notes: the final /compact waits out its preparation
The CHANGELOG and whats-new compaction sections say that the final /compact
now waits out the preparation's own fresh activity (up to 23 seconds, against
9), so an idle seat, such as one out of context, compacts instead of being
refused, and that a busy seat can wait up to about 14 seconds longer (#2764).
2026-11-08 18:20:04 +00:00
Mike Schwarz bbae974d0b fix(daemon): let preparation hooks age before managed compact (#2764)
The managed compaction's guarded final send waited 1 s plus the 8 s readable budget, while a fresh activity hook from its own preparation keeps the seat reading as running for 15 s, so an exhausted seat whose pane was idle could time out before /compact was ever sent. The default idle wait now uses the transport's existing readiness freshness window (SEND_READINESS_FRESHNESS_MS), still clipped to the original attempt deadline, and the guarded replacement preparation send uses the same default. A genuinely running seat, a permission prompt or typed input still holds the send, and cancellation, generation and unknown-write handling are unchanged. Cost: a busy seat's guarded send can wait up to about 14 s longer.
2026-11-08 18:19:29 +00:00
dev60-review 60059f404a docs(release): describe 0.7.11's final selection
The 0.7.11 CHANGELOG, whats-new and capability map gain the changes selected
after the first preparation, on main b40bc71c:

- #2749: on Linux, a daemon run as a systemd user service starts the seats'
  tmux server in its own user scope, so its unit can stop or restart without
  ending them; seats move there at their server's next fresh start.
- #2666: a queue wake refused at a prompt waits while a compaction still owes
  its restore, with the daemon-start and new-handoff paths named as limits.
- #2757: tiles show whole lines at any width without resizing the seat, and
  attach only while someone types; the polling cost is a known limit.
- #2755: OpenShell 0.1.6's maintained cleanup works for a custom image, and
  recreate and an exact resume passed natively; the OpenShell known issue now
  says a restored guest receives its startup text again, strict recall isn't
  met and the worker isn't end to end. The capability map's line, which said
  0.7.10 fixed both problems, now says what 0.7.11 does.
- #2665: a Claude seat launched with spaced --settings JSON can be verified
  again.

The intro names the new base, and the credits add #2749, #2666 and #2665 to
billbenetti's reports.
2026-11-08 17:33:25 +00:00
dev60-review 53d1db9e65 docs(release): 0.7.11 notes say the ACP launcher ships at the CLI's version
The CHANGELOG and whats-new now say `npm run pack:release` packs the
`@openrig/acp` launcher beside the CLI at the same version, without saying
whether this release publishes it. The CHANGELOG credits also thank @ziadeh
for the reports behind #2622 and #2486, and @mlc-factory-bot for the reports
behind #2605 and #2607.
2026-11-08 17:30:20 +00:00
dev60-review 6da68650a1 docs(capability-map): teach what 0.7.11 added
The packaged capability map's header marker moves to
capability-delta-v0.7.11; the verb count stays at ninety-two (no new
top-level verb).

- rig send: a seat whose prompt holds a person's typed draft gets the
  message joined to it by default (transport.draft_delivery deliver);
  refuse brings back prompt_draft; detection isn't complete.
- rig acp serve: the team as an agent in an ACP app (Zed, T3 Code), the
  team's questions in the app, acp.to, and the @openrig/acp launcher.
- rig queue show/list/transitions/update/block --host (claim doesn't),
  and rig queue artifact for an openrig.code-result/v1 manifest.
- rig workflow trace --full, objective:, and unregister.
- Grant-service callers (experimental, off by default): --task-destination,
  a seat's name alone, and the person's question tools.
- launch_wrapper for local Claude Code, Pi and Codex members, and the
  openrig plugin for a person's own coding agent.
2026-11-08 17:30:20 +00:00
dev60-review 752a87201b docs(release): describe 0.7.11
The CHANGELOG gains a [0.7.11] section above main's top section, from the
85 first-parent merges since 0.7.10's base (less #2613, #2634 and #2644,
which 0.7.10 shipped): before you upgrade (five migrations; sends now join
a person's draft by default; Pi account homes), what changed by area,
behaviour that changed, known limits carried from 0.7.10's published notes
or dropped where a merge fixed them, and credits.

whats-new.md is rebuilt from 0.7.10's note with a 0.7.11 section on top,
written for an agent on a just-upgraded install, and keeps #2699's own text
on pruning history-only events. 0.7.10's section moves below it, and its
OpenShell line is corrected and kept visible: the cleanup fix doesn't hold
against OpenShell 0.1.6.

The openrig-core line names the move from 0.1.26 to 0.1.27 (#2756).
2026-11-08 17:30:20 +00:00
dev60-review 0b359b07c4 chore(release): prepare 0.7.11 2026-11-08 17:30:20 +00:00
25 changed files with 1373 additions and 229 deletions

No files matched your search

+376
View File
@@ -8,6 +8,382 @@ deprecations, and behavioral changes. Breaking changes are called out explicitly
---
## [0.7.11]
OpenRig 0.7.11 is a patch for people running their own agent teams, cut from
main `b40bc71c`. Its biggest change: an ordinary send or work notice now goes
into a person's typed draft by default instead of being refused (see Behaviour
that changed). A person can now work with their team from their own coding
agent through the new `openrig` plugin or from an ACP app such as Zed or T3
Code, and, as an experiment that's off by default, answer the team's questions
from an assistant connected through a grant service. A prepared managed compaction waits for an idle moment instead
of asking again, and a restore stopped at a prompt clears itself. On Linux, a
daemon run as a systemd user service no longer takes its seats down when its
unit stops or restarts, and terminal tiles show whole lines at any width. There
are fixes across the queue, workflows, launch and speed under load, many
contributed or reported by people using OpenRig. The OpenShell worker's
maintained cleanup now works on OpenShell 0.1.6, but the worker still isn't end
to end: see Known limits.
### Before you upgrade
- **Back up your database first.** Five new migrations run at the daemon's
first start: 150 (node launch wrapper), 151 (Pi account switches), 152
(workflow instance spec), 153 (events retention marks) and 154 (workflow
instance root objective). 0.7.10 had none. With the daemon stopped, copy the
file `$OPENRIG_DB` names. Going back to 0.7.10 after these migrations isn't
established.
- **Restart the daemon after installing:**
```bash
npm install -g @openrig/cli@0.7.11
rig daemon stop && rig daemon start
```
- **Linux, with the daemon run as a systemd user service:** the seats move into
their own scope only when their tmux server next starts fresh. Until then the
server stays in the daemon's unit, so stopping or restarting that unit still
takes the seats down, as before. ([#2749](https://github.com/mvschwarz/openrig/pull/2749))
- **The openrig-core plugin moves from 0.1.26 to 0.1.27.**
Its activity relay and refocus hooks changed, as did the refocusing,
openrig-user and openrig-skills skills. Replaced files are kept under
`$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits you still
need from there. ([#2756](https://github.com/mvschwarz/openrig/pull/2756), [#2743](https://github.com/mvschwarz/openrig/pull/2743), [#2717](https://github.com/mvschwarz/openrig/pull/2717), [#2735](https://github.com/mvschwarz/openrig/pull/2735), [#2657](https://github.com/mvschwarz/openrig/pull/2657), [#2647](https://github.com/mvschwarz/openrig/pull/2647), [#2619](https://github.com/mvschwarz/openrig/pull/2619))
- **Sends now join a person's draft** (`transport.draft_delivery` = `deliver`).
To keep the old refusal, run `rig config set transport.draft_delivery refuse`. ([#2645](https://github.com/mvschwarz/openrig/pull/2645))
- **A Pi seat that declares an absolute `PI_CODING_AGENT_DIR`** in its `env:`
now uses it as its account home and needs a readable `auth.json` there, or
the launch stops. Prepare one, or remove the declaration to keep the managed
seat login. ([#2630](https://github.com/mvschwarz/openrig/pull/2630))
### Sends and drafts
- **A send now goes into a person's typed draft.** Under the new
`transport.draft_delivery` setting's default, `deliver`, an ordinary
`rig send` or work notice to a seat whose prompt holds a person's typed draft
pastes the message at the cursor and submits the combined input as one
prompt, and the send's result says whether submission was confirmed.
`refuse` (also `OPENRIG_TRANSPORT_DRAFT_DELIVERY` in the daemon's
environment) restores the old refusal where OpenRig detects the draft, from
the next send with no restart. Menus, permission prompts, a seat's typing
guard, managed compaction and `--wait-for-idle` still refuse or wait under
either value. ([#2645](https://github.com/mvschwarz/openrig/pull/2645))
- **A draft only the input box shows is caught:** an ordinary Claude Code or
Codex send reads the box once before pasting (three tmux calls, within
500 ms). Under `refuse` the send is refused before any input; under `deliver`
the result says it joined the draft. ([#2653](https://github.com/mvschwarz/openrig/pull/2653))
- **Claude seats showing a statusLine or the background task panel** read as a
normal composer under the draft-aware typing guard, so an empty box gets the
delivery instead of `draft_input_unknown`. ([#2703](https://github.com/mvschwarz/openrig/pull/2703))
- **The bare `✔ Update installed · Restart to update` row** of Claude Code
2.1.319 in a narrow pane is recognised, so an idle seat's empty prompt reads
empty and wakes go through under the draft-aware guard. Checked against the
row as reported, not a live pane. ([#2662](https://github.com/mvschwarz/openrig/pull/2662))
- **A full held-message quota no longer drops a sender's later wakes:** the
sender's newest held message retires its earlier ones, listed as `coalesced`
in `rig seat held-messages <seat>` and readable with `--id`. ([#2701](https://github.com/mvschwarz/openrig/pull/2701))
- **Seats stopped while they owe work** don't get a burst of undeliverable
wakes at each daemon start; the start-up log says `waiting=N (not replayed: …)`. ([#2620](https://github.com/mvschwarz/openrig/pull/2620))
### Compaction, restore and handover
- **A prepared managed compaction waits for its window.** When the restore map
is complete but `/compact` couldn't go in (a draft, a tmux mode, no idle
moment), the attempt is held as `prepared` and sends `/compact` at the next
idle moment instead of asking the seat to prepare again about every 11
minutes. `rig compact --state` shows the hold and `--cancel` disarms it. A
busy seat isn't asked again after an ended attempt, across a restart too. ([#2698](https://github.com/mvschwarz/openrig/pull/2698), [#2716](https://github.com/mvschwarz/openrig/pull/2716))
- **The final `/compact` waits out the preparation's own fresh activity:** its
guarded send now allows the 15-second hook freshness window plus the 8-second
capture budget (23 seconds, against 9), so a seat that's already idle, such
as one out of context, gets `/compact` instead of a refusal. A busy seat can
wait up to about 14 seconds longer; the attempt's deadline still limits it. ([#2764](https://github.com/mvschwarz/openrig/pull/2764))
- **A restore stopped at a prompt clears itself** (such as Claude's resume
selection) once the seat runs the conversation it was restoring, using the
check `rig seat clear-attention` runs. The docs cover the manual route. ([#2711](https://github.com/mvschwarz/openrig/pull/2711), [#2674](https://github.com/mvschwarz/openrig/pull/2674))
- **A queue wake refused at a prompt no longer goes in right after "Compacted"**
while the compaction still owes its turn boundary and restore request. It
goes in at the seat's next idle moment after that step clears, as a recovered
notification. ([#2666](https://github.com/mvschwarz/openrig/pull/2666))
- **A Codex helper inside a Claude seat** (such as `codex exec` from its Bash
tool) no longer becomes the seat's resume token, so a restart resumes the
Claude conversation instead of failing with `no_conversation_found`. ([#2740](https://github.com/mvschwarz/openrig/pull/2740))
- **A Claude rebuild handover checks its priming packet:** if it's still
staged, the handover reports the submission unconfirmed and says to submit
the existing packet once, never to resend or rerun. ([#2650](https://github.com/mvschwarz/openrig/pull/2650))
- **Restoring a Claude seat's activity hooks** finds the agent's own source from
its saved launch recipe, not a same-name agent in the rig's folder. ([#2617](https://github.com/mvschwarz/openrig/pull/2617))
- **`on_restore: follow_source`** on a spec's `resources.skills[]` entry
refreshes that skill from its source when a Claude or Codex seat is restored,
keeping its conversation; `keep_saved` stays the default. ([#2626](https://github.com/mvschwarz/openrig/pull/2626))
### Your own coding agent
- **The `openrig` plugin,** shipped beside openrig-core, lets a person work with
their local team from their own Claude Code or Codex: hand it work, follow
it, answer the team's questions about it, and read the result. Claude Code:
`claude plugin marketplace add "$(npm root -g)/@openrig/cli/daemon/assets/plugins/openrig"`,
then `claude plugin install openrig@openrig-app`. Codex: the same
`codex plugin marketplace add` path, `codex plugin add openrig@openrig-app`, then
`codex mcp add openrig --env OPENRIG_SESSION_NAME=<address> -- rig mcp serve --front-door`.
The plugin's README has the steps. ([#2647](https://github.com/mvschwarz/openrig/pull/2647))
- **VS Code, Copilot CLI and Cursor** install it too, with no seat; the README
and `rig_whoami` say to add your own `rig mcp serve --front-door` server with
`OPENRIG_SESSION_NAME` set and turn the plugin's off. A seatless app can still
create a task, send and post, as `unknown-mcp-<8 hex>`. ([#2683](https://github.com/mvschwarz/openrig/pull/2683), [#2709](https://github.com/mvschwarz/openrig/pull/2709), [#2718](https://github.com/mvschwarz/openrig/pull/2718), [#2657](https://github.com/mvschwarz/openrig/pull/2657))
- **Answers are recorded exactly as the person typed them.** ([#2733](https://github.com/mvschwarz/openrig/pull/2733))
- **Marketplace files at the repository root** let Claude Code's
`/plugin marketplace add mvschwarz/openrig`, and directories that take a public
Git repo, install it from GitHub (marketplace `openrig-app`). ([#2719](https://github.com/mvschwarz/openrig/pull/2719))
### ACP apps: `rig acp serve`
- **OpenRig as an agent in ACP apps:** `rig acp serve --to <member@rig>` runs
on stdio for an app that launches a custom agent (Zed's `agent_servers`, T3
Code). A prompt becomes a queue row, each row the work reaches shows as a
step, and the closing resolution is the reply. Nothing runs until an app
starts it, and it talks to the daemon `rig` uses. ([#2648](https://github.com/mvschwarz/openrig/pull/2648), [#2714](https://github.com/mvschwarz/openrig/pull/2714))
- **The team's questions reach the app** while a turn is open, as a permission
request or a one-choice form, and the choice is the person's answer. Sessions
can be listed and reopened, and a question left unanswered there is named as
still open in the TUI and Slack. ([#2651](https://github.com/mvschwarz/openrig/pull/2651), [#2708](https://github.com/mvschwarz/openrig/pull/2708), [#2686](https://github.com/mvschwarz/openrig/pull/2686), [#2712](https://github.com/mvschwarz/openrig/pull/2712))
- **The `@openrig/acp` launcher** (`openrig-acp`) runs the matching
`@openrig/cli`'s `rig acp serve` for the ACP registry's `npx` launch, with a
new `acp.to` setting for the receiver. `npm run pack:release` packs it beside
the CLI at the same version. ([#2655](https://github.com/mvschwarz/openrig/pull/2655), [#2715](https://github.com/mvschwarz/openrig/pull/2715))
### People and connected assistants
- **Assistants connected through a grant service** (experimental, unsupported,
off by default: you serve `rig mcp serve --http --grant-service <url>` behind
your own TLS tunnel, and an outside service grants each connection):
`rig_question_list` and `rig_question_answer` let a registered person answer
the team's questions as their own; `--task-destination <member@rig>` names
the receiver for `rig_queue_create`, and a bare seat name resolves. ([#2735](https://github.com/mvschwarz/openrig/pull/2735), [#2638](https://github.com/mvschwarz/openrig/pull/2638), [#2750](https://github.com/mvschwarz/openrig/pull/2750))
- **The `relay` gateway connector** sends questions, updates and notices to an
operator-owned bridge over loopback HTTP and takes replies back. It's off
unless you add a relay binding, and the bridge, with its reach to a messaging
platform, is yours (`docs/reference/gateway-relay.md`). ([#2624](https://github.com/mvschwarz/openrig/pull/2624))
- **The web UI's For You feed** answers a local ask's questions on its card. ([#2612](https://github.com/mvschwarz/openrig/pull/2612))
### Queue, workflows and status
- **One `unclaimed-backlog` notice** replaces a finding per row for a seat that
reads without claiming and has no escalation route or delegation parent. ([#2752](https://github.com/mvschwarz/openrig/pull/2752))
- **Claims read `working`, not stalled,** for up to 60 minutes while a Claude
owner has a live background shell, and 20 while a managed restore is owed. ([#2675](https://github.com/mvschwarz/openrig/pull/2675))
- **`rig queue show`, `list`, `transitions`, `update` and `block` take
`--host <id>`,** as `create` and `handoff` did; `claim` doesn't. ([#2678](https://github.com/mvschwarz/openrig/pull/2678))
- **Cross-host handoffs** that override a claim wake the claimer, and a close
to an unverifiable successor names `rig queue handoff`. ([#2734](https://github.com/mvschwarz/openrig/pull/2734), [#2621](https://github.com/mvschwarz/openrig/pull/2621))
- **Code results:** an `openrig.code-result/v1` manifest as `evidenceRef` gives
`rig_artifact_read` and the new `rig queue artifact` the commit pair to diff. ([#2659](https://github.com/mvschwarz/openrig/pull/2659))
- **Workflows:** a run keeps its own spec when another pod's copy reuses the id
and version; the root objective is kept and printed as `objective:`;
`rig workflow unregister` removes versions; `trace --full` returns the whole
trail. ([#2696](https://github.com/mvschwarz/openrig/pull/2696), [#2738](https://github.com/mvschwarz/openrig/pull/2738), [#2676](https://github.com/mvschwarz/openrig/pull/2676), [#2667](https://github.com/mvschwarz/openrig/pull/2667))
- **Activity:** a helper harness inside a seat no longer reports its turns as
the seat's, and a resumed seat no longer reads a false model divergence. ([#2743](https://github.com/mvschwarz/openrig/pull/2743), [#2664](https://github.com/mvschwarz/openrig/pull/2664))
- **Also:** refocus traces projects outside `workspace.root`; a timed-out
`rig stream emit` names a retry that can't duplicate; `rig context
reconstruction inspect` and `view` read an exact revision from local stores.
([#2717](https://github.com/mvschwarz/openrig/pull/2717), [#2619](https://github.com/mvschwarz/openrig/pull/2619), [#2207](https://github.com/mvschwarz/openrig/pull/2207))
### Launch, install and setup
- **On Linux, a daemon run as a systemd user service starts the seats' tmux
server in its own user scope** (`openrig-tmux-<id>.scope`, with
`OOMPolicy=continue` from systemd 253), so stopping or restarting the
daemon's unit no longer ends the seats, and their memory no longer counts
against it. A daemon started from a login session, or a failed `systemd-run`,
starts the server as before with one log line giving the reason; macOS and
Windows are unchanged. ([#2749](https://github.com/mvschwarz/openrig/pull/2749))
- **A Claude seat whose launch passes `--settings` JSON written with spaces**
(such as `{"env": {…}}` from a proxy alias) can be verified again: OpenRig
reads the value whole, so it finds the seat's conversation. ([#2665](https://github.com/mvschwarz/openrig/pull/2665))
- **A Claude launcher script that execs the native installer's symlink**
becomes ready after a launch or rebuild, and `--no-chrome` is recognised. ([#2751](https://github.com/mvschwarz/openrig/pull/2751))
- **`launch_wrapper`,** an argv prefix whose launcher prepares the environment
and execs the agent, works for local Claude Code, Pi and Codex seats; a
wrapped seat never starts unwrapped. ([#2746](https://github.com/mvschwarz/openrig/pull/2746), [#2609](https://github.com/mvschwarz/openrig/pull/2609))
- **`rig provider switch --seat <pi seat> --to-account <name> --account-home <dir> --reason "<why>"`**
moves a Pi seat to another account through a confirmed successor, keeping its
model, effort and wrapper; `--dry-run` shows the plan. A Pi seat whose spec
declares its account home starts, resumes and switches accounts again: on the
first 0.7.11 candidate it didn't start, because the staged command began with
shell setup that `exec` can't run. ([#2630](https://github.com/mvschwarz/openrig/pull/2630), [#2782](https://github.com/mvschwarz/openrig/pull/2782))
- **Claude seats that load openrig-core themselves** stop loading its skills
twice: the next launch removes OpenRig's unchanged copies. ([#2660](https://github.com/mvschwarz/openrig/pull/2660))
- **Also:** a short permission-hook policy name; `rig attach --self` outside
tmux adds the rig; `rig host pair --timeout` holds the request that long;
`rig config set` names a masking variable; daemon start and stop treat a
probe the OS blocked as unknown; a bare piped `rig` skips the compile cache.
([#2640](https://github.com/mvschwarz/openrig/pull/2640), [#2641](https://github.com/mvschwarz/openrig/pull/2641), [#2692](https://github.com/mvschwarz/openrig/pull/2692), [#2547](https://github.com/mvschwarz/openrig/pull/2547), [#2502](https://github.com/mvschwarz/openrig/pull/2502), [#2643](https://github.com/mvschwarz/openrig/pull/2643))
### Terminals and views
- **Tiles show whole lines at any width without resizing the seat.** A tile
reads its seat's screen about once a second instead of attaching, so a tile
narrower than the seat no longer shows only its left part. A writable tile
attaches when someone types or pastes and goes back to showing after 30
seconds without a key; a read-only tile only shows. With a tmux server older
than 3.5, a tile's first keys go to the seat before the tile attaches, so a
tmux prefix typed as the very first key reaches the seat instead of acting as
the prefix. ([#2757](https://github.com/mvschwarz/openrig/pull/2757), [#2821](https://github.com/mvschwarz/openrig/pull/2821))
- **Herdr follow views** work on macOS too (protocol 22), open a labelled
snapshot where live following isn't supported, and show a cleaner strip. ([#2663](https://github.com/mvschwarz/openrig/pull/2663), [#2706](https://github.com/mvschwarz/openrig/pull/2706))
- **Closing the herdr-core terminal that started OpenRig** no longer ends the
daemon or its seats (per herdr-core's documented behaviour; a real close
wasn't exercised). ([#2737](https://github.com/mvschwarz/openrig/pull/2737))
- **Also:** terminal snapshots keep colours; a reattach during a broker
teardown works; a home under a symlinked parent records its real path;
`rig doctor` says why tmux control mode is off. ([#2654](https://github.com/mvschwarz/openrig/pull/2654), [#2671](https://github.com/mvschwarz/openrig/pull/2671), [#2633](https://github.com/mvschwarz/openrig/pull/2633), [#2707](https://github.com/mvschwarz/openrig/pull/2707))
### Speed under load
- **The stuck sweep** batches its reads and yields every 50 rows: on a copy of
a busy 340 MB database, a pass went from 10.2–11.4 s to 5.1–5.5 s and the
longest stall from about 10 s to 0.57 s. ([#2649](https://github.com/mvschwarz/openrig/pull/2649))
- **`view.changed` events are no longer stored,** so the `events` table grows
about a third as fast on the busy host measured, and opt-in
`retention.events_days` prunes eight history-only event types. ([#2695](https://github.com/mvschwarz/openrig/pull/2695), [#2699](https://github.com/mvschwarz/openrig/pull/2699))
- **The web UI** loads the newest 100 events instead of the whole log. ([#2704](https://github.com/mvschwarz/openrig/pull/2704))
- **`daemon.seat_poll_interval_ms`** (1000 to 2000) trades activity freshness
for daemon CPU on large fleets. ([#2688](https://github.com/mvschwarz/openrig/pull/2688))
- **Also:** with `OPENRIG_TMUX_CONTROL=1`, short sends and `set-option` use the
control client; the idle-shell check reads the plain process table; skill
catalog Git reads time out after five seconds. ([#2636](https://github.com/mvschwarz/openrig/pull/2636), [#2690](https://github.com/mvschwarz/openrig/pull/2690), [#2713](https://github.com/mvschwarz/openrig/pull/2713))
### Docs and tests
Daemon HTTP clients and bearer tokens ([#2652](https://github.com/mvschwarz/openrig/pull/2652)), the 0.7.10 capability map on
main ([#2736](https://github.com/mvschwarz/openrig/pull/2736)), a neutral TUI demo snapshot ([#2724](https://github.com/mvschwarz/openrig/pull/2724)), a web UI editor timer
([#2689](https://github.com/mvschwarz/openrig/pull/2689)), and test and CI fixes ([#2753](https://github.com/mvschwarz/openrig/pull/2753), [#2681](https://github.com/mvschwarz/openrig/pull/2681), [#2685](https://github.com/mvschwarz/openrig/pull/2685), [#2673](https://github.com/mvschwarz/openrig/pull/2673), [#2628](https://github.com/mvschwarz/openrig/pull/2628)).
### Behaviour that changed
- **Sends and work notices that were refused with `prompt_draft` now go
through by default, joined to the person's draft** (above).
`rig config set transport.draft_delivery refuse` restores the old refusal
where OpenRig detects the draft, from the next send with no restart; under
`refuse`, a draft only the input box shows now refuses too. ([#2645](https://github.com/mvschwarz/openrig/pull/2645), [#2653](https://github.com/mvschwarz/openrig/pull/2653))
- **A Pi seat with an absolute `PI_CODING_AGENT_DIR`** uses it as its account
home on launch, restart, resume and restore; no `auth.json`, no launch. ([#2630](https://github.com/mvschwarz/openrig/pull/2630))
- **`/api/events` no longer emits `view.changed`,** live or replayed, so new
ones leave the web UI's log, activity feed and For You and the historical
telemetry export; the view streams still carry them. ([#2695](https://github.com/mvschwarz/openrig/pull/2695))
- **The stuck sweep** files one backlog notice where it filed one per row,
skips `stalled-after-claim` inside the two windows above, re-checks each
rule just before filing, and skips overlapping ticks. ([#2752](https://github.com/mvschwarz/openrig/pull/2752), [#2675](https://github.com/mvschwarz/openrig/pull/2675), [#2649](https://github.com/mvschwarz/openrig/pull/2649))
- **Managed compaction:** a prepared attempt stays `waiting`, and a manual
`rig compact` over it reads `already_in_progress`. ([#2698](https://github.com/mvschwarz/openrig/pull/2698), [#2716](https://github.com/mvschwarz/openrig/pull/2716))
- **The daemon and seats no longer inherit `HC_SCOPE`, `HC_TERMINAL_ID` or
`HC_TERMINAL_SOCKET`;** one already running keeps them until restarted. ([#2737](https://github.com/mvschwarz/openrig/pull/2737))
- **On Linux, the seats' tmux server runs in its own systemd user scope** when
the daemon is a user service (above): the daemon unit's stop, restart and
memory accounting no longer cover the seats. ([#2749](https://github.com/mvschwarz/openrig/pull/2749))
- **Terminal tiles don't attach to show a seat:** a detach shows the seat again
instead of closing the tile, and a tile that can't read its seat ends with
tmux's own error. ([#2757](https://github.com/mvschwarz/openrig/pull/2757))
- **Also:** each sender keeps one live held message per seat; daemon start
doesn't re-send a failed wake to a seat with no receiver; restores record
`restore.outcome_reconciled` themselves; `rig terminal open --follow` returns
`follow_unsupported` with a snapshot. ([#2701](https://github.com/mvschwarz/openrig/pull/2701), [#2620](https://github.com/mvschwarz/openrig/pull/2620), [#2711](https://github.com/mvschwarz/openrig/pull/2711), [#2663](https://github.com/mvschwarz/openrig/pull/2663))
- **MCP and ACP:** an empty or placeholder seat value means no seat; the
seatless `rig_whoami` text changed; `--front-door` is `front-door/v2`;
grant-service callers' `destination` is optional; `rig acp serve` gives
form-taking apps a form, leads step titles with the latest note, and asks
nothing after a cancel. ([#2657](https://github.com/mvschwarz/openrig/pull/2657), [#2709](https://github.com/mvschwarz/openrig/pull/2709), [#2718](https://github.com/mvschwarz/openrig/pull/2718), [#2733](https://github.com/mvschwarz/openrig/pull/2733), [#2638](https://github.com/mvschwarz/openrig/pull/2638), [#2750](https://github.com/mvschwarz/openrig/pull/2750), [#2708](https://github.com/mvschwarz/openrig/pull/2708), [#2686](https://github.com/mvschwarz/openrig/pull/2686), [#2655](https://github.com/mvschwarz/openrig/pull/2655))
- **CLI:** `rig attach --self` outside tmux records `<name>@<rig>`;
`rig host pair` keeps a request for its `--timeout`; `rig config set` and
`reset` name environment overrides; `rig daemon start` and `stop` refuse when
a blocked probe leaves identity or liveness unknown; `--host` receipts print
a runnable command. ([#2641](https://github.com/mvschwarz/openrig/pull/2641), [#2692](https://github.com/mvschwarz/openrig/pull/2692), [#2547](https://github.com/mvschwarz/openrig/pull/2547), [#2502](https://github.com/mvschwarz/openrig/pull/2502), [#2678](https://github.com/mvschwarz/openrig/pull/2678))
- **Seat launches:** unchanged OpenRig copies of openrig-core's skills are
removed, with warnings; the permission hook reads the installed policy lists
on each call; model divergence reads pending until the new occupant's record.
([#2660](https://github.com/mvschwarz/openrig/pull/2660), [#2640](https://github.com/mvschwarz/openrig/pull/2640), [#2664](https://github.com/mvschwarz/openrig/pull/2664))
### Known limits
- **OpenShell:** on OpenShell 0.1.6 the maintained cleanup (`delete-owned`)
now works for a custom image, and recreate and an exact resume passed in our
native run. A restored guest still receives its startup text again, so strict
recall after a restore isn't met, and the worker isn't end to end. The CA fix
holds on 0.1.6. Never give the guest the daemon's main API port (normally
7433). After a same-generation reconciled launch, run
`rig environment connect` yourself. ([#2755](https://github.com/mvschwarz/openrig/pull/2755))
- **Drafts:** detection isn't complete. Under `refuse`, an unreadable input box
lets the send go ahead, so an undetected draft goes with the message; under
`deliver`, a result may not mention a draft it joined. ([#2645](https://github.com/mvschwarz/openrig/pull/2645), [#2653](https://github.com/mvschwarz/openrig/pull/2653))
- **A post-compaction step can still hold** when the box reads as typed, under
draft-aware delivery or the typing guard, or after a lost `/compact`. Run
`rig compact <seat> --cancel`, then send a restore request naming the newest
restore map, marker and packet. A queue wake refused at a prompt now waits for
that step, but recovery at daemon start and a new handoff's wake don't check
for it yet. ([#2575](https://github.com/mvschwarz/openrig/issues/2575), [#2666](https://github.com/mvschwarz/openrig/pull/2666))
- **Draft-aware delivery to Claude:** a message held as `draft_input_unknown`
still isn't retried. Whether managed compaction now gets past the banner
(`target_activity_unknown`) wasn't retested. ([#2605](https://github.com/mvschwarz/openrig/issues/2605))
- **Claude provider switch:** the failure reported in [#2341](https://github.com/mvschwarz/openrig/issues/2341) ("The probe pane
returned to a shell instead of staying inside the runtime") isn't shown fixed.
- **`rig exec`:** checked with Claude Code and Codex through their ACP
adapters. A cancel is acknowledged, not settled, and says nothing about
further spend. Cancel with `rig exec cancel`, not `rig acp cancel`.
- **ACP apps:** checked in T3 Code (Beta) 0.1.7 and with a scripted client; the
unanswered-question notice hasn't run in a live app, nor the note-first step
title in Zed. A cancel stops waiting; the work carries on. ([#2648](https://github.com/mvschwarz/openrig/pull/2648), [#2686](https://github.com/mvschwarz/openrig/pull/2686), [#2712](https://github.com/mvschwarz/openrig/pull/2712))
- **The `openrig` plugin** hasn't run in Claude Desktop, the Codex app or
Copilot CLI. A seatless app can't claim, update, hand off or answer
questions, and everyone using the app on a machine acts as its seat. ([#2647](https://github.com/mvschwarz/openrig/pull/2647), [#2657](https://github.com/mvschwarz/openrig/pull/2657), [#2683](https://github.com/mvschwarz/openrig/pull/2683))
- **Grant-service assistants** still aren't woken: an item stays queued until
its receiver claims it. **Relay:** "Accepted by bridge" means the bridge has
it, not the person, and a failed send replays only at a restart. ([#2735](https://github.com/mvschwarz/openrig/pull/2735), [#2624](https://github.com/mvschwarz/openrig/pull/2624))
- **Event retention** can't be undone, and the database file doesn't shrink. ([#2699](https://github.com/mvschwarz/openrig/pull/2699))
- **`keep_saved` skills:** a skill whose source changes between `rig down` and
the restore comes back missing, named in the restore's warnings; an edited seat
copy is kept. ([#2626](https://github.com/mvschwarz/openrig/pull/2626))
- **Terminal tiles** each run one tmux command a second while showing: 8 tiles
used about 6–8% of one core on an Apple Silicon Mac mini, against about 0.2%
for 0.7.10's attached tiles. ([#2757](https://github.com/mvschwarz/openrig/pull/2757))
- **Claude `launch_wrapper`** hasn't run with zsh, on macOS arm64 or with a real
installed Claude; an alias or shell function named `claude` fails closed. ([#2746](https://github.com/mvschwarz/openrig/pull/2746))
- **Going back to 0.7.10:** besides the migrations, an older permission hook
gives named-policy Claude sessions no decision until relaunched. ([#2640](https://github.com/mvschwarz/openrig/pull/2640))
- **Codex hooks:** a session kept running across an upgrade can show "Hook
failed — plugin contents changed; hook approval required" until relaunched,
and 0.7.11 changes openrig-core again. `rig seat restart --keep-conversation`
refuses a Codex seat launched with developer instructions (`settings_drift`).
- **Codex hook setup:** with smol-toml 1.9.x and an existing `hooks.json`,
OpenRig can leave its activity hooks and trust records unchanged. The range
(`^1.8.0`) still permits 1.9.x; the repository lockfile uses 1.8.0. ([#2720](https://github.com/mvschwarz/openrig/issues/2720))
- **Claude Code 2.1.319 starts in auto mode by default,** so a new Claude seat
may act without asking; manual mode restores asking.
- **Also:** permission requests in seat status show only the latest, in memory
for five minutes, and suggested rules can contain sensitive values; Slack
private channels need the documented, untested route, direct messages aren't
supported and people reads haven't run live; a Codex snapshot restore writes
OpenRig's guidance blocks into a tracked `AGENTS.md` (check `git status`); a
scheduled reminder that fails before input isn't retried; SSH host status
reads `unknown` behind ProxyJump or ProxyCommand; advisories don't show on a
native resume; seats handed over before 0.7.9 refuse stop, restart and
clean; after a project-local install, plain `rig` isn't found in the TUI's
own shell; with Claude Code 2.1.319, `rig up` can wrongly say "Startup prompt
still staged … press Enter"; two npm installs into one folder at once once
left a partial install, cause unknown.
- **Fixed or changed from 0.7.10's known issues:** the bare update banner
([#2662](https://github.com/mvschwarz/openrig/pull/2662)) and the Claude launcher-script readiness timeout with `--no-chrome`
([#2751](https://github.com/mvschwarz/openrig/pull/2751)). 0.7.10's OpenShell limit is superseded by the one above.
- **The earlier limits below that 0.7.11 doesn't fix or change still apply.**
### Credits
Thanks to @billbenetti, @seanmcn, @smota, @Aummadour, @dajiaohuang, @w4rc0n,
@Mohsintl, @twotnguyen, @ziadeh and @Nikhi00718 for merged contributions.
Thanks to @billbenetti for the reports behind [#2752](https://github.com/mvschwarz/openrig/pull/2752), [#2660](https://github.com/mvschwarz/openrig/pull/2660), [#2738](https://github.com/mvschwarz/openrig/pull/2738), [#2749](https://github.com/mvschwarz/openrig/pull/2749), [#2666](https://github.com/mvschwarz/openrig/pull/2666) and [#2665](https://github.com/mvschwarz/openrig/pull/2665), to
@pr0xc3nt4ur1 for the report behind [#2740](https://github.com/mvschwarz/openrig/pull/2740) and [#2743](https://github.com/mvschwarz/openrig/pull/2743), to @oneandonlydean for
the process chain behind [#2751](https://github.com/mvschwarz/openrig/pull/2751), and to @niklasbaldi for the report and cause
behind [#2654](https://github.com/mvschwarz/openrig/pull/2654).
Thanks also to @ziadeh for the reports behind [#2622](https://github.com/mvschwarz/openrig/issues/2622) and [#2486](https://github.com/mvschwarz/openrig/issues/2486), and to @mlc-factory-bot for the reports behind [#2605](https://github.com/mvschwarz/openrig/issues/2605) and [#2607](https://github.com/mvschwarz/openrig/issues/2607).
## [0.7.10]
Published 2026-11-08 as `@openrig/[email protected]`, built from the commit tagged
+5 -5
View File
@@ -26,9 +26,9 @@ use:
```sh
# Claude Code
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.5/skills/rigs --skill rigs --agent claude-code
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.11/skills/rigs --skill rigs --agent claude-code
# Codex
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.5/skills/rigs --skill rigs --agent codex
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.11/skills/rigs --skill rigs --agent codex
```
Then type `/rigs` in Claude Code, or `$rigs` in Codex, and say what you want built. The skill has your agent tell
@@ -68,11 +68,11 @@ Requires Node.js 22 or 24 and tmux, on macOS or Linux, and one working Claude Co
### Install it yourself
This runs OpenRig's install script from the `v0.7.5` release. It checks Node.js and npm, installs the latest published `@openrig/cli` with `npm install -g`, runs the Node.js and SQLite check, then runs `rig setup --dry-run` and `rig setup`. The first line prints that plan and changes nothing:
This runs OpenRig's install script from the `v0.7.11` release. It checks Node.js and npm, installs the latest published `@openrig/cli` with `npm install -g`, runs the Node.js and SQLite check, then runs `rig setup --dry-run` and `rig setup`. The first line prints that plan and changes nothing:
```bash
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh -s -- --dry-run
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.11/scripts/install.sh | sh -s -- --dry-run
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.11/scripts/install.sh | sh
```
If the last step isn't ready, the installer reports `INSTALLED, SETUP NOT COMPLETE` and points to the details from `rig setup` above. Follow that output and any reported Fix, then rerun `rig setup`; the CLI does not need reinstalling. If the only remaining failures are provider sign-ins, sign in to each selected provider as below and continue.
+4 -2
View File
@@ -459,8 +459,10 @@ skills:
An unknown `on_restore` value produces an advisory and uses `keep_saved`; it does not invalidate the spec.
A restore keeps the skills a seat was launched with. With `keep_saved`, it puts back the saved copy and never takes
today's bytes from the source. With `follow_source`, before the seat's session starts, the restore reads the seat's agent
A restore keeps the skills a seat was launched with where it can. With `keep_saved`, it puts back a missing copy only
while its recorded source still has the saved bytes, and never takes today's bytes from the source: if `rig down`
removed an unchanged copy and the source has changed since, the skill stays missing and the restore names it in its
warnings. With `follow_source`, before the seat's session starts, the restore reads the seat's agent
spec as it is now, using the saved launch recipe and rig folder, and:
- replaces the seat's copy with the source folder as it is now, and records it, so `rig down` removes it and the next
+2 -2
View File
@@ -201,11 +201,11 @@ You can also read [`skills/rigs/SKILL.md`](../../skills/rigs/SKILL.md) directly.
script will do and changes nothing:
```sh
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh -s -- --dry-run
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.11/scripts/install.sh | sh -s -- --dry-run
```
The same command without `--dry-run`,
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh`, installs the
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.11/scripts/install.sh | sh`, installs the
latest published `@openrig/cli` with `npm install -g`,
runs the Node.js and SQLite check, then `rig setup --dry-run` and `rig setup`. `rig setup` checks both Claude Code and
Codex and may install a missing one, as described below. A failed step prints
+1 -1
View File
@@ -17,7 +17,7 @@ npx skills add https://github.com/mvschwarz/openrig/tree/<release-tag>/skills/ri
npx skills add https://github.com/mvschwarz/openrig/tree/<release-tag>/skills/rigs --skill rigs --agent codex
```
For a new install, use the latest release's tag, for example `v0.7.5`. If OpenRig is already installed, use the tag
For a new install, use the latest release's tag, for example `v0.7.11`. If OpenRig is already installed, use the tag
that matches `rig --version`, or upgrade first. If the command fails with `npx: command not found`, Node.js isn't
installed or isn't on your PATH yet (see [What you need](#what-you-need)). Then type `/rigs` in Claude Code, or `$rigs` in Codex, and say what
you want built.
+693 -178
View File
@@ -1,94 +1,184 @@
# What changed in OpenRig 0.7.1
# What changed in OpenRig 0.7.11
This note is for an agent on an install that was just upgraded to 0.7.1. It lists only what behaves differently from
0.7.0 and what to do about it. For everything OpenRig can do, read the capability map:
`rig context get onboarding-width/public-what-you-can-do.md`. Each release replaces this file, so
`rig context get reference/whats-new.md` describes the installed version once the upgraded daemon is running. Without
a running daemon, read `$OPENRIG_HOME/reference/whats-new.md`. The daemon refreshes that copy when it starts, so right
after an upgrade it can still hold the previous version's note.
This note is for an agent on an install that was just upgraded to 0.7.11.
OpenRig 0.7.11 sends ordinary messages and work notices into a person's typed
draft by default instead of refusing them, lets a person reach the team from
their own coding agent (the `openrig` plugin) or an ACP app (`rig acp serve`),
holds a prepared managed compaction until the seat is idle, and clears a
restore that stopped at a prompt. On Linux, a daemon run as a systemd user
service keeps its seats when its unit restarts. It adds five database
migrations. On OpenShell 0.1.6 the worker's maintained cleanup now works, but
the worker still isn't end to end. If you upgraded
from 0.7.9 or earlier, also read "What changed in OpenRig 0.7.10" below, and
from 0.7.8 or earlier, "What changed in OpenRig 0.7.9" too. For the full
capability map, read `rig context get onboarding-width/public-what-you-can-do.md`.
After the upgraded daemon starts, `rig context get reference/whats-new.md`
describes this version. Without a running daemon, read the packaged
`daemon/docs/reference/whats-new.md`; `$OPENRIG_HOME/reference/whats-new.md` can
still be the previous copy until startup.
## After the upgrade
- **Restart the daemon** (`rig daemon stop`, then `rig daemon start`). Eight migrations run on the first start (124, 125,
127, 128, 129, 130, 131 and 135). Nine settings are added: `policies.claude_compaction.preparation_root` (unset),
`skills.router_line.enabled` (off), `decisions.enabled` (false), `decisions.consumers`, `decisions.template_dir`,
`retention.stream_days` (30), `retention.stream_max_items` (20000), `provider.selected` (empty) and
`daemon.db_conflict` (`warn`). None is removed.
- **Stream retention is on.** Stream items emitted after the upgrade are deleted after 30 days, or beyond the newest
20,000, unless a record names them (a classification, duplicate link, attempt or disposition). History from before the
upgrade is kept. Raise `retention.stream_days` or `retention.stream_max_items` to keep more.
- **The bundled openrig-core plugin goes from 0.1.17 to 0.1.18.** Startup replaces the installed plugin's changed files:
its Claude hook definitions, three hook scripts and the pre-compaction hook (so hooks start on a seat whose PATH has
no Node), and the `openrig-user`, `messaging-the-human` and `forming-an-openrig-mental-model` skills. As in
0.7.0, it first copies each file it replaces under `$OPENRIG_HOME/backups/managed-refresh/<id>/`, edited or not, and
logs `[plugin-vendor] preserved previous contents of <file> at <copy> before managed refresh`. A copy doesn't mean
anyone edited the file; if someone did, reapply the change from the copy.
- **Claude seats need Claude Code 2.1.280 or newer to see their team skills,** which now arrive as a per-seat plugin
through `CLAUDE_CODE_PLUGIN_DIRS` (below). OpenRig doesn't check the version. A running seat gets the new layout at its
next launch, restore or handover.
- **If a worktree's `.claude/settings.local.json` says `"defaultMode": "acceptEdits"` and the person didn't choose it,**
an earlier OpenRig wrote it when it added or freshly launched a managed Claude seat. 0.7.1 no longer writes it but
doesn't repair it; ask the person which mode they want and set it once.
- **Back up the database, then restart the daemon.** Five migrations run on
the daemon's first start: 150 (node launch wrapper), 151 (Pi account
switches), 152 (workflow instance spec), 153 (events retention marks) and 154
(workflow instance root objective). If `$OPENRIG_DB` isn't backed up yet,
copy that file (`rig config get db.path` shows it) with the daemon stopped,
then `rig daemon start`. Going back to 0.7.10 after these migrations isn't
established.
- **New settings:** `transport.draft_delivery` (`deliver`), `acp.to` (empty),
`daemon.seat_poll_interval_ms` (1000) and `retention.events_days` (`0`, keeps
every event). `rig config` shows each with its source.
- **The openrig-core plugin moves from 0.1.26 to 0.1.27.**
Its activity relay and refocus hooks changed, as did the refocusing,
openrig-user and openrig-skills skills. Managed refresh preserves replaced
files under `$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits
you still need from the preserved copy.
- **Linux, with the daemon as a systemd user service:** the seats move into
their own scope only when their tmux server next starts fresh; until then,
stopping or restarting the daemon's unit still takes them down.
- **Tell the person sends now join their drafts** (below). To keep the refusal,
run `rig config set transport.draft_delivery refuse`; it applies from the
next send, with no restart.
- **Pi seats:** a Pi seat that declares an absolute `PI_CODING_AGENT_DIR` in its
`env:` now uses it as its account home and needs a readable `auth.json`
there, or the launch stops. Prepare one, or remove the declaration to keep
the managed seat login. Ask the person before changing their spec.
- **herdr-core terminals:** new starts no longer inherit `HC_SCOPE`,
`HC_TERMINAL_ID` or `HC_TERMINAL_SOCKET`; a daemon or tmux server already
running with them keeps them until it's restarted.
- **Claude seats that load openrig-core themselves** lose OpenRig's unchanged
copies of its skills at their next launch, so each skill loads once; the
launch result warns with the copies removed or kept.
## New things you can do
## Sends and drafts
- **Choose how much automatic input reaches a seat:** `rig seat set-typing-guard <seat> --mode <mode> --reason "…"`, where
`<mode>` adds `openrig-urgent` (OpenRig's own notices and anything urgent) and `urgent-only` between `off` and `hold`.
Mark a message urgent with `rig send --urgent`, or create the row at priority `urgent` or `critical`. Held input stays
readable (`rig seat held-messages`) and is never replayed.
- **Choose how far OpenRig and peers may act on a seat:** `rig seat set-terminal-policy <seat> --preset
no-peer-interventions|help-within-instructions|full-control --reason "…"`, with `--helper <addresses…>`,
`--rig-default` (seats added later), and exceptions such as `--peer-compaction deny` or `--openrig-model allow`.
Managed compaction and saved model or effort changes are checked; a denial is `terminal_policy_refused`. Other
interventions are guidance only.
- **Read a seat's planned permissions:** `rig seat status <seat>` shows what its next fresh managed launch will pass and
where each part comes from, and its terminal policy. `rig setup --router-line yes|no` decides whether OpenRig adds its
one line to shared guidance files.
- **Let a person choose whether every ask reaches Slack:** `rig gateway human set <id> slack-subscription
every-ask|urgent-only|unset`. Every ask is on the TUI's Feed either way. Under `urgent-only`, only an ask
created or handed off with `--alert` posts. An ask marked `--alert` stands out in Slack (an interrupt with a mention)
under either setting, so use it only for something only they can do, or an approval whose wait costs now.
- **Give one Claude member its own setup** in the rig spec: `agent_file`, `append_system_prompt_file`, `plugin_dirs` and
`advisor` (an Advisor model). They're copied into that seat's storage, never the project.
- **See a seat's own skills:** `rig skill loadout --seat <session|self>`.
- **Reach the team from an assistant without a shell:** the MCP server has 34 tools, including `rig_whoami`,
`rig_restore_status`, `rig_team`, `rig_telemetry_events`, following a handoff, reading a finished item's artifact and
deciding inside a person's decision room. `rig mcp serve --http` serves them over HTTP; `rig mcp token add <name>
--seat <session>` gives each caller its own seat.
- **Hand a row's task to an outside agent:** `rig acp run <row>` for an ACP agent (Hermes, OpenClaw).
`docs/reference/a2a-agents.md` covers A2A agents.
- **Run a community harness that names, reports or reopens its conversations:** preset fields `launch_result`, `resume`,
`names_conversation` and `startup_assisted`, with examples for Cursor, Hermes and OpenClaw under
`docs/reference/community-harness-examples/`.
- **Change one edge on a running rig:** `rig edge add <rig> <from> <to> --kind <kind>` and `rig edge remove <rig>
<edge-id>` (`--plan`, `--json`). A `delegates_to` change says when it moves who work routes to.
- **Run one spec as several rigs:** `rig up <spec> --name <rig>`.
- **Retune a watchdog in place:** `rig watchdog update <jobId> [--interval-seconds n] [--message text] [--spec path]`
keeps its id and attachments. The context watch takes `--threshold-tokens` or `--threshold-percent`.
- **Put restore maps where seats may write:** `rig config set policies.claude_compaction.preparation_root <abs path>`.
- **Pick a Herdr session:** `rig terminal open <view> --provider herdr --session <name>`.
- **Close a row with its artifact:** `rig queue update <id> --state done --evidence-ref <path>`.
- **File and read subscription move reports:** `rig provider moves report` and `rig provider moves`. `--host <id>` and
`--codex-home <dir>` on `rig auth status|list|switch` and `rig seat restart` act on another machine's Codex home.
- **Route stream observations with typed decisions (opt-in, off by default):** once `decisions.enabled` is true,
`decisions.consumers` includes `stream` and the intake seat has `OPENROUTER_API_KEY`, `rig project wake --decide`
gives each eligible stream observation one bounded typed decision during that seat's wake: create a row for a supplied
owner, join supplied work, or record a duplicate or no action. Anything uncertain, refused or failed goes to the
intake seat as its own disposition, and the model isn't asked again. `rig project decisions status` shows the state
without making a call. Without `--decide`, or with decisions off, nothing changes; turning decisions off no longer
strands an accepted create or join whose effect hadn't landed.
- **Read the stream as a subscriber:** `rig stream subscribe <seat>` (`--tags`, `--type`; re-subscribing keeps the
place), then `rig stream read` returns the next bounded page for the calling seat (at most 20 items and 64 KiB, with
`more`, `unread` and any `gap`). `rig stream subscriptions` lists readers; `rig stream unsubscribe <seat>` stops one.
`rig stream show <id>` says what became of an observation.
- **Keep the kernel to the harnesses you chose:** `rig setup --providers <list>` is now saved as `provider.selected`, and
the kernel starts seats only for those.
- **Stop a second daemon on a live database:** `rig config set daemon.db_conflict stop` refuses a start whose database a
matching live daemon already holds (the default `warn` only warns). `rig daemon start --allow-db-conflict` allows one
deliberate start.
- **Under `deliver` (the default),** an ordinary `rig send` or work notice to a
seat whose prompt holds a person's typed draft pastes the message at the
cursor and submits the combined input as one prompt. The result says whether
submission was confirmed and, when OpenRig saw the draft, that it joined it.
An unset, unreadable or invalid value follows `deliver`.
- **Still refused or held under either value:** menus, permission prompts, a
seat's typing guard (`draft-aware`, `hold`), managed compaction's sends and
`--wait-for-idle`.
- **Under `refuse`,** a send is refused, and a work notice retried once the
prompt is clear, where OpenRig detects the draft. Claude Code and Codex sends
now also read the input box before pasting, so a draft only the box shows
refuses too (`target_needs_input` / `prompt_draft`).
- **The draft-aware typing guard** reads a Claude composer with a statusLine or
the background task panel, and Claude Code 2.1.319's bare
`✔ Update installed · Restart to update` row in a narrow pane, so an empty box
gets the wake instead of `draft_input_unknown`.
- **Held messages coalesce:** a sender's newest held message at a seat retires
its earlier ones. Read them with `rig seat held-messages <seat>` (each with
`--id <id>`); an older backlog clears at that sender's next held message or
with `rig seat retire-held-message`.
- **Stopped seats aren't woken at each daemon start:** their rows stay owed, and
the start-up log says `waiting=N (not replayed: …)`. That isn't delivery.
## Behaviour that changed
## Compaction, restore and handover
- **A prepared managed compaction is held, not re-requested.** When the map is
complete but `/compact` couldn't go in, the attempt stays `waiting`, marked
`prepared`, and sends `/compact` at the next idle moment with an empty box.
`rig compact <seat> --state` shows it; a manual `rig compact` reads
`already_in_progress`; `--cancel` disarms it. It ends on a context dip, a
conversation change, a stale map (`prepared_map_stale`: 15 points of growth or
2 hours) or a map that stops reading ready.
- **The final `/compact` waits out the preparation's own fresh activity** (up
to about 23 s, against 9 s), so an idle seat that ran out of context compacts
instead of being refused; a busy seat can wait up to about 14 s longer.
- **A queue wake refused at a prompt** no longer goes in right after
"Compacted" while the compaction still owes its turn boundary and restore
request; it goes in at the next idle moment after that step clears.
- **A full restore stopped at a prompt clears itself** once the seat runs the
conversation it was restoring. If a seat still reads `attention_required`, run
`rig seat clear-attention <seat>`, with `--reason` after a partial restore or
a community harness's assisted startup. `proof_superseded` means the restore
changed mid-check.
- **A Codex helper inside a Claude seat** no longer takes the seat's resume
token (`transcript_not_this_session`).
- **A Claude rebuild handover may report its priming packet unconfirmed.**
Submit the existing packet once, only if it's still staged. Never resend it
blindly or rerun the handover.
- **`on_restore: follow_source`** on a spec's `resources.skills[]` entry
refreshes that skill from its source on restore; `keep_saved` is the default.
## Working with the team from outside
- **The `openrig` plugin** lets a person use their own Claude Code or Codex
with their local team: hand it work, follow it, answer the team's questions
in their own words, and read the result. Install it from the package
(`claude plugin marketplace add "$(npm root -g)/@openrig/cli/daemon/assets/plugins/openrig"`,
then `claude plugin install openrig@openrig-app`) or from this repository on
GitHub. Everyone using the app on a machine acts as the seat it names.
- **VS Code, Copilot CLI and Cursor** start its server with no seat: add your
own `rig mcp serve --front-door` server with `OPENRIG_SESSION_NAME` set and
turn the plugin's off. A seatless app can create tasks, send and post as
`unknown-mcp-<8 hex>`, but can't claim, update, hand off or answer.
- **ACP apps:** `rig acp serve --to <member@rig>` (or the `acp.to` setting)
makes the team an agent in an app that launches a custom ACP agent, such as
Zed or T3 Code. Nothing runs until an app starts it, and it uses the daemon
`rig` uses. A question left unanswered in the app stays open in the TUI and
Slack. The `@openrig/acp` launcher is packed beside the CLI at the same
version. See `docs/reference/acp-agents.md`.
- **Assistants through a grant service** are experimental, unsupported and off
by default: they need your own internet-reachable `rig mcp serve --http
--grant-service <url>` and an outside grant service. A registered person can
answer the team's questions from one, and `--task-destination <member@rig>`
names where its tasks go (`docs/reference/hosted-members.md`).
- **The `relay` gateway connector** is off unless you add a relay binding. It
hands questions and updates to a bridge the operator runs on loopback;
"Accepted by bridge" isn't delivery (`docs/reference/gateway-relay.md`).
## OpenShell workers
On OpenShell 0.1.6 the maintained cleanup (`delete-owned`) now works for a
custom image, and recreate and an exact resume passed in our native run. A
restored guest still receives its startup text again, so strict recall after a
restore isn't met, and the worker isn't end to end. The CA fix holds on 0.1.6.
"Never grant the guest access to the daemon's main API
port (normally 7433)."
## Other changes to act on
- **Rows on another host:** `rig queue show`, `list`, `transitions`, `update`
and `block` take `--host <id>`; `claim` doesn't, since only the destination
seat on its own host claims. `--host` never follows `rig host select`.
- **A seat's unclaimed backlog:** a seat with no escalation route or delegation
parent gets one `unclaimed-backlog` notice. To route its findings to a
watching seat:
`rig edge add <rig> <root seat's logical id> <watching seat> --kind escalates_to`.
- **Claims read `working`** for up to 60 minutes while a Claude owner has a
live background shell, and 20 while a managed restore is owed.
- **A cross-host handoff that overrides your claim** wakes you with the
successor's id: stop and coordinate with that seat.
- **Code results:** set an `openrig.code-result/v1` manifest as `evidenceRef`;
`rig queue artifact <qitemId>` and `rig_artifact_read` return the commit pair.
- **Workflows:** runs keep their own spec; `rig workflow unregister
<name>[@<version>]` (or `--prune-missing`) removes registered versions;
`rig workflow trace <id> --full` returns the whole trail; `show`, `trace` and
`status` print `objective:`.
- **`rig stream emit` timeouts:** repeat with the same `--id`, or check with
`rig stream list` before emitting again.
- **Launch and setup:** `launch_wrapper` works for local Claude Code, Pi and
Codex seats; `rig provider switch --seat <pi seat> --to-account <name>
--account-home <dir> --reason "<why>"` switches a Pi seat's account (`--dry-run`
first), and a Pi seat with a declared account home starts again; `rig config set` says when
an environment variable masks the saved value.
- **Linux seats:** when the daemon runs as a systemd user service, the seats'
tmux server starts in its own user scope (`openrig-tmux-<id>.scope`), so the
daemon's unit can stop or restart without ending them. Otherwise the server
starts as before, with one log line giving the reason.
- **Terminal tiles** show whole lines at any width without resizing the seat: a
tile reads its seat about once a second, a writable tile attaches when someone
types and goes back to showing after 30 seconds without a key, and a detach
shows the seat again instead of closing the tile. With a tmux server older
than 3.5, a tmux prefix typed as a tile's very first key reaches the seat
instead of acting as the prefix.
- **A Claude seat launched with `--settings` JSON written with spaces** can be
verified again.
- **`view.changed` is no longer stored,** and `/api/events` no longer sends it.
- **Old history-only events can be pruned, if you choose to.** `retention.events_days` (default `0`, which keeps every
event) turns on a pass in the retention sweep (at daemon start and daily) that deletes `view.changed`,
`snapshot.created`, `watchdog.evaluation_fired` and the `queue.created`, `queue.updated`, `queue.claimed`,
@@ -107,105 +197,530 @@ after an upgrade it can still hold the previous version's note.
- **With pruning on, `rig telemetry events` reports the pruned ranges.** A page that spans pruned events inside the
retained window has a `retained_sequence_gap` for each missing seq range and `coverage.status` `partial` (it used to be
`retained_window`); events pruned from the start of the log show up as `before_retained_floor`.
- **Large fleets:** `daemon.seat_poll_interval_ms` (up to 2000) lowers CPU at
the cost of activity freshness; keep 1000 if tmux is slow.
- **Terminals:** Herdr follow views work on macOS (protocol 22) and open a
snapshot (`follow_unsupported`) where live following isn't supported.
- **Team skills live in each seat's own storage** (`$OPENRIG_HOME/state/seats/<rig id>/<logical seat>/<runtime>/`), not
in the project's `.claude/skills` or `.agents/skills`. Claude seats get them as a generated plugin (`openrig:<id>`),
Codex seats as their own catalog. If a link sits in that path, the seat starts without them and the result names
what wasn't delivered.
- **Urgent wakes are labelled:** an urgent or critical row's wake starts with `Urgent (marked by …):` at every seat. A
seat on `hold` gets one held notice per wake instead of re-nudges, and lifecycle work is refused at the middle levels as
at `hold`.
- **A daemon restart picks up a managed compaction where it stopped.** An owed restore step resumes, a `/compact` that
may have been sent is never sent again, and `rig compact --state` shows anything stranded or owed.
- **A Claude seat whose startup turn is refused** (an expired login, say) holds the rest of its startup context as owed.
A startup input sent before the refusal is noticed can still be submitted (see Known issues in the
[0.7.1 release notes](https://github.com/mvschwarz/openrig/releases/tag/v0.7.1)).
- **`rig send` refuses with `target_needs_input`** when a Claude seat's hook has just reported it running and its screen
shows an MCP enablement menu, a question or a permission prompt.
- **`rig down` asks before stopping the caller's own rig.** Without a terminal it exits 1, stops nothing and prints the
`--yes` command.
- **`rig up --plan` doesn't start a stopped daemon:** it exits 1 with `daemon_not_running` and names `rig daemon start`
and `--no-kernel`.
- **`rig daemon start`** waits up to 20 seconds for a slow daemon, and starts with a warning when only a secondary
listener, such as Tailscale's, can't be verified.
- **A seat joined with `rig attach` as an outside app** (Claude Desktop) stays attached through `rig down` and restore
(`left_attached`). To replace it, `rig unclaim <name>`, then launch.
- **New seats get the daemon's `OPENRIG_DB` and `TMUX_TMPDIR`.**
- **A rebuilt Codex seat keeps its observed permissions,** and a read-only Codex profile launches without OpenRig's
writable roots.
- **A seat's own role** goes only to that seat, never into a shared `CLAUDE.md` or `AGENTS.md`, and `rig down` returns
guidance files byte for byte.
- **Workflow actions** (`project`, `route`, `resume`, `abort`) record the calling agent; a request with neither identity
nor actor gets the shared `actor_required` error.
- **`rig provider moves` is back** for subscription move reports. It switches nothing.
- **`--dangerously-interact` into a seat's input box stops first** when the box already holds typed or pasted text: it
shows that text (apart from suggestions) and types nothing. Clear the box and retry, or pass `--type-without-checking`,
which clears nothing. Keys for a recognized native menu are sent as before.
- **A terminal seat gets exactly the text sent to it,** without the From/To lines (the result says `envelopeOmitted`).
A queue wake to a terminal seat types nothing and fails `failed:terminal-seat`; the row stays queued.
- **A daemon restart that can't see the seats' tmux server** no longer detaches seats whose recorded process still
runs, so it doesn't relaunch them beside themselves.
- **Provider credentials and the activity-hook token stay off tmux, shell and `env` command lines** at launch and
handover. A launch whose staging fails stops instead of falling back.
- **An answer retried from Slack or Steering** after an interrupted close reuses the first reply and finishes the
request once.
- **One bad row no longer stops the stuck sweep** from flagging the others; **a malformed unrelated diagnosis row** no
longer stops health diagnoses from listing.
## Limits to keep in view
## Output that changed shape
- **OpenShell:** a restored guest gets its startup text again, strict recall
isn't met, and the worker isn't end to end (above).
- **Drafts:** detection isn't complete. Under `refuse`, an unreadable input box
lets the send go ahead; under `deliver`, a result may not mention a draft.
- **A post-compaction step can still hold** when the box reads as typed, under
draft-aware delivery or the typing guard, or after a lost `/compact`. Run
`rig compact <seat> --cancel`, then send a restore request naming the newest
restore map, marker and packet. A refused queue wake now waits for that step,
but recovery at daemon start and a new handoff's wake don't check yet.
- **`keep_saved` skills:** a skill whose source changes between `rig down` and
the restore comes back missing, named in the restore's warnings; an edited seat
copy is kept.
- **Terminal tiles** each run one tmux command a second while showing (8 tiles
used about 6–8% of one core on an Apple Silicon Mac mini).
- **A message held as `draft_input_unknown`** isn't retried, and whether
managed compaction gets past the update banner wasn't retested.
- **Claude provider switch:** the "probe pane returned to a shell" failure in
[#2341](https://github.com/mvschwarz/openrig/issues/2341) isn't shown fixed;
the launcher-script rebuild timeout and `--no-chrome` are fixed.
- **ACP apps** were checked in T3 Code (Beta) 0.1.7 and with a scripted client;
the unanswered-question notice hasn't run in a live app. A cancel stops
waiting; the team's work carries on.
- **The `openrig` plugin** hasn't run in Claude Desktop, the Codex app or
Copilot CLI. **Grant-service assistants** still aren't woken.
- **Claude `launch_wrapper`** hasn't run with zsh, on macOS arm64 or with a real
installed Claude; an alias or shell function named `claude` fails closed.
- **Going back to 0.7.10** isn't established; an older permission hook also
gives named-policy Claude sessions no decision until they're relaunched.
- **The other 0.7.10 limits below still apply:** `rig exec` cancels,
permission requests in seat status, Codex hooks (including
`rig seat restart --keep-conversation` refusing a Codex seat launched with
developer instructions), smol-toml 1.9.x, Slack, Claude Code 2.1.319's auto
mode, Codex snapshot restores and `AGENTS.md`, unretried reminders, SSH host
status, advisories on a native resume, seats handed over before 0.7.9, `rig`
in the TUI's shell after a project-local install, the "Startup prompt still
staged" message, and a partial install from two npm installs at once.
0.7.10's bare-banner and launcher-script limits are fixed, and its OpenShell
limit is replaced by the one above.
- **`rig ps --nodes`** separates every cell with one space (the default row is 5 columns wider), adds an `ATTN` column
(`yes`, `no` or `?`) before `REASON`, and marks a `running` seat whose tmux couldn't be reached as unconfirmed.
- **`rig queue show --json`** no longer has `lastNudgeInputBox`, and send results no longer describe the input box.
- **`rig queue show` and `rig queue whoami`** print their human notices on stderr, so stdout is the JSON.
- **A claimed row whose owner waits on a person** reads pickup `needs-input`, with the reason.
- **`rig down --json`** adds `leftRunning`; `rig reconcile-session` says whether resume metadata was recorded.
- **`rig compact-plan`** labels its thresholds as its own and says whether automatic compaction is on.
- **A send held for a draft** in the seat's input box reports `inputPhase: "not_reached"`: it was never typed.
- **A stream body over 16 KiB** is stored truncated, ending `[truncated by OpenRig: kept N of M bytes]`, and reads mark it
`bodyTruncated`. `rig stream list --after <sortKey>` from a pruned item returns 410 `stream_cursor_expired`, naming
`--since` to resume, instead of an empty list.
---
## What to stop doing
## What changed in OpenRig 0.7.10
- **Don't read `lastNudgeInputBox`.** It's gone.
- **Don't parse `rig ps --nodes` by column position.** Use `--json`, with `--fields` to pick fields.
- **Don't look for a team seat's skills in the project folders.** Use `rig skill loadout --seat`.
- **Don't stop and re-register a watchdog to change it.** Use `rig watchdog update`.
- **Don't take a rig down to fix one edge.** Use `rig edge add` or `remove`.
- **Don't print `OPENRIG_ACTIVITY_*` values** while looking for identity. Use `rig whoami`.
- **Don't use `--dangerously-interact` to type over a half-written input box.** It stops and shows the text; choose
explicitly, or use `rig send --urgent` where the seat's interruption level allows it.
The changes from 0.7.9 to 0.7.10, published on 2026-11-08, for an install
upgrading from 0.7.9 or earlier. 0.7.10 added `rig exec` for finite runs on
Claude Code or Codex, sent a held post-compaction step after two idle polls that
can't read the input box, left every tmux mode before a delivery, and showed
agent spec advisories at launch. Its OpenShell line below is corrected.
## Known gaps in 0.7.1
### After the upgrade
- **A new Claude seat's startup text can stay in its input box unsent.** If a fresh Claude seat sits idle with text at
its prompt, press Enter in its pane, or ask the person to.
- **Claude seats on a macOS native install can read as needing attention after a restore.** Check the pane before
acting on it.
- **A Slack channel-mapping change needs a daemon restart.** Don't use `rig slack disable` then `enable` for it: enabling
marks any alerts waiting to post to Slack as already seen.
- **Ubuntu 22.04's tmux 3.2a:** the query known to crash its server is gone, but a narrower race remains. Avoid 3.2a
(`tmux -V`).
- **`rig daemon start` can take 20 to 40 seconds** before its listener warning, longer than the TUI's 30-second "restore
everything" wait. On a slow host, start the daemon yourself first.
- **A launch that warns "Scoped tmux environment unavailable; retaining the previous environment"** didn't give that seat
the clean environment. Check `env` in the seat before relying on OpenRig's variables.
- **Going back to 0.7.0 with a Slack subscription set:** 0.7.0 can't read a person file that holds one, and its people
registry stops loading. Save the choice first (`rig gateway human show <id> --json > <id>.json`), then run
`rig gateway human set <id> slack-subscription unset` (`urgent-only` becomes every-ask, as on 0.7.0); set it again after
returning to 0.7.1. A person file still holding a `subscriptions:` entry fails on 0.7.0 too. Don't restore an older
gateway folder or database instead: that can lose newer people, bindings and delivery records.
- **A Herdr tile over SSH** doesn't come back by itself after a Herdr restart. Reopen it with `rig terminal open <view>
--provider herdr`.
- **Delegated decisions:** the ask is created before the decision, so without quiet hours it can post to the person
first. Pair a decision room with quiet hours, or decide before asking.
- **`restore-check` still treats a missing startup file as rig-wide.** Trust `rig up --existing`'s per-seat result.
- **Managed Claude launch requires `/usr/bin/env` support for `-0` and `-u`** (option support checked on GNU coreutils
and macOS 15; BusyBox and older macOS unverified). Without the required option support, launch stops instead of
falling back to credential-bearing command-line arguments.
- **In draft-aware mode, an agent's multi-line message to a Claude seat can remain unsubmitted when the paste changes
Claude's footer hint.** OpenRig withholds Enter, leaving the text in the composer and later sends waiting. Observed on
Claude 2.1.309 with both 0.7.0 and the 0.7.1 pre-cut build; #14 remains open.
- **A Claude seat signed in with a long-lived token (`claude setup-token`) gets no usage reading.** The reader accepts
only claude.ai subscription logins and reports `not_subscription_login`.
- **Restart the daemon** to load the new code: `rig daemon stop`, then
`rig daemon start`. This release adds no migrations.
- **The bundled openrig-core plugin goes from 0.1.25 to 0.1.26.** Its Claude
hooks add a `PermissionRequest` hook that reports permission requests to seat
status, and its refocus hook and the refocusing skill's trace script changed.
Managed refresh preserves replaced files under
`$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits you still
need from the preserved copy.
- **Check agent specs before relaunching seats:** run `rig agent validate <path>`
and read any `agent spec advisory` line. A profile's `uses.<category>` written
as a single value now selects that resource; if the resource pool doesn't hold
it, a member using that profile fails to resolve with "not found in resource
pool". Ask the person before you change their spec.
- **Codex hooks:** a Codex seat kept running across the upgrade can show "Hook
failed — plugin contents changed; hook approval required" until relaunched; in
0.7.9's upgrade test, one seat restarted with OpenRig's managed restart showed
no review afterwards.
- **OpenShell:** rebuild a guest image with this release's `guest.py` before
using it; the CA repair is in that file, which the image carries.
- **Codex tool commands use a non-login shell:** managed Codex seats now pass
Codex `-c allow_login_shell=false`, so plain `rig` works however OpenRig was
installed, with a Codex that supports `allow_login_shell` (checked in Codex's
source at 0.162.1 and 0.166); not yet run with a live Codex seat. 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 refusal, "login shell is disabled by config". To
defer to Codex's own setting, run `rig config set launch.codex_login_shell
true`; a login file that sets `PATH` outright can then hide `rig`. It applies
at each seat's next launch, and a restart in place keeps the running choice.
([#2613](https://github.com/mvschwarz/openrig/pull/2613))
### Finite runs: `rig exec`
`docs/reference/acp-agents.md` is the rule. Read it before handing a row to
Claude Code or Codex.
- **Install the adapters first:** `@agentclientprotocol/claude-agent-acp`
0.101.0 and `@agentclientprotocol/codex-acp` 2.5.0, as that page shows.
`rig exec harnesses` reports the adapter and native versions it finds.
- **Run:** `rig exec run <row> --harness claude` (or `codex`). `--plan` shows
what would run and starts nothing; `--wait` waits for the outcome. A connector
owns the run, so leaving `--wait` or closing the terminal doesn't stop it.
Running it again returns the row's run instead of sending the task twice;
`--again` starts another turn once that run has ended.
- **Read the outcome** with `rig exec status <runId|row>`. `interrupted` means
the connector died without recording an outcome, so the outcome is unknown. A
`completed` turn is the agent's report, not finished work: check the work
before you close the row. `--wait` exits 0 for `completed`, 2 for any other
outcome and 3 when you stop waiting.
- **Cancel** with `rig exec cancel <runId>`, not `rig acp cancel`. An
acknowledgement isn't a settled shutdown, and it says nothing about further
spend.
- **Usage and cost** the agent reported are on the row's outcome note, for
`rig acp run` too, each with its source; what it didn't report reads unknown.
### Compaction and delivery
- **A held post-compaction step goes in.** After two idle polls in a row that
can't read a seat's input box, the next poll sends the step (the turn
boundary, the restore request or the audit). `rig compact <seat> --state` and
`rig seat status` say it went in over a box OpenRig couldn't read; they never
call that box empty. A draft OpenRig can't recognise in that box may receive
the restore text.
- **Still held:** a box that reads as typed, with or without a suggestion; a
seat whose draft-aware delivery or typing guard holds OpenRig's input; and the
step after a `/compact` whose result was lost. To take over a held restore by
hand, run `rig compact <seat> --cancel`, then send the seat a restore request
naming its newest restore map, its restore marker and its packet. A step
already being pasted can still land.
- **tmux modes:** a send or wake now leaves every tmux mode in a seat's pane,
including a chooser, customize or clock mode a person has open. If a send is
still refused for a tmux mode, the refusal names why leaving failed.
- **A scrolled pane with a stale record:** leaving the mode now goes through the
seat's address, so a stale binding naming the same reused tmux pane, from
another rig for example, no longer makes the send fail with nothing sent.
- **Herdr views kept across an upgrade keep working,** on Linux Herdr 0.9.5 /
protocol 22. A viewer or follow view kept from 0.7.5 through 0.7.9 now counts
as the same attachment when only OpenRig's tile script changed, and moves to
the current command, so the viewer stays confirmed and the follow view goes on
adding and closing tiles. A kept viewer that's already running keeps its
earlier sizing for as long as it runs, its own re-attaches included; writable
viewers opened after the upgrade size their pane. A different session, tmux,
server or read-only setting still holds the view, as before. Checked against
the scripts those releases used, not yet with a live Herdr. It follows
@natefaerber's approach in
[#2467](https://github.com/mvschwarz/openrig/pull/2467). ([#2634](https://github.com/mvschwarz/openrig/pull/2634))
### OpenShell workers
0.7.10 fixes, in the source, the two problems that stopped the OpenShell worker
in 0.7.9, but the worker hasn't run end to end. Don't rely on this path yet. The
guest launcher now passes the CA settings the OpenShell exec supplies
(`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE` and
`CURL_CA_BUNDLE`) to its Claude, and deleting an owned environment reads
Podman's inventory through routes Podman serves, stopping as
`environment_cleanup_unconfirmed` on any other answer. A verified export, a
finished task and an exact restore haven't been shown. If you set one up anyway,
follow `daemon/assets/openshell/README.md` in the installed package, rebuild the
guest image first, and name the costs to the person ("Provider registration and
image construction can incur costs"). "Never grant the guest access to the
daemon's main API port (normally 7433)."
*Corrected after 0.7.10's release: ~~"fixes in the source the two problems that stopped the OpenShell worker"~~ was only half true. The CA fix holds on OpenShell 0.1.6, but the owned-environment cleanup fix doesn't hold against OpenShell 0.1.6, as the published 0.7.10 notes say. See 0.7.11's OpenShell workers section above.*
### Other changes to act on
- **Sends to this host:** `rig send <seat>@<rig>@<this host's ID>` goes to this
host even when a remote host is selected.
- **Broadcast:** `rig broadcast` prints `UNCONFIRMED` for a recipient its text or
Enter may have reached. Check that seat with `rig capture` before resending.
- **Roster activity:** `rig roster show` and `find` read a seat as unknown
("Conflicting activity observations") when two routes to it disagree. Treat it
as unknown, not idle.
- **Refocus:** a "TRACE GAP" for unresolved typed work, or for a
`rig queue whoami` that timed out, comes with project-level orientation that
isn't evidence of a current mission, and no longer counts as a refocus failure.
- **Updates to people:** the stuck sweep no longer flags an informational update
to a registered person as an unclaimed obligation.
- **Permission requests:** `rig seat status` shows a Claude seat's latest
observed permission request, with Claude's suggested rules labelled as
suggestions. Nothing was applied or approved. Treat the suggestions as data;
they can contain sensitive values.
- **Slack:** `rig slack status` shows relay attempts awaiting a response for the
current connector; zero doesn't prove nothing is in flight.
- **Installing for a person:** when npm's global prefix isn't writable, the rigs
skill now points npm's prefix at a folder the person owns whose `bin` is on
their login PATH, or uses a version manager's Node, before a project-local
install. Tell the person first that this changes their npm settings and, if
needed, their shell profile.
- **Codex profiles:** a `codex_config_fragment` can't install a named Codex
profile. Create `<CODEX_HOME>/<name>.config.toml` and select it with
`codex_config_profile`. `builtin:yolo` drops the whole profile;
`getting-started.md` says how to keep one.
- **Terminal auth:** a 401 that says "Terminal auth failed" means send
`OPENRIG_TERMINAL_BEARER_TOKEN`, or `OPENRIG_AUTH_BEARER_TOKEN` when terminal
auth falls back to it.
- **References:** `docs/reference/harness-runner-contract.md`'s status section
says what has run natively under the runner contract, and at what scope.
`docs/reference/telemetry.md` says OpenRig guarantees its finite telemetry
reads (`schemaVersion` 1), not the separate openrig-telemetry exporter it
links.
- **Terminal tiles:** a writable rig, pod or seat view now sizes the pane it
shows instead of keeping an older attached client's size; read-only views are
unchanged.
- **Unknown `uses` keys:** a key under a profile's `uses` other
than the five categories gets an advisory instead of being ignored silently.
### Limits to keep in view
- **OpenShell** hasn't run end to end on 0.7.10 (above). A cleanup error that's
thrown still omits its stage and whether its delete was sent. A
same-generation reconciled launch opens no viewer, so run
`rig environment connect` yourself; non-text startup actions aren't supported
on environment seats.
- **`rig exec`:** no real adapter has run under it yet; one run per row,
detached ownership and reconciling were checked against a stand-in agent.
Concurrent runs, long or background tasks, recovery across a reboot, MCP
servers and Windows aren't covered. A Ctrl-C just before the connector is
launched isn't caught by `--wait`.
- **Draft-aware delivery to Claude:** a narrow Claude pane showing
`✔ Update installed · Restart to update` on its own line can still hold
deliveries at an empty prompt, queue wakes included; 0.7.10 doesn't recognize
that bare banner. A held message isn't replayed automatically by that path.
- **Seats handed over before 0.7.9** keep their adopted origin until a later
launched handover or managed launch records a new session. Until then
`rig seat stop`, `clean` and in-place restart refuse them, and now say why.
Don't use `rig unclaim` to get around it: that ends OpenRig's management of
the seat.
- **Permission requests in seat status** show only the latest one, held in
memory for five minutes and lost on a daemon restart.
- **Agent spec advisories** don't show for a native resume that continues a
saved conversation.
- **The other 0.7.9 limits below still apply:** open updates for a person, Slack,
SSH host status, shared asks, permit records, existing seats, Codex hooks, and
grant-service assistants not being woken. 0.7.9's limits on a restore request
held after compaction, `rig broadcast`'s `FAILED`, what has run under the
runner contract, and Codex seats' `rig: command not found` are fixed or
changed. The 0.7.8 limits below that 0.7.9
didn't fix or change, and the
[0.7.7](https://github.com/mvschwarz/openrig/releases/tag/v0.7.7) and
[0.7.6](https://github.com/mvschwarz/openrig/releases/tag/v0.7.6) limits, still
apply.
---
## What changed in OpenRig 0.7.9
The changes from 0.7.8 to 0.7.9, published on 2026-11-06, for an install
upgrading from 0.7.8 or earlier.
### After the upgrade
- **Restart the daemon** to load the new code: `rig daemon stop`, then
`rig daemon start`.
- **Migrations 146 to 149** record a Pi seat's guidance file, what OpenRig last
wrote in each managed guidance block, an index for reading a rig's outbox,
and the stream intake position.
- **Tell the person that stream intake is on.** Each `rig stream emit` now opens
an intake row for the kernel queue worker (`queue.worker`), or adds a note to
the one still unclaimed, and the row wakes the queue worker like any queue row.
Earlier stream history isn't replayed. If they want 0.7.8's behaviour, with no
intake rows, run `rig config set stream.intake off`.
- **The bundled openrig-core plugin goes from 0.1.24 to 0.1.25.** Its
seat-continuity-and-handover skill changed. Managed refresh preserves replaced
files under `$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits
you still need from the preserved copy.
- **Where seat guidance goes now:**
- a fresh Pi seat gets OpenRig's guidance in its own agent folder's
`AGENTS.md`, unless that file or folder is a link;
- a fresh Codex seat on Codex 0.164.0 or later gets it as launch developer
instructions, not in the repository `AGENTS.md`, when no config layer
OpenRig reads sets `developer_instructions`;
- a Codex resume, fork, restore or handover successor, an older or unknown
Codex, and a config layer that sets `developer_instructions` or can't be read
still use the repository `AGENTS.md`;
- if only the person's shell startup sets `CODEX_HOME`, OpenRig reads
`~/.codex`, so a `developer_instructions` set in their home is replaced for
that session. Declare the same absolute path as `CODEX_HOME` in the member's
or rig's `env:` to have it checked. `docs/reference/agent-startup-guide.md`
("Codex guidance at launch") is the rule;
- a managed block a person edited by hand is kept and named
(`guidance_block_edited`), not replaced. Read the warning before you change
the block.
- **Slack:** on the first start after upgrading, the connector edits the asks
that ended earlier to show they were handled, at Slack's edit rate: about an
hour for 450 asks. `rig slack status` shows pending edits.
- **OpenShell:** rebuild a guest image built for 0.7.8 with this release's
helper; an older helper rejects the new startup step, and the error names the
rebuild.
### Runner seats
Section 7 of `docs/reference/harness-runner-contract.md` is the rule for
permissions, and section 4 for activity. Read them before writing or changing a
runner.
- **Typed words:** a reply of just yes or no counts as the agent's allow or
reject. Any other words choose nothing: OpenRig's default permits the request
when the agent offered an allow option, and the read result carries the words
whole as `said`. A runner may queue them for the agent as an ordinary prompt
after the current turn, never as the answer; one that drops them still keeps
the contract. The ask now tells the person this.
- **Permit records:** each permit OpenRig decides by default (no one to ask, or
an ask closed without a choice, including a row passed to an agent seat) is
recorded once, as a stream item from the seat, naming the tool call, why no
person answered and the option given. Read them with
`rig stream list --tag runner-permission-permit` or `--source <seat>`. A record
has no destination and never changes the permit. Stream intake reads past it, so
it opens no intake row and wakes no one.
- **The working beat:** while a prompt is outstanding and the agent is alive, a
runner may post `active` / `liveness` so a long call keeps reading working.
After delivering an answer or permit, it posts `active` /
`permission_resolved`. A runner that sends no beats behaves as before. The Pi
runner and the dsh example post it.
- **Check a runner** with
`node daemon/docs/reference/runner-conformance/check-runner.mjs --runner '<command with {agent}>' --name <harness>`.
It now covers the beat, and no longer falsely fails a correct runner on a busy
machine.
- **The dsh example** reads `DSH_SEAT_BIN` and `DSH_SEAT_HOME` from the member's
`env:`, and section 8 names ACP's `session/resume`.
### OpenShell workers
0.7.9 includes a repair for the OpenShell worker that wasn't usable in 0.7.8,
but the worker still doesn't work in 0.7.9. Don't rely on this path in 0.7.9.
Startup reaches the guest through a hash-checked first-input file, and nothing
is typed through the host viewer. Readiness checks the guest's own execution,
and a failed owned delete reports the stage that failed. Release testing on
0.7.9 then found two more problems: the guest's Claude couldn't reach its model
(`SELF_SIGNED_CERT_IN_CHAIN`), because the guest launcher left out the CA
settings the OpenShell exec supplies, and deleting an owned environment never
sent the delete, because its inventory step misread a "Not Found" reply. 0.7.10's
fixes for both are described above. "Never grant the guest access to the
daemon's main API port (normally 7433)."
### Assistants connected through a grant service (experimental)
Experimental, unsupported, off by default, and at the person's own risk. With
`rig mcp serve --http --grant-service <url>` and a grant service the person
supplies and runs, a connected assistant can claim, update and park the rows
addressed to its own seat, and see its team with `rig_team`, within the scope its
existing tools have. Nothing wakes it: an item stays queued until its receiver
claims it. Name the outside service, and the exposure of serving MCP over HTTP,
to the person before you set this up. `docs/reference/hosted-members.md` is the
rule.
### Other changes to act on
- **Held messages:** a seat that starts, or takes over a seat, with held
messages is told how many there are. Read them with
`rig seat held-messages <seat>`.
- **Unconfirmed sends:** `rig send --verify` now reports unconfirmed and exits 1
when it can't see what its Enter did, and `rig walk` reports unconfirmed
instead of lost. Inspect the recipient before any resend.
- **Deadlines:** to accept a re-estimate without unclaiming the work, run
`rig queue update <id> --closure-required-at <ISO>`.
- **Shared workspaces:** any registered person can settle another's ask as
themselves, and the first committed answer wins; a later answer gets 409
`qitem_already_terminal` and records nothing. A person registered with no
connector binding is asked on Steering and in the Feed only.
- **Handover:** a fresh or rebuild successor's first turn lists its startup
files, role first. A successor that started without a required startup file is
marked in the seat list.
- **Ambiguous rig names:** when an active rig name matches more than one rig,
`rig env down`, `status` and `logs` list the matching rig IDs and stop, and
`rig doctor --spec` reports conformance as unknown. Use the rig ID.
- **Seat activity:** window samples older than their bound no longer decide a
seat's activity, so a seat whose pane can't be read reads unknown rather than
its last verdict. Treat unknown as unknown, not idle.
- **Launch options:** `specs.builtins_visible: false` hides built-in specs
(`rig specs ls --include-builtins` still lists them); `plugin_channels` gives
one Claude member its own `--channels`, untested with a real channel plugin.
- **Files:** `rig down` removes OpenRig's own additions from a working folder
once no live seat uses them. `rig export` with no `-o` writes
`rig-export.yaml` beside an existing `rig.yaml`. `rig workspace validate` exits
1 for malformed optional frontmatter.
- **Kernels:** a new default kernel starts in `<workspace.root>/kernel`.
Existing kernels stay where they are. `docs/reference/agent-startup-guide.md`
explains the optional move; a fresh launch there starts a new native
conversation, so ask the person before moving.
- **PATH:** if preflight warns that a Codex seat's login shell can't find `rig`,
follow its fix. `getting-started.md` explains the `~/.profile` line and how to
check it with `bash -lc 'command -v node rig'`.
### Limits to keep in view
- **OpenShell:** the worker doesn't work in 0.7.9 (above). A same-generation
reconciled launch opens no viewer, so run `rig environment connect` yourself;
non-text startup actions aren't supported on environment seats.
- **Open updates for a person** read a 1,001-row window, so a page can come back
short, marked `truncated`.
- **Slack:** a private channel needs the documented route, which isn't tested,
and direct messages aren't supported. The channel and people reads haven't
been run against a live Slack workspace. Redaction of the bot token from a
transport error can still leave a malformed token fragment shorter than six
characters, so review an error before sharing it.
- **SSH host status** reads `unknown` behind a ProxyJump or ProxyCommand; that
isn't proof the connection is broken. `rig host ls` reads the ssh config with
`ssh -G`, which runs any `Match exec` command in it.
- **Shared asks:** a click's option written before its answer row can stand
beside or inside the closer's answer without the first clicker's authorship.
- **Permit records** last as long as stream retention keeps them. A permit a
runner makes on its own, when it can't reach OpenRig, is in its pane only. For
an ask opened before 0.7.9, a record may omit the option's kind.
- **Existing seats** pick up Codex launch instructions and the single
openrig-core skill load at their next fresh launch. A `CLAUDE.md` or
`AGENTS.md` that already grew a line per launch isn't trimmed.
- **Codex hooks after an upgrade:** Codex sessions kept running across the
upgrade show "Hook failed — plugin contents changed; hook approval required"
until relaunched.
- **0.7.8's limits** on the dsh `env:` names, conformance false failures, typed
words, the README's operator address, a symlinked rebuild, the rebuild receipt,
a re-parked click and unrecorded default permits are fixed or changed. The
other 0.7.8 limits below, and the
[0.7.7](https://github.com/mvschwarz/openrig/releases/tag/v0.7.7) and
[0.7.6](https://github.com/mvschwarz/openrig/releases/tag/v0.7.6) limits, still
apply.
---
## What changed in OpenRig 0.7.8
The changes from 0.7.7 to 0.7.8, published on 2026-11-05, for an install
upgrading from 0.7.7 or earlier.
### After the upgrade
- **Restart the daemon** to load the new code: `rig daemon stop`, then
`rig daemon start`.
- **Migrations 142 to 145** store a Claude seat's MCP opt-out, seat activity
grants, OpenShell environment bindings and a seat's chosen permission person.
- **The bundled openrig-core plugin goes from 0.1.23 to 0.1.24.** One skill line
changed. Managed refresh preserves replaced files under
`$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits you still
need from the preserved copy.
### Runner seats: a person answers permission requests
Section 7 of `docs/reference/harness-runner-contract.md` is the rule. Read it
before writing or changing a runner.
- **Who is asked:**
- **the default person** (`rig gateway default`, or the only registered
person) gets an ordinary ask in the Feed and on Slack;
- **a chosen person,** after
`rig seat set-permissions-to <seat> --to <person> --reason "…"`. `--clear`
returns to the default;
- **no one:** when no one can be asked, the request is "Permitted at once,
with the agent's own allow option";
- **refusal** is opt-in, with `--deny`. `rig seat status` shows the choice.
- **What a person's answer does:** a click gives that option, a plain yes gives
the allow, and a plain no gives the reject. "A person's no is never a grant."
An unanswered request stays pending until the agent's turn ends.
- **The runner's protocol:**
- `POST /api/activity/permission-requests` submits the request;
- `GET …/<askId>?waitSeconds=25` reads the answer (25 at most);
- `POST …/<askId>/withdraw` withdraws it at the turn's end.
Deliver an answer once, only to the live request.
- **Check a runner** with
`node daemon/docs/reference/runner-conformance/check-runner.mjs --runner '<command with {agent}>' --name <harness>`
from the installed package. It needs Node and tmux, and no daemon, model or
network.
- **The worked example** is the dsh runner under
`daemon/docs/reference/community-harness-examples/dsh/`.
- **A seat activity grant** lets a runner outside the host's hook setup report
activity for one seat and launch, without the install-wide hook token.
### OpenShell workers
0.7.8 includes `rig environment` (`setup`, `create`, `attach`) and a reference
for binding a seat to an NVIDIA OpenShell sandbox (`daemon/assets/openshell/README.md`).
**In our release testing the worker never became usable:** after the sandbox was
created and the native process started, `rig environment connect` failed when its
check timed out after 5 seconds, so no team work ran. Don't rely on this path in
0.7.8; the later repairs are described above.
**What it needs:**
- NVIDIA OpenShell 0.1.6;
- a Linux image;
- an API-backed provider using `ANTHROPIC_API_KEY`. Subscription or OAuth
credentials aren't supported.
"Provider registration and image construction can incur costs", so name them to
the person before you start. "Never grant the guest access to the daemon's main
API port (normally 7433)."
### Other changes to act on
- **Mission following:** `rig terminal open mission:<id> --follow --provider herdr`
follows a mission's current owners, with the same Herdr path as slices.
- **Launch choices:**
- `claude_mcp: none` on a Claude member launches it without MCP servers;
- `rig config set terminal.tmux_server isolated` gives this OpenRig home its
own persistent tmux server.
- **Hosts:** `rig host edit` and `rig host remove` correct or retire a host
entry. Use them instead of editing `hosts.yaml` by hand.
- **Another user's daemon:** when `rig doctor` warns that the port is held by
another OS user's daemon, follow its fix (`rig config set daemon.port <free-port>`),
or set `OPENRIG_URL` to share that daemon on purpose.
- **tmux control mode:** with `OPENRIG_TMUX_CONTROL=1`, OpenRig starts a control
client only on tmux 3.7c or newer, and uses spawned clients otherwise.
- **Read-only tmux clients:** a submit key refused because of one goes through
the paste route.
- **Slack:** a typed reply to a decision parked on its person is recorded as the
answer. Read the person's words; don't treat the close as approval.
- **The TUI:** agent pages now show health findings and recently finished items,
and an agent spec's "not loaded in this view" means it wasn't read, not that
nothing runs it.
### Limits to keep in view
- ~~**No harness has run end to end under the runner contract yet.** The dsh
example and the conformance check were checked against stand-ins.~~ *Corrected
after release:* 0.7.8's release testing ran a real dsh 0.2.1-rc.2 through the
shipped dsh example runner for a person's allow-once and reject-once, and
through an outside runner, dsh-seat 0.3.0, for launch, queue work, multiline
input, activity, cancel and exact resume. The conformance check itself runs
against a stand-in agent.
- **The answer owner is a registered person,** and a colleague's answer is
followed on this host only. A request handed to another host waits as needs
input until the turn ends.
- **Permits and answers aren't recorded** beyond the runner's pane and the ask.
If the agent offers only `allow_always`, a permit makes its harness remember a
permission nobody chose.
- **Coming as follow-ups:** recording permits made when no one can be asked, and
passing typed words that aren't a plain yes or no to the agent.
- **The dsh example keeps one hold per seat:** two asks in one turn can leave one
waiting after the hold is released.
- **An OpenShell worker's activity reads unknown,** sampled from the pane, never
idle.
- **The [0.7.7](https://github.com/mvschwarz/openrig/releases/tag/v0.7.7) and
[0.7.6](https://github.com/mvschwarz/openrig/releases/tag/v0.7.6) limits still apply.**
+5 -5
View File
@@ -1,12 +1,12 @@
{
"name": "openrig",
"version": "0.7.1",
"version": "0.7.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openrig",
"version": "0.7.1",
"version": "0.7.11",
"license": "Apache-2.0",
"workspaces": [
"packages/daemon",
@@ -7298,7 +7298,7 @@
},
"packages/cli": {
"name": "@openrig/cli",
"version": "0.7.1",
"version": "0.7.11",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -7336,7 +7336,7 @@
},
"packages/daemon": {
"name": "@openrig/daemon",
"version": "0.7.1",
"version": "0.7.11",
"license": "Apache-2.0",
"dependencies": {
"@hono/node-server": "^1.13.0",
@@ -7379,7 +7379,7 @@
},
"packages/ui": {
"name": "@openrig/ui",
"version": "0.7.1",
"version": "0.7.11",
"license": "Apache-2.0",
"dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.2.8",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openrig",
"version": "0.7.1",
"version": "0.7.11",
"private": true,
"license": "Apache-2.0",
"type": "module",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@openrig/acp",
"version": "0.7.1",
"version": "0.7.11",
"description": "OpenRig as an agent in ACP apps: starts rig acp serve from the @openrig/cli it pins.",
"type": "module",
"bin": {
@@ -15,7 +15,7 @@
"node": ">=22"
},
"dependencies": {
"@openrig/cli": "0.7.1"
"@openrig/cli": "0.7.11"
},
"license": "Apache-2.0",
"author": {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"id": "openrig",
"name": "OpenRig",
"version": "0.7.1",
"version": "0.7.11",
"description": "Send work to your OpenRig team of agents and follow it through to the result",
"repository": "https://github.com/mvschwarz/openrig",
"website": "https://openrig.dev",
@@ -12,7 +12,7 @@
"license_url": "https://github.com/mvschwarz/openrig/blob/main/LICENSE",
"distribution": {
"npx": {
"package": "@openrig/[email protected]"
"package": "@openrig/[email protected]1"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@openrig/cli",
"version": "0.7.1",
"version": "0.7.11",
"type": "module",
"description": "Build your own network of agents from Claude Code, Codex and Pi: persistent teams with roles, shared context and owned work.",
"keywords": [
+1 -1
View File
@@ -128,7 +128,7 @@ Startup uses a private, hash-checked `control/first-input.txt` file and a bounde
The placement name is supplied by the caller. Create refuses an already-listed name, and each binding retains its one selected name; OpenRig does not reserve names globally across bindings. Immediately before stop/delete, OpenRig checks the saved immutable identity, then the pinned vendor call addresses the name and workspace. A deliberate reuse of that name between inspection and the vendor call could affect the replacement. The pinned CLI has no expected-id condition for these calls. Later reconciliation cannot undo that mutation.
Exact restore onto recreated compute stages the current resolved role, skills, plugins and required startup files as destination files, including files originally intended only for a fresh start. Declared context changes, additions and removals are allowed. Current source and destination bytes are verified through the ordinary projection checks; a missing or stale required destination refuses launch. Startup actions and text are not replayed into the conversation. The checkpoint pins the original native history and verified artifact bytes, which remain separate and unchanged; it does not freeze the context.
Exact restore onto recreated compute stages the current resolved role, skills, plugins and required startup files as destination files, including files originally intended only for a fresh start. Declared context changes, additions and removals are allowed. Current source and destination bytes are verified through the ordinary projection checks; a missing or stale required destination refuses launch. Saved startup actions are not resent, but the restored guest's first input asks it to read the projected `CLAUDE.md` and its startup files again, so it takes in its startup content again and strict recall after a restore isn't met. The checkpoint pins the original native history and verified artifact bytes, which remain separate and unchanged; it does not freeze the context.
A pending finite-completion attempt records its last outcome and attempt time in the existing operation receipt. Queue events and boot/manual checks observe it at most once per 60 seconds; there is no new polling timer. Eligibility is checked again after the driver's identity await, immediately before its stop command. A known refusal before dispatch releases only the unstarted effect claim; an uncertain dispatched request is never replayed. With no pending selection, a queue event performs one filtered query and no provider call; the operation table has no additional index or migration in this increment.
@@ -11,7 +11,7 @@ not know exists.** So the point of what follows is not skill. It is recognition:
lands, something rhymes, and you go check instead of building.
Ninety-two top-level verbs ship (capability canon refreshed through
`capability-delta-v0.7.10`). This marker describes the pack's teaching, not publication or live
`capability-delta-v0.7.11`). This marker describes the pack's teaching, not publication or live
adoption. Read this once for shape, and let it make you suspicious that a thing already exists.
What changed in the installed version, and what to do about it, is one read away:
`rig context get reference/whats-new.md`.
@@ -134,6 +134,12 @@ unseen — which makes messages the one delivery channel that never gets skipped
`caller_cancelled` and never types later. **`--urgent`** marks a message urgent, as does a row
at priority `urgent` or `critical`: its wake starts `Urgent (marked by …):`, and it gets through a
seat whose typing guard lets only urgent input type (`openrig-urgent` or `urgent-only`, below).
**A seat whose prompt holds a person's typed draft gets the message joined to it** by default
(`transport.draft_delivery` `deliver`): the message is pasted at the cursor and the combined input
submitted as one prompt, and the result says when it joined a draft OpenRig saw. Tell the person.
`rig config set transport.draft_delivery refuse` brings back the refusal (`prompt_draft`) from the
next send. Menus, permission prompts, a seat's typing guard, managed compaction and
`--wait-for-idle` refuse or wait under either value. Draft detection isn't complete.
- **`rig send --raw`** — send exact text or keystrokes without the From/To envelope;
the interactive-prompt guard still applies. To deliberately drive an interactive prompt,
**`--dangerously-interact --reason "<why>"`** is the explicit override and implies raw text.
@@ -208,6 +214,15 @@ unseen — which makes messages the one delivery channel that never gets skipped
judge and close. Each permission request is a row to `--permissions-to` (default: you). The guide
is `docs/reference/acp-agents.md` in the source repo, and `docs/reference/a2a-agents.md` covers
A2A agents.
- **`rig acp serve --to <member@rig>`** — the other direction: the team as an agent inside an ACP
app that launches a custom agent over stdio (Zed's `agent_servers`, T3 Code). A prompt becomes a
queue row, each row the work reaches shows as a step, and the closing resolution is the reply.
The team's questions reach the app as a permission request or a one-choice form, and the choice
is recorded as the person's answer; one left unanswered there stays open in the TUI and Slack.
`acp.to` sets the default receiver. Nothing runs until an app starts it, and it uses the daemon
`rig` uses (`OPENRIG_URL` can name another). The `@openrig/acp` launcher runs the matching
CLI's `rig acp serve` for the ACP registry's `npx` launch. A cancel stops waiting; the work
carries on. It's been checked in T3 Code and with a scripted client.
- **`rig exec run <row> --harness claude|codex`** — a finite task for Claude Code or Codex without
building a seat: the same single turn through that harness's ACP adapter, run by a detached
connector that outlives the command, so closing the terminal or leaving `--wait` doesn't stop it.
@@ -228,7 +243,10 @@ unseen — which makes messages the one delivery channel that never gets skipped
wait for a phrase like “show me my agents”; the same command brings their agents back later. No
YAML, herdr when installed or plain tmux. The layout follows the terminal's width: on a narrow
one the operator gets the first tab; on a wide one the dashboard and operator sit side by side
and the advisor gets its own tab. A `kernel` view the person saved still wins. Inside herdr it
and the advisor gets its own tab. A `kernel` view the person saved still wins.
Tiles show their seat's screen without attaching, so whole lines show at any tile width; a
writable tile attaches when someone types or pastes and goes back to showing after 30 seconds
without a key. Inside herdr it
opens a focused `openrig kernel` space. It opens where the caller is: Terminal gets a window and
Ghostty a tab; from Claude Desktop, iTerm or VS Code on a Mac it opens a new Ghostty or Terminal
window, and only then say that clicking macOS's Allow is fine. Herdr is visible only in a
@@ -348,6 +366,10 @@ your context, your compaction and your replacement — and it is the only thing
everything.
- **`rig queue show`** — what a row *actually says*. The header is not the body. On a closed row,
`--full` adds `closingTransition`: how it ended, who closed it and the closing note.
`rig queue show`, `list`, `transitions`, `update` and `block` take `--host <id>` for a row on
another host, as `create` and `handoff` do; `claim` doesn't, since only the destination seat
claims, on its own host. `rig queue artifact <qitemId>` (and MCP `rig_artifact_read`) returns the
exact commit pair from an `openrig.code-result/v1` manifest set as the row's `evidenceRef`.
- **`rig queue claim` / `unclaim`** — is this mine and running, or still pending where two agents
might double-work it. Put it down honestly when it is not yours.
- **`rig queue update`** — record what happened in a form the rest of the system can act on. A
@@ -468,7 +490,9 @@ relay is a reason to inspect the available routing, not to surrender judgment to
inspect `rig gateway human list --json`; when several people are registered, choose a default
with `rig gateway default <id>`, or name one in `workspace.operator_seat_name`, which is read first.
- **`rig workflow show` / `trace`** — what this instance is, and every step, actor and exit that
got it here.
got it here. `trace --full` returns the whole trail, and `show`, `trace` and `status` print the run's
`objective:`. **`rig workflow unregister <name>[@<version>]`** (or `--prune-missing`) removes
registered versions; a running instance keeps the spec it started with.
- **`rig workflow revise <instance>`** — compare the bound graph with current authored
inputs before changing a progressed run. Ordinary show and TUI inspection distinguish
source-only edits (such as a catalog address) from executable changes. Preview names
@@ -580,6 +604,14 @@ not have to tear one down to change it, and you rarely have to start from nothin
selects only the tmux server: home, credentials, filesystem and network stay shared.
**`rig up <spec> --name <rig>`** runs one spec as another rig under that name; the rig keeps the
name for `--existing`, snapshots and restore, and the spec file isn't changed.
- **`launch_wrapper`** on a local Claude Code, Pi or Codex member is an argv prefix whose launcher
prepares the environment and execs the agent; a wrapped seat never starts unwrapped. It hasn't
run with zsh or with a real installed Claude.
- **A person can work with the team from their own Claude Code or Codex** through the `openrig`
plugin, shipped beside openrig-core (its README in the installed package has the install
steps): hand it work, follow it, answer the team's questions in their own words, and read the
result. VS Code, Copilot CLI and Cursor load it with no seat: give the app its own
`rig mcp serve --front-door` server with `OPENRIG_SESSION_NAME` set and turn the plugin's off.
- **A Claude member can bring its own setup** in the rig spec: `agent_file`,
`append_system_prompt_file`, `plugin_dirs` and `advisor` (an Advisor model). They're copied into
that seat's own storage, never the project, so one seat in a shared repository can have its own
@@ -720,6 +752,9 @@ version, from checking the environment to a useful report to the OpenRig team.
Stop distinguishes process/listener exit from a clean asynchronous drain: inspect the matching
`daemon-shutdown.json` and log when completion is incomplete or unverified. No target is a
distinct no-op, and a timeout never proves down. Use command help for exact limits and recovery.
On Linux, when the daemon runs as a systemd user service, the seats' tmux server starts in its
own user scope, so stopping or restarting the daemon's unit keeps the seats; otherwise it starts
as before, with one log line giving the reason.
Startup brings back a kernel that this start found down (what a reboot leaves). A kernel that
was already down before this start stays down: `rig start` and `rig daemon start
--wait-for-kernel` say so, and `rig up kernel --existing` brings it back.
@@ -1383,9 +1418,11 @@ your circumstances is configuration, and the ones that are not, another agent ca
stop them without killing the rig.
- **`rig environment`** — not `rig env`: places a worker seat in an NVIDIA OpenShell sandbox and returns
verified result files (`setup`, `create` or `attach`, `export`; reference in the packaged
`daemon/assets/openshell/README.md`). In 0.7.8's release testing the worker never became usable;
0.7.10 fixes in the source the two problems that stopped it in 0.7.9, but it hasn't run end to
end. Read the installed version's release notes before relying on it.
`daemon/assets/openshell/README.md`). In 0.7.8's release testing the worker never became usable.
In 0.7.11, on OpenShell 0.1.6, the maintained cleanup works for a custom image, and recreate
and an exact resume passed in native testing; a restored guest still receives its startup text
again, and the worker isn't end to end. Read the installed version's release notes before
relying on it.
- **`rig setup`** — what OpenRig would change about this machine, shown before it touches anything.
It installs herdr by default on macOS and Linux (`--no-herdr` declines; if the install fails,
views fall back to plain tmux) and on a Mac offers Ghostty once (`--ghostty` / `--no-ghostty`).
@@ -1476,6 +1513,12 @@ your circumstances is configuration, and the ones that are not, another agent ca
get a standard challenge; the key and its grant still decide what a caller may do. The rows a
granted caller's actions write record whose grant acted: attribution, not authentication.
Clients on MCP protocol revision 2026-07-28 and on the 2025 revisions share the same endpoint.
With a grant service (experimental and off by default; you serve it to the internet behind
your own tunnel), `--task-destination <member@rig>` names where a caller's `rig_queue_create`
goes when it gives no destination, and a seat's name alone resolves within the grant's team. A
grant that names a person adds `rig_question_list` and `rig_question_answer`, so that person
answers the team's questions from their assistant, recorded as their answer through that
connection.
**An assistant connected this way can be a hosted member of a rig:** mark its roster entry
`"hosted": true` and `rig_team` lists it with its declared capabilities and caveat, activity
`unknown`. The server's instructions teach it the loop: its inbox, claiming, closing or handing
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@openrig/daemon",
"version": "0.7.1",
"version": "0.7.11",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -1,7 +1,7 @@
import type { TerminalActor, TerminalPolicyStore } from "./terminal-policy.js";
import { TerminalPolicyRefusal } from "./terminal-policy.js";
import { DeliveryGuardError } from "./seat-delivery-guard.js";
import { CAPTURE_READABLE_BUDGET_MS, type SessionTransport, type SendResult } from "./session-transport.js";
import { CAPTURE_READABLE_BUDGET_MS, SEND_READINESS_FRESHNESS_MS, type SessionTransport, type SendResult } from "./session-transport.js";
import type { ClaudeCompactionPolicy, SettingsStore } from "./user-settings/settings-store.js";
import { postCompactExtraSetting } from "./managed-restore-read-access.js";
import * as fs from "node:fs";
@@ -718,7 +718,8 @@ export class ClaudeCompactionEnforcer {
this.now = opts?.now ?? (() => Date.now());
this.sleep = opts?.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
this.postCompactSendWaitMs = opts?.postCompactSendWaitMs ?? POST_COMPACT_SEND_WAIT_MS_DEFAULT;
this.compactSendIdleWaitMs = opts?.compactSendIdleWaitMs ?? 1000;
// A fresh preparation hook can veto even a visibly idle pane for the full send-readiness window.
this.compactSendIdleWaitMs = opts?.compactSendIdleWaitMs ?? SEND_READINESS_FRESHNESS_MS;
this.restartRecoveryWindowMs = opts?.restartRecoveryWindowMs ?? RESTART_RECOVERY_WINDOW_MS_DEFAULT;
this.daemonInstance = opts?.daemonInstance ?? DAEMON_PROCESS_ID;
this.resolveOccupantGeneration = opts?.resolveOccupantGeneration;
@@ -220,6 +220,12 @@ export class SeatLaunchEnvironment {
].join("\n");
}
const assignments = Object.entries(env).map(([key, value]) => shellQuote(`${key}=${value}`));
// A long Pi command is exec'd from a staged script, and exec takes one command: `exec unset …; …` started no agent.
// Its account step goes to env as -u instead (the selected home is already an assignment above).
if (target.runtime === "pi" && accountStep) {
const unset = [...(clearing && homeVar ? [homeVar] : []), ...(accountHome || selectedPiHome ? scrub : [])];
return `/usr/bin/env ${unset.map((name) => `-u ${name}`).join(" ")} ${assignments.join(" ")} PATH=${shellQuote(binDir)}:"$PATH" ${prefix}${command}`;
}
return `${accountStep}/usr/bin/env ${assignments.join(" ")} PATH=${shellQuote(binDir)}:"$PATH" ${prefix}${command}`;
} catch {
// This best-effort correction is not a new launch admission gate. Do not
@@ -61,7 +61,7 @@ import { getLatestStartupFailedEvent, hasClaudeResumeIdentityWork, isRecoverable
// (daintree #3938) cite Claude 1-3s / Codex 3-5s inter-tool-call gaps with a 6s idle debounce. A 15s
// window comfortably spans one inter-tool gap (so a mid-turn reading stays trusted) while refusing to
// authorize a send from a reading tens of seconds old. (EXA: agtx#14, daintree#3938.)
const SEND_READINESS_FRESHNESS_MS = 15_000;
export const SEND_READINESS_FRESHNESS_MS = 15_000;
// transport.verify_input_box: each extra input-box read gives up after this long and reads `unavailable`. A deadline
// budget, not a guarantee: the pre-Enter read comes after the existing 200 ms wait and can add about this much before
@@ -57,7 +57,11 @@ export function tileAttachArgv(
// into a file, through any paste it opens, with a UTF-8 character it ends inside completed. Nothing passes through
// a shell variable, so every byte is kept, NUL included, and flow-control and other special keys are read as keys.
// - Keys alone go in the attach command itself, through the new client (send-keys -K): tmux runs them for that
// client before anything typed since, so its key table applies, the prefix included.
// client before anything typed since, so its key table applies, the prefix included. That needs a tmux 3.5 or later
// server (3.3a has no -K, and 3.4 drops keys sent that way). The server's own #{version} decides, read just before
// the attach: a newer tmux binary can still be talking to an older server. Below 3.5, or when the version can't be
// read, they go to the pane before the tile attaches: a prefix typed first then reaches the seat instead of acting
// as the prefix.
// - A burst with a paste goes to the pane in order before the tile attaches: keys, then the paste (paste-buffer
// frames it if the agent asked), then keys. tmux reads anything typed since only once the tile has attached.
// The tile's terminal keeps that one mode (raw, paste framing on) from the first key until tmux takes the terminal at
@@ -69,8 +73,9 @@ export function tileAttachArgv(
'else view "$t" "" "$@"; seen=$?; fi;',
'if [ "$seen" -ne 0 ] || ! here "$@"; then printf "\\033[?2004l"; stty sane 2>/dev/null; [ -z "$f" ] || rm -f "$f" "$f".*; return 1; fi;',
'pz=; if LC_ALL=C grep -aqF -- "$b0" "$f"; then pz=1; put "$@"; fi;',
'me=$(tty 2>/dev/null) || me=; w=; if [ -n "$me" ]; then idle "$me" "$@" & w=$!; fi; printf "\\033[?2004l";',
'if [ -z "$pz" ] && [ -s "$f" ]; then go "$@"; else "$@" attach -t "$t"; fi;',
'me=$(tty 2>/dev/null) || me=; w=; if [ -n "$me" ]; then idle "$me" "$@" & w=$!; fi;',
'kk=; case "$("$@" display-message -p -t "$t" "#{version}" 2>/dev/null)" in next-*|master|[4-9].*|[1-9][0-9].*|3.[5-9]*|3.[1-9][0-9]*) kk=1;; esac;',
'ng=1; if [ -z "$pz" ] && [ -s "$f" ]; then if [ -n "$kk" ]; then ng=; go "$@"; else kz 0 $(($(wc -c < "$f"))) "$@"; fi; fi; [ -z "$ng" ] || "$@" attach -t "$t";',
'if [ -n "$w" ]; then kill "$w" 2>/dev/null; wait "$w" 2>/dev/null; fi; rm -f "$f" "$f".*; return 0; };',
'take() { i=0; while [ "$i" -lt 256 ]; do',
'if [ $(($(LC_ALL=C grep -abo -F -- "$b0" "$f" | wc -l))) -gt $(($(LC_ALL=C grep -abo -F -- "$b1" "$f" | wc -l))) ];',
@@ -12,7 +12,7 @@
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
import { ClaudeCompactionEnforcer as ActualEnforcer, buildPostCompactRestorePrompt } from "../src/domain/claude-compaction-enforcer.js";
import { CAPTURE_READABLE_BUDGET_MS, type SessionTransport } from "../src/domain/session-transport.js";
import { CAPTURE_READABLE_BUDGET_MS, SEND_READINESS_FRESHNESS_MS, type SessionTransport } from "../src/domain/session-transport.js";
import type { ClaudeCompactionPolicy, SettingsStore } from "../src/domain/user-settings/settings-store.js";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
@@ -1333,7 +1333,7 @@ describe("ClaudeCompactionEnforcer", () => {
return { ok: true, activity: { state: "idle" } as any, attempts: 1, waitedMs: 0 };
};
await enforcer.triggerManualCompact({ sessionName: SEAT, runtime: "claude-code", usedPercentage: 20 });
expect(callOrder).toEqual(["prep", "idle", `compact:${JSON.stringify({ openrigNotice: true, preserveInput: true, waitForIdleMs: 1_000 + CAPTURE_READABLE_BUDGET_MS, readinessFromPaneOnly: true })}`]);
expect(callOrder).toEqual(["prep", "idle", `compact:${JSON.stringify({ openrigNotice: true, preserveInput: true, waitForIdleMs: SEND_READINESS_FRESHNESS_MS + CAPTURE_READABLE_BUDGET_MS, readinessFromPaneOnly: true })}`]);
});
it.each([0, 75])("wait-for-idle failure means /compact never landed: the back-half is NOT seeded (preparation delay=%ims)", async (delayMs) => {
@@ -6,7 +6,7 @@ import {mkdtempSync, mkdirSync, writeFileSync, renameSync, rmSync, readFileSync,
import {tmpdir} from 'node:os';
import {join, dirname} from 'node:path';
import {ClaudeCompactionEnforcer, COMPACTION_RECORD_FILE, AUTO_PREP_WAIT_MS_DEFAULT, AUTO_RETRY_AFTER_STOP_MS_DEFAULT, MANUAL_PREP_WAIT_MS_DEFAULT} from '../src/domain/claude-compaction-enforcer.js';
import {SessionTransport, CAPTURE_READABLE_BUDGET_MS} from '../src/domain/session-transport.js';
import {SessionTransport, CAPTURE_READABLE_BUDGET_MS, SEND_READINESS_FRESHNESS_MS} from '../src/domain/session-transport.js';
import {SeatDeliveryGuard} from '../src/domain/seat-delivery-guard.js';
import {TmuxAdapter} from '../src/adapters/tmux.js';
import {ContextMonitor} from '../src/domain/context-monitor.js';
@@ -41,9 +41,9 @@ function stageMap(f:ReturnType<typeof fixture>,suffix=''){
}
const compacts=(f:ReturnType<typeof fixture>)=>f.writes.filter(t=>t.startsWith('/compact'));
// Real readable capture and managed send, including the adapter's final beforePaste check.
async function tinyPane(kind:'empty'|'unknown'|'typed'='empty',liveProbe=true){
const f=fixture();vi.spyOn(Date,'now').mockImplementation(()=>f.clock());
await f.e.maybeAutoCompact(input);stageMap(f);
async function tinyPane(kind:'empty'|'unknown'|'typed'='empty',liveProbe=true,manualBudget?:number){
const f=fixture(manualBudget);vi.spyOn(Date,'now').mockImplementation(()=>f.clock());
if(manualBudget===undefined){await f.e.maybeAutoCompact(input);stageMap(f);}
const frame=readFileSync(join(import.meta.dirname,'fixtures/input-box/claude-code-2.1.296/openrig-seat-accept-edits-idle.txt'),'utf8').trimEnd().split('\n');
const row=frame.findLastIndex(line=>line.startsWith('❯'));
if(kind==='typed')frame[row]='❯ keep this draft';
@@ -67,6 +67,55 @@ async function tinyPane(kind:'empty'|'unknown'|'typed'='empty',liveProbe=true){
if(liveProbe)delete(f.transport as any).classifySendReadiness;
return{...f,tmux,window};
}
// Exercise the real hook freshness, pane classifier, final composer check and transport wait.
// Only the activity-store read and tmux I/O are fixtures; no readiness result is injected.
function exhaustedPrompt(f:Awaited<ReturnType<typeof tinyPane>>){
const screen=f.tmux.capturePaneScreen.getMockImplementation()!;
f.tmux.capturePaneScreen.mockImplementation(async()=>(await screen()).replace(/^▎.*$/m,'Context left until auto-compact: 0%'));
f.tmux.capturePaneContent.mockImplementation(f.tmux.capturePaneScreen.getMockImplementation()!);
}
function preparationHook(f:ReturnType<typeof fixture>){
const hook={state:'running',reason:'user_prompt_submit',evidenceSource:'runtime_hook',stale:false,eventAt:new Date(f.clock()).toISOString()};
(f.transport as any).agentActivityStore={getLatestForNode:()=>hook};
}
it('fresh preparation hook ages out before the guarded compact send on a visibly idle pane',async()=>{
const f=await tinyPane();exhaustedPrompt(f);preparationHook(f);const start=f.clock();
await f.e.maybeAutoCompact(input);
expect(compacts(f)).toHaveLength(1);
expect(f.clock()-start).toBeGreaterThan(15_000);
expect(f.window).toEqual({width:11,height:20,own:null,recorded:''});
});
for(const budget of [40_000,9_000])it(`manual skip-map final send keeps its original ${budget}ms deadline with a delayed preparation hook`,async()=>{
const f=await tinyPane('empty',true,budget);f.window.width=120;f.window.height=40;
exhaustedPrompt(f);
// The prep hook can arrive between the initial idle observation and final guarded send.
const wait=f.transport.waitUntilIdle.bind(f.transport);
f.transport.waitUntilIdle=async(...args)=>{const result=await wait(...args);preparationHook(f);return result;};
const start=f.clock(),result=await f.e.triggerManualCompact(input,{operatorInitiated:true,skipMap:true});
expect(f.writes.filter(text=>!text.startsWith('/compact'))).toHaveLength(1);
expect(result).toMatchObject({triggered:budget>15_000});
expect(compacts(f)).toHaveLength(budget>15_000?1:0);
expect(f.clock()-start).toBeLessThanOrEqual(budget);
expect(f.e.getPreparationState(seat)?.deadlineAt).toBe(start+budget);
});
for(const pane of ['running','permission','typed','unknown'] as const)it(`an aged preparation hook still cannot compact a ${pane} pane`,async()=>{
const f=await tinyPane(pane==='typed'?'typed':pane==='unknown'?'unknown':'empty');
if(pane==='running'||pane==='permission'){
const screen=pane==='running'
?readFileSync(join(import.meta.dirname,'fixtures/input-box/claude-code-2.1.296/after-enter-0.4s.J.txt'),'utf8')
:'Allow this command?\n❯ 1. Yes, continue\n 2. No, cancel\nPress enter to continue';
f.tmux.capturePaneScreen.mockImplementation(async()=>screen);f.tmux.capturePaneContent.mockImplementation(async()=>screen);
}
preparationHook(f);const start=f.clock();await f.e.maybeAutoCompact(input);
expect(f.clock()-start).toBeGreaterThan(15_000);
expect(compacts(f)).toHaveLength(0);
});
for(const stop of ['cancel','replacement'] as const)it(`fresh-hook final wait still honors ${stop}`,async()=>{
const f=await tinyPane();preparationHook(f);
(f.transport as any).sleep=async(ms:number)=>{f.advance(ms);if(stop==='cancel')f.e.cancelPreparation(seat);else f.generation('generation-two');};
await f.e.maybeAutoCompact(input);
expect(compacts(f)).toHaveLength(0);
});
it.each(['live-pane','final-composer'])('submits managed /compact from an 11x20 pane through the %s readable read and restores the window',async gate=>{
const f=await tinyPane('empty',gate==='live-pane');
// The pre-existing readable path can classify this fixture and give the inherited sizing back.
@@ -870,7 +919,7 @@ for(const fresh of [false,null,"below","high"] as const)it(`an armed completed m
const monitor=new ContextMonitor(db as any,store as any,undefined,f.e);
const beforeBusyPoll=f.clock();
await monitor.pollOnce();expect(compacts(f)).toHaveLength(0);
expect(f.clock()-beforeBusyPoll).toBeLessThanOrEqual(1_000+CAPTURE_READABLE_BUDGET_MS);
expect(f.clock()-beforeBusyPoll).toBeLessThanOrEqual(SEND_READINESS_FRESHNESS_MS+CAPTURE_READABLE_BUDGET_MS);
expect(f.e.getPreparationState(seat)?.status).toBe('waiting');
f.activity('idle');await monitor.pollOnce();
expect(compacts(f)).toHaveLength(fresh==='below'?0:1);expect(f.writes.filter(t=>!t.startsWith('/compact'))).toHaveLength(1);
+44 -6
View File
@@ -15,6 +15,28 @@ import time
shell, writable, readonly = sys.argv[1], json.loads(sys.argv[2]), json.loads(sys.argv[3])
tmux = shutil.which("tmux")
def version_at_least(version, wanted):
"""Whether tmux -V's version is at least wanted, (major, minor). next-* and master count as the newest."""
v = version[len("tmux "):] if version.startswith("tmux ") else version
if v.startswith("next-") or v == "master":
return True
major, _, rest = v.partition(".")
minor = ""
for ch in rest:
if not ch.isdigit():
break
minor += ch
return major.isdigit() and minor != "" and (int(major), int(minor)) >= wanted
TMUX_VERSION = subprocess.run([tmux, "-V"], capture_output=True, text=True).stdout.strip()
# From tmux 3.5 a tile sends its first keys through the attaching client, so tmux's prefix applies to them. Below 3.5 they
# go to the pane, as tile-attach decides from tmux -V.
THROUGH_CLIENT = version_at_least(TMUX_VERSION, (3, 5))
# Before 3.4, tmux's own client start writes paste framing off, so the tile's framing before the attach can't be told apart.
FRAMING_GAP_SEEN = version_at_least(TMUX_VERSION, (3, 4))
# Wider than any tile below, so a tile that attached would show only the start of it.
LINE = "START " + " ".join(str(i) for i in range(1, 45)) + " END"
# A rule as wide as the seat's window, as Claude Code draws them.
@@ -215,12 +237,20 @@ def writable_first_input(server):
server.wait_for(lambda: received(server) == expected, "a NUL first key reaches the seat")
server.wait_for(lambda: server.client_ttys() == [tile[3]], "a NUL first key attaches the tile")
server.wait_for(lambda: server.client_ttys() == [], "an idle tile detaches", 8)
# tmux's prefix works as in an attached tile: Ctrl-b d detaches at once, and nothing reaches the seat.
os.write(tile[1], b"\x02d")
server.wait_for(lambda: "[detached" in server.seen(tile).rpartition("\x1b[?2004h")[0][-400:] or
server.seen(tile).count("[detached") >= 3, "Ctrl-b d detaches the tile")
time.sleep(0.5)
assert received(server) == expected, "the prefix and its key must not reach the seat: " + repr(received(server)[len(expected):])
if THROUGH_CLIENT:
# tmux's prefix works as in an attached tile: Ctrl-b d detaches at once, and nothing reaches the seat.
os.write(tile[1], b"\x02d")
server.wait_for(lambda: "[detached" in server.seen(tile).rpartition("\x1b[?2004h")[0][-400:] or
server.seen(tile).count("[detached") >= 3, "Ctrl-b d detaches the tile")
time.sleep(0.5)
assert received(server) == expected, "the prefix and its key must not reach the seat: " + repr(received(server)[len(expected):])
else:
# Below tmux 3.5 the first keys go to the pane before the tile attaches, so a prefix typed first reaches the seat.
os.write(tile[1], b"\x02d")
expected += b"\x02d"
server.wait_for(lambda: received(server) == expected, "below tmux 3.5, a prefix typed first reaches the seat")
server.wait_for(lambda: server.client_ttys() == [tile[3]], "below tmux 3.5, the first keys attach the tile")
server.wait_for(lambda: server.client_ttys() == [], "an idle tile detaches", 8)
# A paste while showing: the tile's terminal frames it only if the tile asked for that, as a terminal does.
server.wait_for(lambda: server.seen(tile).rfind("\x1b[?2004h") > server.seen(tile).rfind("[detached"), "the tile shows again")
framed = server.seen(tile).rfind("\x1b[?2004h") > server.seen(tile).rfind("\x1b[?2004l")
@@ -237,12 +267,17 @@ def writable_first_input(server):
server.wait_for(lambda: server.client_ttys() == [], "an idle tile detaches", 8)
# A key and, at once, a paste: the paste is still framed, and both arrive in order.
server.wait_for(lambda: server.seen(tile).rfind("\x1b[?2004h") > server.seen(tile).rfind("[detached"), "the tile shows again")
mark = len(server.seen(tile))
os.write(tile[1], b"A")
time.sleep(0.03)
framed = server.seen(tile).rfind("\x1b[?2004h") > server.seen(tile).rfind("\x1b[?2004l")
os.write(tile[1], b"\x1b[200~" + paste + b"\x1b[201~" if framed else paste)
expected += b"A\x1b[200~" + paste + b"\x1b[201~"
server.wait_for(lambda: received(server) == expected, "a key then a paste arrive in order, the paste framed")
# From the first key until tmux takes the terminal, its paste framing stays on: a paste in that gap stays one paste.
server.wait_for(lambda: "\x1b[?2004h" in server.seen(tile)[mark:], "tmux takes the tile's terminal")
gap = server.seen(tile)[mark:].split("\x1b[?2004h", 1)[0]
assert not FRAMING_GAP_SEEN or "\x1b[?2004l" not in gap, "the tile turned paste framing off before tmux attached: " + repr(gap[-80:])
server.wait_for(lambda: server.client_ttys() == [], "an idle tile detaches", 8)
# A run of text that ends in ; keeps it, through the client and before a paste alike.
os.write(tile[1], b"ls;\r")
@@ -259,6 +294,9 @@ def writable_first_input(server):
expected += b"\x1b[A"
server.wait_for(lambda: received(server) == expected, "Up as the first key reaches the seat as Up")
server.wait_for(lambda: server.client_ttys() == [], "an idle tile detaches", 8)
if not THROUGH_CLIENT:
# Below tmux 3.5 the prefix typed first reaches the seat (above), so the prefix-then-PgUp case applies from 3.5 on.
return {"bytes": len(received(server))}
# The prefix, then PgUp as a person types them: tmux scrolls back, and nothing reaches the seat.
os.write(tile[1], b"\x02")
server.wait_for(lambda: server.client_ttys() == [tile[3]], "the prefix attaches the tile")
+1 -1
View File
@@ -247,7 +247,7 @@ describe("owned Herdr viewer", () => {
// Both attaches are the typing path's: a plain one (also after a paste), and one that carries the first keys.
expect(rw.match(/attach -t/g)).toEqual(['attach -t', 'attach -t']);
expect(rw).toContain('then pz=1; put "$@"; fi;');
expect(rw).toContain('then go "$@"; else "$@" attach -t "$t"; fi;');
expect(rw).toContain('then if [ -n "$kk" ]; then ng=; go "$@"; else kz 0 $(($(wc -c < "$f"))) "$@"; fi; fi; [ -z "$ng" ] || "$@" attach -t "$t";');
expect(rw).toContain('go() { set -- "$@" attach -t "$t";');
// The first input's files are readable by this user alone.
expect(rw.startsWith("umask 077;")).toBe(true);
@@ -0,0 +1,92 @@
// A Pi seat's command is staged into a private script that execs it (execInScript), so the agent replaces the staging
// shell. A declared account home adds a credential scrub to that command; composed as `unset …; …`, the staged
// `exec unset …` failed and no agent started (0.7.11). The real SeatLaunchEnvironment composes each Pi caller's
// command, the real TmuxAdapter stages it, and /bin/sh runs the staged script with stale provider keys in its environment.
import { spawn } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, expect, it, vi } from "vitest";
import { SeatLaunchEnvironment } from "../src/domain/seat-launch-environment.js";
import { TmuxAdapter, type TmuxFileOps } from "../src/adapters/tmux.js";
const roots: string[] = [];
afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); });
const stale = { PI_CODING_AGENT_DIR: "/pi/stale", MISTRAL_API_KEY: "fixture-mistral", GEMINI_API_KEY: "fixture-gemini", OPENAI_API_KEY: "fixture-openai", ANTHROPIC_API_KEY: "fixture-anthropic" };
async function staged(target: Parameters<SeatLaunchEnvironment["command"]>[2], selection: "selected" | "none") {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "pi-staged-exec-")));
roots.push(root);
const cli = path.join(root, "rig");
fs.writeFileSync(cli, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
// Stands in for the Pi runner: says which process it is and what it was given. A real seat's command runs past the
// 512-byte direct-typing bound (its runner path, state root and environment), so it is staged; this path does too.
const runnerDir = path.join(root, ...Array.from({ length: 8 }, () => "nested-runner-directory-" + "x".repeat(24)));
fs.mkdirSync(runnerDir, { recursive: true });
const runner = path.join(runnerDir, "pi-runner");
fs.writeFileSync(runner, "#!/bin/sh\nprintf 'pid=%s\\n' \"$$\"\nenv\n", { mode: 0o700 });
const paneTmux = {
getPaneCommand: async () => "bash",
getSessionEnv: async (_session: string, key: string) => key === "OPENRIG_NODE_ID" ? "pi-node" : "seat@rig",
} as unknown as TmuxAdapter;
const launch = new SeatLaunchEnvironment(paneTmux, { PATH: "/usr/bin:/bin", OPENRIG_HOME: root }, root, cli, undefined,
(_node, _runtime, clearing) => selection === "none" ? undefined : clearing ? "/pi/configured" : "/pi/selected");
const command = await launch.command("seat@rig", runner, target);
const script = path.join(root, "launch.sh");
const files = new Map<string, string>();
let names = 0;
const fileOps: TmuxFileOps = {
// The first name is the staged script; later ones are the paste buffer that types its invocation.
tmpName: () => names++ === 0 ? script : path.join(root, "paste.txt"), bufferName: () => "buffer",
writeFile: vi.fn(async (file, text) => { files.set(file, text); }),
unlink: vi.fn(async file => { files.delete(file); }),
};
const transport = new TmuxAdapter(vi.fn(async (request: string) => request.includes("#{pane_current_command}") ? "bash" : ""), fileOps);
expect(await transport.sendShellCommand("seat@rig", command, undefined, { stageIfLong: true, execInScript: true })).toEqual({ ok: true });
const body = files.get(script);
if (body === undefined) return { command, body, ran: null };
fs.writeFileSync(script, body, { mode: 0o600 });
const child = spawn("/bin/sh", [script], { env: { PATH: "/usr/bin:/bin", ...stale } });
let out = "", err = "";
child.stdout.on("data", c => { out += c; });
child.stderr.on("data", c => { err += c; });
const code = await new Promise<number | null>(resolve => child.on("close", resolve));
return { command, body, ran: { code, out, err, shellPid: child.pid } };
}
it.each([
["launch, selection read from the seat", { nodeId: "pi-node", runtime: "pi", generation: "g1" }, "/pi/selected"],
["resume with the seat's selection", { runtime: "pi", nodeId: "pi-node", accountHome: "/pi/selected" }, "/pi/selected"],
["account switch to a new home", { nodeId: "pi-node", runtime: "pi", accountHome: "/pi/switched" }, "/pi/switched"],
["clear back to the configured home", { nodeId: "pi-node", runtime: "pi", accountHome: null }, "/pi/configured"],
] as const)("%s: the staged script scrubs provider keys and execs the agent in its own process", async (_case, target, home) => {
const { body, ran } = await staged(target, "selected");
expect(body).toBeDefined();
expect(ran!.err).toBe("");
expect(ran!.code).toBe(0);
expect(ran!.out).toContain(`PI_CODING_AGENT_DIR=${home}\n`);
expect(ran!.out).not.toContain("fixture-");
// exec: the agent took over the staging shell's process.
expect(ran!.out).toContain(`pid=${ran!.shellPid}\n`);
});
it("a Pi seat with no account selection composes and stages exactly as before", async () => {
const { command, body, ran } = await staged({ nodeId: "pi-node", runtime: "pi" }, "none");
expect(command.startsWith("/usr/bin/env '")).toBe(true);
expect(command).not.toMatch(/(^|[ ;])(unset|-u) /);
if (body !== undefined) {
expect(body.split("\n")[1]).toBe(`exec ${command}`);
expect(ran!.code).toBe(0);
expect(ran!.out).toContain(`pid=${ran!.shellPid}\n`);
}
});
it("a clear with no configured home still removes the stale home, and the agent still execs", async () => {
const { body, ran } = await staged({ nodeId: "pi-node", runtime: "pi", accountHome: null }, "none");
expect(body).toBeDefined();
expect(ran!.err).toBe("");
expect(ran!.code).toBe(0);
expect(ran!.out).not.toContain("PI_CODING_AGENT_DIR=");
expect(ran!.out).toContain(`pid=${ran!.shellPid}\n`);
});
@@ -6,6 +6,13 @@ import { describe, expect, it } from "vitest";
import { tileAttachArgv } from "../src/domain/terminal/tile-attach.js";
const available = spawnSync("tmux", ["-V"]).status === 0 && spawnSync("python3", ["--version"]).status === 0;
// From tmux 3.5 a tile's first keys go through the attaching client, so a prefix typed first acts as the prefix. Below
// 3.5 they go to the pane, so Ctrl-b d typed first reaches the seat (two more bytes), and the prefix-then-PgUp case is
// skipped.
const tmuxVersion = String(spawnSync("tmux", ["-V"], { encoding: "utf8" }).stdout ?? "").trim().replace(/^tmux /, "");
const tmuxMinor = /^(\d+)\.(\d+)/.exec(tmuxVersion);
const throughClient = /^(next-|master$)/.test(tmuxVersion)
|| (tmuxMinor !== null && (Number(tmuxMinor[1]) > 3 || (Number(tmuxMinor[1]) === 3 && Number(tmuxMinor[2]) >= 5)));
// Linux's /bin/sh is often dash, whose background commands read /dev/null unless given another input.
const shells = ["/bin/sh", ...(existsSync("/bin/dash") ? ["/bin/dash"] : [])];
// The helper runs for many seconds; running it without blocking keeps the test worker able to report to vitest.
@@ -16,13 +23,23 @@ const helper = async (shell: string, ...more: string[]) => JSON.parse((await pro
JSON.stringify(tileAttachArgv("tmux", ["-S", "TILE_SOCKET"], "seat", true, 1, 2)),
...more,
], { encoding: "utf8", timeout: 150000 })).stdout);
// tmux 3.4 and later turn paste framing on only when a client starts, so a framing-off write between the first key and
// the attach leaves a paste in that gap unframed: each of its lines would reach the agent as Enter.
it("a writable tile turns paste framing off only on the return that doesn't attach", () => {
const script = tileAttachArgv("tmux", ["-S", "TILE_SOCKET"], "seat", true, 1, 2).find((arg) => arg.includes("use()")) ?? "";
const use = script.slice(script.indexOf("use()"), script.indexOf("take()"));
expect(use.length).toBeGreaterThan(0);
expect(use.split("\\033[?2004l").length - 1).toBe(1);
expect(use).toContain(`then printf "\\033[?2004l"; stty sane 2>/dev/null;`);
});
describe.skipIf(!available)("a tile views its seat without sizing it, and attaches only while the person types", () => {
it.each(shells)("under %s: whole lines, the seat's own size, keys reaching the seat, and back to the view when idle", async (shell) => {
expect(await helper(shell)).toEqual({
readOnlyAlone: { window: "160x20" },
readOnlyBesideAnother: { window: "160x20" },
writable: { used: "40x30" },
firstInput: { bytes: 183 },
firstInput: { bytes: throughClient ? 183 : 185 },
firstInputWindow: { bytes: 3 },
});
}, 180000);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@openrig/ui",
"version": "0.7.1",
"version": "0.7.11",
"private": true,
"license": "Apache-2.0",
"type": "module",