mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 08:11:17 -10:00
Compare commits
3
Commits
main
...
dd/072-candidate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235a46c143 | ||
|
|
a48d959ba0 | ||
|
|
4191955389 |
No files matched your search
+574
@@ -8,6 +8,580 @@ deprecations, and behavioral changes. Breaking changes are called out explicitly
|
||||
|
||||
---
|
||||
|
||||
## [0.7.2]
|
||||
|
||||
OpenRig 0.7.2 gives each seat more of its own setup. A seat can run on its own
|
||||
Claude or Codex account, move to another login or to the other runtime without
|
||||
leaving its team, and keep its own Claude tool rules in a shared project
|
||||
folder. Sends are refused less often at seats that are plainly working, and
|
||||
more reliably at real dialogs, including Pi's. Codex seats can still commit
|
||||
after a restore, an assistant over MCP can hand work on, and Rex joins Herdr
|
||||
and cmux as a terminal. It is built from the commit tagged `v0.7.2`. The changes
|
||||
since 0.7.1:
|
||||
|
||||
### Before you upgrade
|
||||
|
||||
- **Restart the daemon after installing.** Six database updates apply on its
|
||||
first start.
|
||||
```bash
|
||||
npm install -g @openrig/cli@0.7.2
|
||||
rig daemon stop && rig daemon start
|
||||
```
|
||||
- **Startup replaces OpenRig's own plugin and skill files** (openrig-core goes
|
||||
from 0.1.18 to 0.1.19; the `delegating-work` and `openrig-user` skills
|
||||
changed). As in 0.7.1, it first copies each file it replaces under
|
||||
`$OPENRIG_HOME/backups/managed-refresh/` and logs where, edited or not. If
|
||||
you did edit one, reapply your change from the copy.
|
||||
- **On Linux, if a seat's tools use the session bus, set the bus address in
|
||||
the rig's or member's `env:`.** Seats now start with an address that can't
|
||||
connect, even when a shell startup file sets `XDG_RUNTIME_DIR` (see Behaviour
|
||||
that changed and Known issues).
|
||||
([#2036](https://github.com/mvschwarz/openrig/pull/2036))
|
||||
- **Declare a seat's own `CLAUDE_CONFIG_DIR` or `CODEX_HOME` in the rig spec's
|
||||
`env:`.** A one-off `rig up --env` no longer applies those two, and says so.
|
||||
([#1945](https://github.com/mvschwarz/openrig/pull/1945))
|
||||
- **If you might go back to an earlier version, set equivalent native Claude
|
||||
rules first.** Code from before #1914 opens the upgraded database but leaves
|
||||
per-member Claude tool rules out of launch, so a seat's denies stop applying.
|
||||
`docs/reference/rig-spec.md` has the details.
|
||||
([#1914](https://github.com/mvschwarz/openrig/pull/1914))
|
||||
|
||||
### What's new and fixed
|
||||
|
||||
**A seat's own account and runtime**
|
||||
- A seat can run on its own Claude or Codex login. Sign in to a separate home
|
||||
once, natively, then name it as an absolute `CLAUDE_CONFIG_DIR` or
|
||||
`CODEX_HOME` in the member's or rig's `env:`. It applies at the seat's next
|
||||
launch, and `rig seat status` and `rig provider status` show which home a
|
||||
seat uses and where it comes from.
|
||||
([#1937](https://github.com/mvschwarz/openrig/pull/1937),
|
||||
[#1945](https://github.com/mvschwarz/openrig/pull/1945))
|
||||
- `rig provider switch --seat <seat>` now moves one seat to another
|
||||
subscription login on the same conversation. A Codex seat moves to a login
|
||||
saved in its own Codex home (`--to-account <name>`, Linux only), after its
|
||||
current login is saved as `before-switch-<time>`. A Claude seat moves to its
|
||||
own subscription token file (`--token-file <path>`, made with
|
||||
`claude setup-token`), and `--stored-login` moves it back; OpenRig keeps only
|
||||
the file's path and your label, never the token. `rig provider precheck
|
||||
--seat <seat>` shows what a switch would do, changing nothing.
|
||||
([#1972](https://github.com/mvschwarz/openrig/pull/1972),
|
||||
[#1980](https://github.com/mvschwarz/openrig/pull/1980),
|
||||
[#1991](https://github.com/mvschwarz/openrig/pull/1991))
|
||||
- `rig seat set-runtime <seat> --runtime claude-code|codex --reason "<why>"`
|
||||
moves a live seat between Codex and Claude Code. The successor starts first,
|
||||
gets a handover and confirms it before the old runtime stops, and the seat
|
||||
keeps its address, queue rows and claims. `--dry-run` shows the settings, what
|
||||
is cleared and the costs; `--recover` finishes an interrupted switch. Built
|
||||
on @Aschenbath's design in
|
||||
[#1214](https://github.com/mvschwarz/openrig/pull/1214).
|
||||
([#1895](https://github.com/mvschwarz/openrig/pull/1895))
|
||||
- `rig provider status --refresh --provider claude --claude-token-file <path>
|
||||
--credential-label <label>` reads a Claude token's usage on request, one
|
||||
model turn per refresh.
|
||||
([#1901](https://github.com/mvschwarz/openrig/pull/1901))
|
||||
- A Claude seat set to the alias `haiku` with the `plan` permission is told at
|
||||
launch, at restore and in `rig seat status` that Claude Code answers its
|
||||
plan-mode turns with Sonnet, and how to set a full Haiku id.
|
||||
([#1942](https://github.com/mvschwarz/openrig/pull/1942))
|
||||
|
||||
**Claude tool rules per member.** Claude members that share a project folder
|
||||
can keep their own tool rules: `claude_permissions` with `allow` and `deny`
|
||||
lists in the spec, or `rig seat set-permissions <seat> --rules <file.json>
|
||||
--reason "<why>"` for an existing seat. A change is audited and applies at the
|
||||
next managed launch or resume; OpenRig never writes the rules into the
|
||||
project's Claude settings. Checked natively on Claude Code 2.1.309: two seats
|
||||
in one folder enforce their own rules, a managed policy wins, and fresh, resume
|
||||
and fork launches enforce them. See Known issues for what isn't claimed.
|
||||
([#1914](https://github.com/mvschwarz/openrig/pull/1914))
|
||||
|
||||
**Sending to a seat**
|
||||
- A working Claude seat that quotes approval-prompt text in its own output, such
|
||||
as `› 1. Yes, proceed (y)`, no longer has sends and queue wakes refused as if
|
||||
it were at a prompt, including when Claude draws its input box high in a tall
|
||||
pane. Real permission, question and MCP dialogs still refuse.
|
||||
([#1955](https://github.com/mvschwarz/openrig/pull/1955),
|
||||
[#2005](https://github.com/mvschwarz/openrig/pull/2005))
|
||||
- A draft in a Claude seat's input box that starts with a blank line no longer
|
||||
reads as an empty box, so a send isn't typed into it.
|
||||
([#1977](https://github.com/mvschwarz/openrig/pull/1977))
|
||||
- A draft-aware send to a Claude seat keeps ownership of its paste when Claude's
|
||||
footer changes to a hint such as "paste again to expand" (see Known issues
|
||||
for #14). ([#1921](https://github.com/mvschwarz/openrig/pull/1921))
|
||||
- An ordinary `rig send` to an adopted Pi window waiting at a confirmation
|
||||
dialog is refused with `target_needs_input` and types nothing, instead of
|
||||
choosing the highlighted answer
|
||||
([#2015](https://github.com/mvschwarz/openrig/pull/2015)). A later change
|
||||
extends that recognition to Pi's input and editor dialogs, including help
|
||||
rows that wrap in an 80-column pane
|
||||
([#2026](https://github.com/mvschwarz/openrig/pull/2026)). A Pi seat that
|
||||
OpenRig runs shows an extension dialog in its pane, cancels it and marks the
|
||||
seat as needing attention
|
||||
([#1996](https://github.com/mvschwarz/openrig/pull/1996)).
|
||||
- A queue wake refused at an approval prompt is retried once when the seat's
|
||||
attention clears, instead of waiting for the stuck sweep.
|
||||
([#1961](https://github.com/mvschwarz/openrig/pull/1961))
|
||||
- After a person declines a Claude permission prompt, `rig seat clear-attention
|
||||
<seat> --reason "<what you saw>"` clears the prompt the latest hook still
|
||||
names, and the send refusal says to.
|
||||
([#1912](https://github.com/mvschwarz/openrig/pull/1912))
|
||||
- `rig send` to a mid-turn Codex seat says the message joins the running turn
|
||||
and may redirect it, and names `--wait-for-idle` for separate work.
|
||||
([#2007](https://github.com/mvschwarz/openrig/pull/2007))
|
||||
- `rig capture` says when a pane smaller than 80x24 may cut off the prompt a
|
||||
seat is at, with the command to resize it.
|
||||
([#1969](https://github.com/mvschwarz/openrig/pull/1969))
|
||||
- A seat with an unrelated pane beside its own still gets sends and wakes, in
|
||||
its recorded pane.
|
||||
([#1616](https://github.com/mvschwarz/openrig/pull/1616), @Vaishnavi220506)
|
||||
|
||||
**Codex seats**
|
||||
- A Codex seat on the default workspace-write sandbox can still commit after a
|
||||
resume, fork or restore, and can write its own topology folder, so it keeps
|
||||
its `LEARNED.md`. When the sandbox refuses its recap, the daemon saves it.
|
||||
([#2000](https://github.com/mvschwarz/openrig/pull/2000),
|
||||
[#2023](https://github.com/mvschwarz/openrig/pull/2023),
|
||||
[#2018](https://github.com/mvschwarz/openrig/pull/2018),
|
||||
[#2008](https://github.com/mvschwarz/openrig/pull/2008))
|
||||
- `rig seat restart --keep-conversation` on a Codex seat keeps a writable root
|
||||
whose path holds a space, looks for the resumed conversation in the home the
|
||||
running Codex uses, and, when it can't prove the resume, says the pane
|
||||
restarted instead of "nothing was restarted".
|
||||
([#2003](https://github.com/mvschwarz/openrig/pull/2003),
|
||||
[#2037](https://github.com/mvschwarz/openrig/pull/2037),
|
||||
[#1950](https://github.com/mvschwarz/openrig/pull/1950),
|
||||
[#1958](https://github.com/mvschwarz/openrig/pull/1958))
|
||||
- When a Codex home has a `hooks.json`, OpenRig puts its activity hooks there
|
||||
and moves its own old inline block out of `config.toml`. Your hooks, their
|
||||
trust and your disables stay; @fsafey found the mixed-format problem in their
|
||||
fork. ([#1957](https://github.com/mvschwarz/openrig/pull/1957)) Inline hook
|
||||
trust is keyed by the managed group's actual position.
|
||||
([#2028](https://github.com/mvschwarz/openrig/pull/2028), @dajiaohuang)
|
||||
- A signed-out Codex seat at the sign-in chooser or API-key entry reports
|
||||
`login_required` instead of waiting out the readiness timeout (Codex 0.164.0
|
||||
and 0.162.1).
|
||||
([#1967](https://github.com/mvschwarz/openrig/pull/1967), @Aschenbath)
|
||||
([#1983](https://github.com/mvschwarz/openrig/pull/1983))
|
||||
- A resumed Codex seat is recognized when its footer shows a model with a
|
||||
reasoning label, such as `GPT-6.1 Sol medium`, or a versioned non-`gpt`
|
||||
model, so `rig up --existing` no longer stops there.
|
||||
([#1944](https://github.com/mvschwarz/openrig/pull/1944))
|
||||
([#1778](https://github.com/mvschwarz/openrig/pull/1778), @Nikhila-Dhevi-R)
|
||||
|
||||
**Claude compaction and restore**
|
||||
- A managed Claude restore reads its own restore files without stopping at a
|
||||
Read approval, including on macOS, through a symlinked home or project, and
|
||||
for the generic or configured `post-compact-extra.md` and preparation root.
|
||||
The grants apply from the seat's next launch or resume.
|
||||
([#1940](https://github.com/mvschwarz/openrig/pull/1940),
|
||||
[#1952](https://github.com/mvschwarz/openrig/pull/1952),
|
||||
[#1962](https://github.com/mvschwarz/openrig/pull/1962))
|
||||
- A seat restored with `claude --resume` that stops at Claude's resume chooser
|
||||
in a tall pane is reported as waiting for a person's choice instead of
|
||||
resumed, and a Codex model chooser there reads as `model_selection_gate`. An
|
||||
old chooser above a resumed session no longer reads as live.
|
||||
([#2010](https://github.com/mvschwarz/openrig/pull/2010))
|
||||
- After a managed compaction, a Claude seat's restore starts on the next poll
|
||||
instead of waiting for something to change its screen.
|
||||
([#1971](https://github.com/mvschwarz/openrig/pull/1971))
|
||||
- `rig compact <seat> --state` and seat status show an owed restore stage and
|
||||
its last delivery result, and repeated failures open one queue item for
|
||||
whoever can act. ([#1953](https://github.com/mvschwarz/openrig/pull/1953))
|
||||
- When Claude refuses a startup turn late, for example on an expired login,
|
||||
OpenRig checks again before the next startup input's Enter and holds the rest
|
||||
as owed. ([#1976](https://github.com/mvschwarz/openrig/pull/1976))
|
||||
- The model-divergence monitor no longer flags a Claude seat pinned to an
|
||||
undated id that answers with its dated snapshot, or to a Bedrock
|
||||
inference-profile id.
|
||||
([#1927](https://github.com/mvschwarz/openrig/pull/1927),
|
||||
[#1931](https://github.com/mvschwarz/openrig/pull/1931))
|
||||
|
||||
**Assistants and other agents**
|
||||
- OpenRig's MCP server adds `rig_queue_handoff` and `rig_queue_unclaim` (36
|
||||
tools), so an assistant can pass held work on, or put it back, in one step.
|
||||
([#1970](https://github.com/mvschwarz/openrig/pull/1970))
|
||||
- A community-harness preset can set `first_input: true` to hand the harness
|
||||
its first startup turn at launch instead of pasting it into the pane. The
|
||||
Cursor example uses it and is no longer startup-assisted, and OpenClaw seats
|
||||
keep named conversations as a local agent or a Gateway client.
|
||||
([#1987](https://github.com/mvschwarz/openrig/pull/1987),
|
||||
[#2017](https://github.com/mvschwarz/openrig/pull/2017),
|
||||
[#2009](https://github.com/mvschwarz/openrig/pull/2009))
|
||||
- `rig stream act <id>` records what a reader did about a stream item:
|
||||
`--work <row>`, `--decline "<reason>"` or `--duplicate <id>`.
|
||||
`rig stream show` lists each reader's current action.
|
||||
([#1899](https://github.com/mvschwarz/openrig/pull/1899))
|
||||
- The guide that `rig acp run --help` names now ships:
|
||||
`rig context get reference/acp-agents.md`.
|
||||
([#1930](https://github.com/mvschwarz/openrig/pull/1930))
|
||||
|
||||
**Running a team**
|
||||
- Rex is a terminal provider. From a Rex pane, `rig terminal open <view>
|
||||
--window` and the TUI open the team's view as a Rex session;
|
||||
`--provider rex` selects it.
|
||||
([#1935](https://github.com/mvschwarz/openrig/pull/1935), @vanducng)
|
||||
- An open rig view keeps a seat's tile when its tmux session is replaced, and a
|
||||
fresh replacement prints how to reopen the view.
|
||||
([#1964](https://github.com/mvschwarz/openrig/pull/1964),
|
||||
[#1941](https://github.com/mvschwarz/openrig/pull/1941))
|
||||
- `rig launch <rig> <seat>` first-starts one seat of a team imported with
|
||||
`--materialize-only`, without restoring the others.
|
||||
([#1965](https://github.com/mvschwarz/openrig/pull/1965))
|
||||
- A pod seat that never launched starts fresh from its saved launch recipe after
|
||||
a daemon restart, instead of waiting at a shell.
|
||||
([#1503](https://github.com/mvschwarz/openrig/pull/1503), @Vaishnavi220506)
|
||||
- After a team's tmux server dies, `rig down` settles its seats, so `rig
|
||||
archive` and `rig up` can proceed.
|
||||
([#1999](https://github.com/mvschwarz/openrig/pull/1999))
|
||||
- A team of terminal seats reads fully restored after `rig start`, and
|
||||
`rig down` names the terminals it stops.
|
||||
([#1984](https://github.com/mvschwarz/openrig/pull/1984))
|
||||
- A launch whose tmux made no session fails as `session_not_created` or
|
||||
`session_unconfirmed` instead of reading launched.
|
||||
([#1910](https://github.com/mvschwarz/openrig/pull/1910))
|
||||
- A service bundle install on a machine without Docker Compose v2 stops with
|
||||
`compose_missing` before it writes the target or creates the team, and
|
||||
`--plan` warns that it would.
|
||||
([#2025](https://github.com/mvschwarz/openrig/pull/2025))
|
||||
- `rig export` copies the team's culture file and names each seat whose agent
|
||||
ref points outside the exported folder.
|
||||
([#1975](https://github.com/mvschwarz/openrig/pull/1975))
|
||||
- `rig queue resolve <row> --decision "<evidence>"` also releases a row parked
|
||||
on a typed blocker such as `external:`, `fold:` or `auth:`.
|
||||
([#1809](https://github.com/mvschwarz/openrig/pull/1809), @dajiaohuang, from
|
||||
@BillyOutlast's report)
|
||||
- `rig queue whoami` resolves a project-tagged row against its catalogued
|
||||
project, `rig proof add` finds a slice in another catalogued project, and
|
||||
`rig scope mission create` writes the mission's name.
|
||||
([#1526](https://github.com/mvschwarz/openrig/pull/1526), @Vaishnavi220506)
|
||||
([#1938](https://github.com/mvschwarz/openrig/pull/1938),
|
||||
[#1936](https://github.com/mvschwarz/openrig/pull/1936))
|
||||
- A `rig queue create` whose outcome is unknown reads the row back once by its
|
||||
ID before reporting, and a cross-host write that times out reads
|
||||
INDETERMINATE instead of failed.
|
||||
([#1978](https://github.com/mvschwarz/openrig/pull/1978),
|
||||
[#1911](https://github.com/mvschwarz/openrig/pull/1911))
|
||||
- Slack: every post names its sender first, an escalation names the seat the
|
||||
work waits on, a reply from another registered person reaches the agent as
|
||||
their own message instead of answering someone else's question, and Slack
|
||||
rows name who wrote.
|
||||
([#1960](https://github.com/mvschwarz/openrig/pull/1960),
|
||||
[#1893](https://github.com/mvschwarz/openrig/pull/1893))
|
||||
|
||||
**Reliability**
|
||||
- A dead seat's stuck-sweep finding goes to someone who can act, not to the
|
||||
dead seat, and says which rule chose them.
|
||||
([#2011](https://github.com/mvschwarz/openrig/pull/2011),
|
||||
[#2021](https://github.com/mvschwarz/openrig/pull/2021),
|
||||
[#2033](https://github.com/mvschwarz/openrig/pull/2033))
|
||||
- A seat held at a choice prompt that no hook announces, such as Claude's
|
||||
usage-limit dialog, now escalates while it holds work.
|
||||
([#1919](https://github.com/mvschwarz/openrig/pull/1919))
|
||||
- The parked-owner reminder no longer reaches a seat in the first 5 minutes
|
||||
after it starts waiting, such as just after a handoff or a question to a
|
||||
person. A seat at an approval or question prompt gets no grace.
|
||||
([#1963](https://github.com/mvschwarz/openrig/pull/1963))
|
||||
- A Pi or OMP seat's second startup block reaches the running turn instead of
|
||||
raising `runtime_error`.
|
||||
([#1943](https://github.com/mvschwarz/openrig/pull/1943), reported by
|
||||
@astral-fleet-sessions)
|
||||
- The daemon starts listening before it works through a backlog of queue
|
||||
notifications, so startup doesn't time out behind them.
|
||||
([#1993](https://github.com/mvschwarz/openrig/pull/1993))
|
||||
- A startup database-conflict check that can't get an answer now says so,
|
||||
instead of reading like a clean one.
|
||||
([#1974](https://github.com/mvschwarz/openrig/pull/1974), from @calv-io-n's
|
||||
finding)
|
||||
- Proof-source monitoring watches the missions root, not the whole workspace.
|
||||
([#1951](https://github.com/mvschwarz/openrig/pull/1951))
|
||||
- Queue views, health reads with long histories and send checks do less
|
||||
repeated work. ([#2031](https://github.com/mvschwarz/openrig/pull/2031),
|
||||
[#2013](https://github.com/mvschwarz/openrig/pull/2013),
|
||||
[#1994](https://github.com/mvschwarz/openrig/pull/1994))
|
||||
- A stalled-after-claim finding's age counts from the stall, not the claim.
|
||||
([#1959](https://github.com/mvschwarz/openrig/pull/1959))
|
||||
- From contributors: queued terminal input is dropped after its client
|
||||
disconnects ([#1636](https://github.com/mvschwarz/openrig/pull/1636),
|
||||
@Vaishnavi220506); TUI focused summaries load their health findings
|
||||
([#1986](https://github.com/mvschwarz/openrig/pull/1986), @Kero-66 and
|
||||
@Aschenbath); the TUI keeps the stream's connection state across pages
|
||||
([#2014](https://github.com/mvschwarz/openrig/pull/2014), @Nikhi00718); the
|
||||
web dashboard reads a skipped kernel boot as "Boot not tracked", not
|
||||
"Unknown" ([#1917](https://github.com/mvschwarz/openrig/pull/1917),
|
||||
@Aschenbath, reported by @Kero-66); the scenario runner binds compaction and
|
||||
restore, and the test-system docs state the CI job's real counts
|
||||
([#1924](https://github.com/mvschwarz/openrig/pull/1924),
|
||||
[#1925](https://github.com/mvschwarz/openrig/pull/1925), @DeryFerd).
|
||||
- Guidance: the installer guide says what setup and starting Codex seats write
|
||||
to your home configuration, and the operator puts project work on a team's
|
||||
queue; a Codex user who declines the remembered command rule is offered the
|
||||
operator conversation; `rig queue update --help` and its refusal say how to
|
||||
cancel a row (`--state done --closure-reason canceled`); `rig skill audit`
|
||||
says what its stale and hash findings mean; 14 subcommands that showed a
|
||||
blank help summary now have one; the `openrig-user` skill says a queue
|
||||
create's request ID isn't the row; `delegating-work` also applies when judging
|
||||
a delegated result; and `docs/reference/seat-delivery.md` lists all six
|
||||
typing-guard modes.
|
||||
([#2001](https://github.com/mvschwarz/openrig/pull/2001),
|
||||
[#1973](https://github.com/mvschwarz/openrig/pull/1973),
|
||||
[#1956](https://github.com/mvschwarz/openrig/pull/1956),
|
||||
[#1966](https://github.com/mvschwarz/openrig/pull/1966),
|
||||
[#1923](https://github.com/mvschwarz/openrig/pull/1923),
|
||||
[#1909](https://github.com/mvschwarz/openrig/pull/1909),
|
||||
[#2012](https://github.com/mvschwarz/openrig/pull/2012),
|
||||
[#1989](https://github.com/mvschwarz/openrig/pull/1989))
|
||||
|
||||
### Behaviour that changed
|
||||
|
||||
- On Linux, a seat launched by OpenRig now gets the daemon's `XDG_RUNTIME_DIR`,
|
||||
with `DBUS_SESSION_BUS_ADDRESS=disabled:`, a session bus address that can't
|
||||
connect. Tools whose sockets live in the runtime directory work in seats
|
||||
without per-tool setup, and a seat's D-Bus clients don't find or start the
|
||||
session bus on their own. A seat's own `XDG_RUNTIME_DIR`, from `env:` for
|
||||
example, leaves both as before, and its own bus address is kept. Unchanged:
|
||||
macOS, a daemon started without `XDG_RUNTIME_DIR`, and
|
||||
`terminal.inherit_tmux_environment` on. See Known issues.
|
||||
([#2036](https://github.com/mvschwarz/openrig/pull/2036), reported by
|
||||
@borfast in
|
||||
[#2030](https://github.com/mvschwarz/openrig/issues/2030))
|
||||
- `rig up --env CLAUDE_CONFIG_DIR=…` or `CODEX_HOME=…` isn't applied, and the
|
||||
CLI says so; other `--env` values work as before. Name a seat's home in the
|
||||
spec's `env:`. ([#1945](https://github.com/mvschwarz/openrig/pull/1945))
|
||||
- `rig provider switch` acts: it used to answer `failed_safely` every time.
|
||||
([#1972](https://github.com/mvschwarz/openrig/pull/1972),
|
||||
[#1980](https://github.com/mvschwarz/openrig/pull/1980))
|
||||
- A `rig send` to a seat that is switching runtime is held, not typed
|
||||
(`outcome: retained`, `reason: runtime_switch_in_progress`); send it again
|
||||
after the switch. ([#1895](https://github.com/mvschwarz/openrig/pull/1895))
|
||||
- `rig seat status --json` always carries `permissions.account.advisories`
|
||||
(empty unless there's an advisory).
|
||||
([#1942](https://github.com/mvschwarz/openrig/pull/1942))
|
||||
- Every Slack post starts with `From <seat> on <machine>`, and an escalation's
|
||||
headline reads `Waiting on <seat>: <summary>`; update anything that read the
|
||||
sender from the last line.
|
||||
([#1960](https://github.com/mvschwarz/openrig/pull/1960))
|
||||
- A row OpenRig creates itself, such as a stuck-sweep finding or a workflow
|
||||
escalation, wakes its seat from OpenRig's own sender with no reply line, instead
|
||||
of from the row's creator. A stuck-sweep closure is recorded as
|
||||
`stuck-sweep@system`.
|
||||
([#1905](https://github.com/mvschwarz/openrig/pull/1905))
|
||||
([#1811](https://github.com/mvschwarz/openrig/pull/1811), @Aschenbath)
|
||||
- With `agents.operator_session` set, a dead seat's finding goes to that agent
|
||||
first, then the operator seat (`workspace.operator_seat_name`), then the
|
||||
row's creator. Approvals still reach the person. With it unset, routing is as
|
||||
before. ([#2021](https://github.com/mvschwarz/openrig/pull/2021))
|
||||
- An alert for a seat at a choice prompt follows `queue.wake_human_rung`: under
|
||||
`explicit-only` it stays with the operator seat as a visible row, unless that
|
||||
seat is a person. Only one choice-prompt alert reaches a person at a time on a
|
||||
machine; the others wait as visible rows and post when it retires.
|
||||
Permission-prompt alerts post as before.
|
||||
([#1919](https://github.com/mvschwarz/openrig/pull/1919))
|
||||
- On a Codex home with a `hooks.json`, OpenRig's activity hooks go there, and
|
||||
its own old inline block moves out of `config.toml`.
|
||||
([#1957](https://github.com/mvschwarz/openrig/pull/1957))
|
||||
- The recap store refuses to write through a link at `RECAP.md` or
|
||||
`recap-superseded/` (`recap_write_refused`), a layout that used to work.
|
||||
([#2018](https://github.com/mvschwarz/openrig/pull/2018))
|
||||
- Cross-host queue writes wait 15 seconds for the daemon's answer, and a
|
||||
timeout reads "queue request got no answer in time" with the ID to reconcile
|
||||
by. ([#1911](https://github.com/mvschwarz/openrig/pull/1911))
|
||||
- The Cursor reference preset is `startup_assisted: false` and passes its first
|
||||
prompt at launch. ([#2017](https://github.com/mvschwarz/openrig/pull/2017))
|
||||
- In a Pi seat that OpenRig runs, an extension `/command` typed mid-turn runs
|
||||
at once instead of being refused as a steer.
|
||||
([#1943](https://github.com/mvschwarz/openrig/pull/1943))
|
||||
|
||||
### Known issues
|
||||
|
||||
- **On Linux, seats now get the runtime directory with a session bus address
|
||||
that can't connect.** This is an accepted trade-off, and default hygiene, not
|
||||
isolation. A shell startup file that sets only `XDG_RUNTIME_DIR` used to let a
|
||||
seat's clients find the session bus; now the seat keeps
|
||||
`DBUS_SESSION_BUS_ADDRESS=disabled:`. To use a session bus, set its address in
|
||||
the rig's or member's `env:`. `systemctl --user` reaches the person's user
|
||||
service manager from a seat, whatever the bus address. A Codex stdio MCP
|
||||
server still needs `env_vars = ["XDG_RUNTIME_DIR"]` in Codex's `config.toml`
|
||||
to see the variable. ([#2036](https://github.com/mvschwarz/openrig/pull/2036))
|
||||
- **Per-member Claude tool rules have limits.** Member denies aren't claimed to
|
||||
hold in bypass mode (`full_bypass`). Handover and restore carry the rules, but
|
||||
aren't proven to keep the same conversation. An allow list doesn't withdraw
|
||||
broader inherited grants, and these are Claude's own tool rules, not OS or
|
||||
credential isolation. A change applies at the next launch or resume, not in a
|
||||
running process. Going back to code from before #1914 drops the stored rules
|
||||
from launch (see Before you upgrade).
|
||||
([#1914](https://github.com/mvschwarz/openrig/pull/1914))
|
||||
- **Pi dialog recognition is a bounded screen check.**
|
||||
[#2015](https://github.com/mvschwarz/openrig/pull/2015) refuses ordinary
|
||||
sends at an adopted Pi confirmation dialog, and
|
||||
[#2026](https://github.com/mvschwarz/openrig/pull/2026) later extended the
|
||||
same classifier to Pi's input and editor dialogs and wrapped help rows. Each
|
||||
was checked with a real Pi for its own case only. Layouts outside them keep
|
||||
the earlier behaviour, so an ordinary send can still type into them: more than
|
||||
six rows under a confirmation dialog's border, a widget that draws its own
|
||||
border, clipped or incomplete help, more than three wrapped help rows, and
|
||||
custom extension UIs. Neither check was repeated on the final 0.7.2 build.
|
||||
- **Moving a seat's account or runtime has limits.** A conversation saved under
|
||||
a seat's earlier home doesn't resume in its new one, and live usage refresh
|
||||
still reads only the daemon's own logins. A Codex login switch is Linux only;
|
||||
other seats on that Codex home keep their old login until their next token
|
||||
refresh, then stop with "signed in to another account" until you run the
|
||||
printed `rig seat restart <peer> --keep-conversation` for each. A runtime
|
||||
switch starts a new native conversation (only the handover carries over),
|
||||
clears the source runtime's model, effort and Codex profile, and doesn't apply
|
||||
Claude launch additions such as an agent file or run startup slash commands.
|
||||
None of these was run with real accounts before this release.
|
||||
- **A new Claude agent's startup text can stay unsent in its input box.** Press
|
||||
Enter in its terminal.
|
||||
- **Claude seats on a macOS native install can read as needing attention after a
|
||||
restore** ([#86](https://github.com/mvschwarz/openrig/issues/86)). Check the
|
||||
seat's terminal before acting on it.
|
||||
- **A Slack channel-mapping change needs a daemon restart.** Don't use
|
||||
`rig slack disable` then `enable` for it, which marks waiting alerts as seen.
|
||||
- **Ubuntu 22.04's tmux 3.2a:** the known crashing query is gone, but a narrower
|
||||
race remains. Prefer another tmux.
|
||||
- **`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, run
|
||||
`rig daemon start` yourself first.
|
||||
- **A Claude seat signed in with a long-lived token (`claude setup-token`) still
|
||||
gets no automatic usage reading.** The reader accepts only claude.ai
|
||||
subscription logins and reports `not_subscription_login` for any other. Read a
|
||||
token's usage on request with `rig provider status --refresh --provider claude
|
||||
--claude-token-file <path> --credential-label <label>`; the label is yours,
|
||||
not an account the provider confirmed.
|
||||
- **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. 0.7.2
|
||||
reads Claude's recognized footer hints as part of the same composer
|
||||
([#1921](https://github.com/mvschwarz/openrig/pull/1921)), but the original
|
||||
failure didn't reproduce in a native check, so
|
||||
[#14](https://github.com/mvschwarz/openrig/issues/14) remains open.
|
||||
- **Prompt-looking text in a seat's recent output can still be mistaken for an
|
||||
active approval prompt in some layouts, so OpenRig can refuse sends and queue
|
||||
wakes.** 0.7.2 lets sends through to a Claude seat whose running timer row sits
|
||||
directly above its empty, framed input box (checked natively on Claude Code
|
||||
2.1.309, for the layouts exercised). An idle Claude seat with quoted prompt
|
||||
text above its input box, a draft in the box, a mode bar OpenRig doesn't
|
||||
recognize (such as `⏸ manual mode on`), an unframed box, and Codex seats are
|
||||
unchanged. Inspect the current native prompt and the refusal before retrying;
|
||||
do not assume that waiting 15 seconds clears it.
|
||||
- **The shared-database check needs `lsof`.** Without a working `lsof`, the
|
||||
check can't tell whether another live daemon holds the database. The daemon
|
||||
now says so at startup and on `/healthz`, but the optional
|
||||
`daemon.db_conflict: stop` stops only on a confirmed conflict, so it doesn't
|
||||
stop then. The warning for a database outside `OPENRIG_HOME` doesn't depend
|
||||
on `lsof`. ([#1974](https://github.com/mvschwarz/openrig/pull/1974))
|
||||
- **Managed Claude restoration can still ask for approvals.** 0.7.2 lets a seat
|
||||
read its own restore files without asking, from its next launch or resume. A
|
||||
relative `policies.claude_compaction.message_file_path` still asks once, and a
|
||||
packet read can ask after the native conversation changes. In 0.7.1's
|
||||
representative test, the restore also asked to run four read-only commands,
|
||||
which these grants don't cover. Check the retained restore map and reported
|
||||
gaps before treating restoration as complete.
|
||||
([#1940](https://github.com/mvschwarz/openrig/pull/1940),
|
||||
[#1952](https://github.com/mvschwarz/openrig/pull/1952),
|
||||
[#1962](https://github.com/mvschwarz/openrig/pull/1962)) A native check on Claude
|
||||
2.1.309 passed the Read grants with no approval prompt; a complete restore
|
||||
with no help isn't claimed.
|
||||
- **A managed Claude restore can remain owed.** If a seat stays idle after
|
||||
compaction, `rig compact <seat> --state` and seat status now show the owed
|
||||
stage and its last delivery result; also read its saved restore map. Before
|
||||
sending a manual restore request, reconcile the native conversation and
|
||||
pending state to avoid a duplicate; do not repeat `/compact` merely because
|
||||
the restore request has not arrived.
|
||||
([#1953](https://github.com/mvschwarz/openrig/pull/1953))
|
||||
- **On Linux, very large trees can still cause severe daemon stalls and
|
||||
file-watch-limit failures.** Proof-source monitoring now recurses only at the
|
||||
configured missions root, so unrelated checkouts and build trees in the
|
||||
workspace aren't watched, but a large missions tree still is, and the stalls'
|
||||
cause hasn't been confirmed.
|
||||
([#1813](https://github.com/mvschwarz/openrig/issues/1813),
|
||||
[#1951](https://github.com/mvschwarz/openrig/pull/1951))
|
||||
- **A Claude startup refusal that arrives after OpenRig's last check can still
|
||||
race the next startup input's Enter.** When OpenRig withholds Enter, the
|
||||
pasted input may stay in the input box: don't resend it, and follow the
|
||||
recovery step the seat reports. Recovery after signing in wasn't verified
|
||||
natively.
|
||||
([#1976](https://github.com/mvschwarz/openrig/pull/1976))
|
||||
- **Claude Code answers plan-mode turns with Sonnet when a seat's model is the
|
||||
alias `haiku`.** That's Claude Code's own behaviour, and OpenRig now says so.
|
||||
Configuring the full id `claude-haiku-5-5` gave a Haiku answer in a plan-mode
|
||||
follow-up on Claude Code 2.1.309.
|
||||
([#1942](https://github.com/mvschwarz/openrig/pull/1942))
|
||||
- **A cross-host queue create can still end with an unknown outcome.** A
|
||||
timed-out `rig queue create --host` now reads INDETERMINATE, and the CLI reads
|
||||
the row back once by its ID. If it's still unknown, check that host for the
|
||||
original row ID before retrying. If the row exists, do not create a
|
||||
replacement. If a retry is needed, reuse the same `--id` and payload; an
|
||||
unsuccessful read is not proof that the row is absent.
|
||||
([#1911](https://github.com/mvschwarz/openrig/pull/1911),
|
||||
[#1978](https://github.com/mvschwarz/openrig/pull/1978))
|
||||
- **A Codex seat whose queue-state folder or team-workspace path contains a
|
||||
space can still be misread on an ordinary resume or restore,** because those
|
||||
arguments come before the conversation id. An in-place restart is fixed.
|
||||
([#2037](https://github.com/mvschwarz/openrig/pull/2037))
|
||||
- **Automatic Codex hook approval needs OpenRig's plugin folder to match what
|
||||
shipped.** If `$OPENRIG_HOME/plugins/openrig-core` holds files that didn't
|
||||
ship, the Codex plugin still installs or upgrades, but OpenRig approves
|
||||
nothing itself: an existing approval stays as it was, and Codex's own hook
|
||||
review applies. On a restart without a version change, if a hook file or the
|
||||
plugin manifest there differs from what this version shipped, OpenRig doesn't
|
||||
install or upgrade its Codex plugin. On a version upgrade, OpenRig's managed
|
||||
refresh replaces an edited plugin file with the shipped one and keeps your
|
||||
edited copy under `$OPENRIG_HOME/backups/managed-refresh/`; after upgrading,
|
||||
inspect those backups and reapply any deliberate customization.
|
||||
- **Terminal views:** a view opened before 0.7.2 needs closing and reopening to
|
||||
keep its tiles through a session replacement, and a replacement that takes
|
||||
too long can still need a reopen. This wasn't run in the Herdr, cmux or Rex
|
||||
apps or over SSH, so
|
||||
[#1939](https://github.com/mvschwarz/openrig/issues/1939) stays open. Rex
|
||||
wasn't run on Linux, and a Rex tile that fails 2 to 10 seconds after opening
|
||||
is still listed as opened.
|
||||
([#1964](https://github.com/mvschwarz/openrig/pull/1964),
|
||||
[#1935](https://github.com/mvschwarz/openrig/pull/1935))
|
||||
- **Herdr's own status for a seat can read idle or unknown while OpenRig shows
|
||||
it waiting** (`needs-input`), because OpenRig doesn't feed Herdr a state. Use
|
||||
OpenRig's view or the Feed to find waiting seats. Reopening a view with
|
||||
`--provider herdr` adds a workspace each time; from inside Herdr,
|
||||
`rig terminal open <view> --window` reuses a matching one.
|
||||
([#2032](https://github.com/mvschwarz/openrig/pull/2032))
|
||||
|
||||
### Credits
|
||||
|
||||
Thank you to everyone whose work is in this release: @Vaishnavi220506,
|
||||
@Aschenbath, @dajiaohuang, @DeryFerd, @vanducng, @Nikhila-Dhevi-R, @Nikhi00718
|
||||
and @Kero-66, and to @borfast, @BillyOutlast, @tianqizhao0704, @calv-io-n,
|
||||
@george661, @fsafey, @astral-fleet-sessions and the Heretek-AI team, whose
|
||||
reports, ideas and outside audit led to fixes here.
|
||||
|
||||
Thanks also to @alexveli1 and @george661, who reported
|
||||
[#1764](https://github.com/mvschwarz/openrig/issues/1764) (addressed by
|
||||
[#1778](https://github.com/mvschwarz/openrig/pull/1778) and
|
||||
[#1944](https://github.com/mvschwarz/openrig/pull/1944)); @kainne44 and
|
||||
@bobbiejaxn, who reported [#81](https://github.com/mvschwarz/openrig/issues/81)
|
||||
(partly addressed by [#1919](https://github.com/mvschwarz/openrig/pull/1919));
|
||||
@oneandonlydean, who reported
|
||||
[#1813](https://github.com/mvschwarz/openrig/issues/1813) (partly addressed by
|
||||
[#1951](https://github.com/mvschwarz/openrig/pull/1951)); @todie, who reported
|
||||
[#1419](https://github.com/mvschwarz/openrig/issues/1419) (its closure
|
||||
attribution addressed by
|
||||
[#1811](https://github.com/mvschwarz/openrig/pull/1811)); @reisalbuquerque, who
|
||||
reported [#49](https://github.com/mvschwarz/openrig/issues/49) (partly addressed
|
||||
by [#1503](https://github.com/mvschwarz/openrig/pull/1503)); @jimallen, who
|
||||
reported [#17](https://github.com/mvschwarz/openrig/issues/17) (partly addressed
|
||||
by [#2028](https://github.com/mvschwarz/openrig/pull/2028)); @parkingmeter, who
|
||||
reported [#1933](https://github.com/mvschwarz/openrig/issues/1933) (addressed by
|
||||
[#1963](https://github.com/mvschwarz/openrig/pull/1963)); @vanducng, who
|
||||
reported [#1939](https://github.com/mvschwarz/openrig/issues/1939) (addressed by
|
||||
[#1964](https://github.com/mvschwarz/openrig/pull/1964) and
|
||||
[#1941](https://github.com/mvschwarz/openrig/pull/1941), open for confirmation);
|
||||
and @haichongfu and @vadym-abstract-factory, who reported
|
||||
[#14](https://github.com/mvschwarz/openrig/issues/14) (worked on again in
|
||||
[#1921](https://github.com/mvschwarz/openrig/pull/1921); still open).
|
||||
|
||||
## [0.7.1] - 2026-10-25
|
||||
|
||||
Published 2026-10-25 as `@openrig/[email protected]`, built from the commit tagged
|
||||
|
||||
@@ -26,9 +26,9 @@ use:
|
||||
|
||||
```sh
|
||||
# Claude Code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.0/skills/rigs --skill rigs --agent claude-code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.2/skills/rigs --skill rigs --agent claude-code
|
||||
# Codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.0/skills/rigs --skill rigs --agent codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.2/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.0` 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.2` 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.0/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.0/scripts/install.sh | sh
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.2/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.2/scripts/install.sh | sh
|
||||
```
|
||||
|
||||
If a provider isn't signed in yet, the last step reports `FAILED [4/4]`, and `rig setup` lists the sign-in under "Some steps need attention". If the only remaining failures are provider sign-ins, the install steps finished; sign in to each selected provider as below and continue.
|
||||
|
||||
@@ -191,11 +191,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.0/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.2/scripts/install.sh | sh -s -- --dry-run
|
||||
```
|
||||
|
||||
The same command without `--dry-run`,
|
||||
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.0/scripts/install.sh | sh`, installs the
|
||||
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.2/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
|
||||
|
||||
@@ -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.0`. If OpenRig is already installed, use the tag
|
||||
For a new install, use the latest release's tag, for example `v0.7.2`. 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.
|
||||
|
||||
+264
-151
@@ -1,165 +1,288 @@
|
||||
# What changed in OpenRig 0.7.1
|
||||
# What changed in OpenRig 0.7.2
|
||||
|
||||
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:
|
||||
This note is for an agent on an install that was just upgraded to 0.7.2. It lists only what behaves differently from
|
||||
0.7.1 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.
|
||||
after an upgrade it can still hold the previous version's note. The capability map hasn't been
|
||||
updated for 0.7.2 yet, so this note is where 0.7.2's new commands are listed.
|
||||
|
||||
## 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
|
||||
- **Restart the daemon** (`rig daemon stop`, then `rig daemon start`). Six migrations run on the first start (132, 133,
|
||||
134, 136, 138 and 139). They add records for runtime switches, stream reader actions, a seat's launch recipe, a Claude
|
||||
seat's token reference and its tool rules, and rebuild the provider usage readings table to hold labelled Claude
|
||||
token readings, keeping existing readings. No setting is added or removed.
|
||||
- **The bundled openrig-core plugin goes from 0.1.18 to 0.1.19.** Only two skills changed: `delegating-work` (its
|
||||
description now also covers deciding how far to trust a delegated result) and `openrig-user` (reading a queue write,
|
||||
a seat's own Claude or Codex account, moving one seat to another login, and the MCP handoff and unclaim tools). As in
|
||||
0.7.1, startup 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.
|
||||
- **Startup also refreshes the person-facing `rigs` skill** in `~/.claude/skills` and `~/.agents/skills`, with the same
|
||||
copy first, when OpenRig installed it (a copy installed another way is left as it is). It now says what setup and
|
||||
Codex seats write to a Codex home, offers the operator conversation to a Codex user who declined Codex's remembered
|
||||
command rule, and covers Rex.
|
||||
- **Codex seats:** OpenRig's Codex plugin is openrig-core, so Codex sees it as changed. As in 0.7.1, OpenRig renews
|
||||
its own hook approval only where the installed plugin folder matches what shipped. If a Codex seat stops at Codex's
|
||||
"Hooks need review" after the upgrade, that folder held other files: review the hooks in Codex.
|
||||
- **On Linux, check seats that use your session bus** (the keyring, notifications). From their next launch, restore or
|
||||
handover they get `DBUS_SESSION_BUS_ADDRESS=disabled:` unless their `env:` sets an address (see Known gaps).
|
||||
- **Launch fixes reach a seat at its next launch, resume, restore or handover,** not a running process: the Read
|
||||
grants a managed Claude restore needs, a Codex seat's writable git and topology folders, a seat's own account home,
|
||||
the Linux runtime directory and member tool rules.
|
||||
- **Close and reopen terminal views opened before the upgrade** (Herdr, cmux or tmux). Only views opened on 0.7.2 keep
|
||||
their tiles when a seat's session is replaced.
|
||||
- **If a seat runs on a home given by `rig up --env CLAUDE_CONFIG_DIR=…` or `--env CODEX_HOME=…`,** declare it in the
|
||||
member's or rig's `env:` instead. 0.7.2 leaves both names out of `--env`, with a note.
|
||||
|
||||
## New things you can do
|
||||
|
||||
- **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.
|
||||
- **Move a live seat between Codex and Claude Code:** `rig seat set-runtime <seat> --runtime <claude-code|codex>
|
||||
--reason "…"`, with `--dry-run` first to see the target's settings, what is cleared and the costs. The successor
|
||||
starts beside the current occupant, reads a handover (the seat's recap, its open rows and the predecessor's screen)
|
||||
and confirms with `rig seat set-runtime <seat> --confirm <switch-id> --note "…"`. Only then does the seat move, keeping
|
||||
its address, connections, rows and claims. Without a confirmation in 180 seconds (`--confirm-timeout`) nothing
|
||||
changes; `--recover` finishes an interrupted switch. The switch starts a new conversation and isn't written back to
|
||||
the team file (`docs/reference/getting-started.md`, "Move a seat to the other runtime").
|
||||
- **Run a seat on its own Claude or Codex account:** sign a home in once natively (`CLAUDE_CONFIG_DIR=<home> claude`
|
||||
then `/login`, or `CODEX_HOME=<home> codex login`), then declare that absolute path as `CLAUDE_CONFIG_DIR` or
|
||||
`CODEX_HOME` in the member's or rig's `env:`. Launch, resume, restore, handover, OpenRig's managed Codex config and
|
||||
hooks, usage limits and status follow it. `rig seat status` and `rig provider status` show the home and where it came
|
||||
from; a relative value is ignored and shown.
|
||||
- **Move one seat to another login on the same conversation:** for a Codex seat on Linux, `rig provider precheck --seat
|
||||
<seat> --to-account <name>`, then `rig provider switch --seat <seat> --to-account <name>`, using a login saved in that
|
||||
seat's Codex home (`rig auth save`). It saves the outgoing login, activates the chosen one, restarts the seat in place
|
||||
and prints a restart command for every peer on that home. For a Claude seat, `--to-account <label> --token-file
|
||||
<absolute path>` moves it to its own subscription token (from `claude setup-token`, in a file only the user can read);
|
||||
OpenRig keeps the path and label, never the token. `--stored-login` moves it back.
|
||||
- **Read a labelled Claude token's usage:** `rig provider status --refresh --provider claude --claude-token-file
|
||||
<absolute path> --credential-label <label>`. Each refresh spends one model turn; status and the TUI show only the
|
||||
cached reading. The label is yours, not an account Claude reported.
|
||||
- **Give each Claude member its own tool rules,** even in a shared folder: `claude_permissions: { allow: [...], deny:
|
||||
[...] }` on the member, or `rig seat set-permissions <seat> --rules <json-file> --reason "…"` for an existing seat.
|
||||
The update is audited, applies at the next managed launch or resume, and leaves the permission mode alone. Rules join
|
||||
OpenRig's one `--settings` value and never go into the project's Claude settings. They're native tool rules, not OS
|
||||
isolation, and an allow list doesn't withdraw broader inherited grants. Limits are under Known gaps.
|
||||
- **Say what you did about a stream item:** `rig stream act <id>` with exactly one of `--work <qitem>` (work you opened
|
||||
or joined through the queue), `--decline "<reason>"` or `--duplicate <qitem|stream id>`. Actions are append-only, the
|
||||
poster's `rig stream show` lists each reader's current action, and retention keeps an item a reader acted on. A
|
||||
reader who links different work from someone else is told about theirs.
|
||||
- **Pass held work on over MCP:** `rig_queue_handoff` closes an item as handed off and opens its successor in one step,
|
||||
and `rig_queue_unclaim` puts a claim back. The MCP server has 36 tools.
|
||||
- **Release a row parked on a typed blocker:** `rig queue resolve <id> --decision "<evidence>"` now also unparks a row
|
||||
blocked on `external:`, `fold:` or `auth:`, and records the evidence as a wait resolution, not a person's decision.
|
||||
- **Open views in Rex:** from a Rex pane, `rig terminal open <view> --window` or the TUI opens the view as a new Rex
|
||||
session and switches the Rex app to it. `--provider rex` picks it explicitly. Outside Rex the default stays Herdr,
|
||||
then cmux (`docs/reference/terminal-sessions.md`). Not run on Linux.
|
||||
- **Hand a community harness its first turn at launch:** a preset's `first_input: true` writes the first startup text
|
||||
to a private file and passes `OPENRIG_FIRST_INPUT=<path>`; the launcher hands it to the harness. The Cursor example
|
||||
uses it and now sets `startup_assisted: false`. OpenClaw gains a Gateway-client example (`openclaw-gateway.yaml`)
|
||||
beside the local one, and both resume a named conversation.
|
||||
- **Start one seat of a materialized team:** after `rig import <spec> --materialize-only`, `rig launch <rig> <logical
|
||||
id>` (for example `dev.lead`) starts only that never-started seat. Teams materialized by older daemons aren't covered.
|
||||
- **Clear a prompt a hook still names:** once `rig capture <seat>` shows it's gone, `rig seat clear-attention <seat>
|
||||
--reason "<what you saw>"` clears a retained permission or waiting hook, so sends and wakes go through. A refusal that
|
||||
rests on such a hook now names this step.
|
||||
|
||||
## Behaviour that changed
|
||||
|
||||
- **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.
|
||||
- **A working Claude seat that quotes approval text** in its output no longer refuses sends and wakes as if it were at
|
||||
a prompt, including when its input box is drawn high in a tall pane. The permission dialog, AskUserQuestion and the
|
||||
MCP menu still refuse.
|
||||
- **A Claude draft whose first line is blank** reads as a draft, so a send doesn't type into it or submit it.
|
||||
- **A draft-aware paste to a Claude seat** is no longer read as edited when Claude swaps its footer for "paste again to
|
||||
expand", so OpenRig no longer withholds Enter for that reason. It isn't confirmed to fix #14 (see Known gaps).
|
||||
- **A send no longer answers a Pi dialog.** At an adopted Pi TUI's selector (a confirmation), an ordinary `rig send`
|
||||
used to type and press Enter, choosing the highlighted answer; it now refuses with `target_needs_input` and types
|
||||
nothing (#2015). #2026 extends this to Pi v1.4.0 extension input and editor dialogs, including help wrapped over up to
|
||||
three rows in an 80-column pane. The audited deliberate-interaction route is unchanged.
|
||||
- **Runner-managed Pi seats show and cancel an extension dialog** in the pane, so nothing is approved, and need
|
||||
attention until their next agent run. On Pi and OMP seats, a second startup block joins the running turn instead of
|
||||
failing with "Agent is already processing".
|
||||
- **A send to a busy Codex seat** warns that the message joins the running turn and may redirect it, and names
|
||||
`--wait-for-idle <seconds>` for separate work.
|
||||
- **A seat with another pane in its tmux session** gets sends and wakes in its recorded pane instead of a refusal.
|
||||
- **A wake refused at an approval prompt** retries once when the seat's attention clears, instead of waiting for the
|
||||
stuck sweep.
|
||||
- **A seat held at a choice prompt that no hook announces** (a usage-limit dialog, say) now escalates once the refusal
|
||||
has held for one retry interval (`queue.wake_retry_interval_seconds`, 300 seconds by default). Choice-prompt alerts
|
||||
follow `queue.wake_human_rung`: under `explicit-only` they stay with the operator seat unless that seat is a person.
|
||||
Permission-prompt alerts post as before.
|
||||
- **The parked-owner reminder waits 5 minutes** after a seat starts waiting, so a seat that just handed work off or
|
||||
asked a person isn't reminded at once; a seat at an approval or question prompt gets no grace. The reminder tells a
|
||||
seat waiting on a person to hold the row (`rig queue block <id> --on <who> --wake-after <duration>`), not ask again.
|
||||
- **A dead seat's stuck-sweep finding goes to someone who can act:** the operator agent (`agents.operator_session`)
|
||||
when set, then the operator seat (`workspace.operator_seat_name`), then the row's creator, skipping any whose session
|
||||
is confirmed missing. `workspace.operator_seat_name` still decides where approvals go.
|
||||
- **Rows OpenRig creates itself** (stuck-sweep findings, workflow escalations, continuity batons, diagnoses run with
|
||||
`--apply`) wake from OpenRig's own sender, with no reply line, instead of from the row's creator. When the sweep
|
||||
closes its own finding, the actor is `stuck-sweep@system`.
|
||||
- **A cross-host queue write that times out reads INDETERMINATE,** not failed, and the CLI waits 15 seconds for the
|
||||
daemon's answer. When `rig queue create` gets no answer, it reads the row back once by its ID and reports an exact
|
||||
match as created. It never sends the create twice.
|
||||
- **Codex seats keep write access to their git metadata** after resume, fork and a restore without a pod, so they can
|
||||
still commit, and to their own topology folder, so they keep `LEARNED.md` and their recap. `rig context recap-write`
|
||||
falls back to the daemon when the seat's sandbox refuses the write.
|
||||
- **Recap writes refuse a link** at `RECAP.md` or `recap-superseded/` with `recap_write_refused`, a layout that used to
|
||||
work. Replace the link with a real file or folder.
|
||||
- **`rig seat restart --keep-conversation` on a Codex seat** keeps a writable root whose path holds a space, proves the
|
||||
resume in the `CODEX_HOME` the running Codex uses, and reads complete when a directory it was given has a space. When
|
||||
the resume isn't proven, it says the pane restarted, not "nothing was restarted".
|
||||
- **A Codex seat at Codex 0.164.0's sign-in chooser or API-key entry, or 0.162.1's chooser,** reports
|
||||
`attention_required` with `login_required` instead of a readiness timeout. Nothing is typed into the login screen.
|
||||
- **Resume recognition:** a resumed Codex whose footer shows a display name with a reasoning label (`GPT-6.1 Sol
|
||||
medium`) or a non-`gpt` model reads as resumed, so `rig up --existing` doesn't stop. A Claude or Codex resume chooser
|
||||
drawn high in a tall pane reads as a chooser, not as resumed.
|
||||
- **Codex hooks:** when a Codex home has a `hooks.json`, OpenRig puts its activity hooks there at the next Codex seat
|
||||
launch and moves its own old block out of `config.toml`. Your hooks, their order, trust and disables stay; your own
|
||||
inline hooks stay in `config.toml`. Inline hook trust is keyed by the group's actual position.
|
||||
- **A managed Claude restore reads its own files without a Read approval:** the seat's marker and restore map, the
|
||||
generic or configured extra-instruction file, the restore skill files, restore maps under a configured
|
||||
`preparation_root` and the packet files are granted at launch and resume, at both the named and the real path (so
|
||||
macOS `/tmp` works).
|
||||
- **After managed compaction,** a Claude seat's restore starts on the next poll instead of waiting for the screen to
|
||||
change. `rig compact --state` and seat status show an owed restore stage and its last delivery result; repeated
|
||||
failures open one queue item for the delegation owner.
|
||||
- **A Claude startup refused late** (an expired login) no longer gets Enter on its next startup input: OpenRig checks
|
||||
again before pressing it and reports attention. The text may stay in the input box; don't resend it.
|
||||
- **A Claude seat with the model alias `haiku` and `plan` mode** gets a line in its launch, restore and `rig seat
|
||||
status` results: Claude Code answers plan turns with Sonnet; set a full Haiku id with `rig seat set-model` to keep
|
||||
Haiku. Nothing about the launch changes.
|
||||
- **The model-divergence monitor** no longer flags a Claude seat pinned to an undated id that answers with its dated
|
||||
snapshot, or one pinned to a Bedrock cross-Region profile id such as `global.anthropic.<model>`.
|
||||
- **A pod seat that never launched** starts fresh from its saved launch recipe after a daemon restart, instead of
|
||||
sitting at a shell. That starts the harness and can use provider usage. Seats materialized before 0.7.2 have no recipe
|
||||
and report `attention_required`, keeping the shell.
|
||||
- **`rig down` after a team's tmux server died** treats tmux's "no server running" as absence and records the seats
|
||||
stopped, so `rig archive` and `rig up` go ahead. A team of plain terminal seats restores as `fully_restored`, and
|
||||
`rig down` names the terminal sessions it stops.
|
||||
- **A launch whose tmux made no session** fails with `session_not_created` or `session_unconfirmed` instead of reporting
|
||||
the seat launched. The message says which directory must be writable.
|
||||
- **Terminal tiles survive a seat's session replacement:** a tile waits up to 30 seconds for the same session and seat
|
||||
identity, then reattaches. A fresh replacement's result says how to reopen the view. Input still queued when a
|
||||
terminal client disconnects is dropped, not typed.
|
||||
- **Reopening a Herdr view** with `--provider herdr` adds a workspace each time. From inside Herdr, `rig terminal open
|
||||
<view> --window` focuses a matching one. The `openrig-herdr` skill now says so.
|
||||
- **A service bundle install without Docker Compose v2** stops with `compose_missing` before writing the target or
|
||||
creating the team. `rig bundle install --plan` warns instead.
|
||||
- **`rig export`** copies the declared culture file beside the YAML for a local daemon, and names agent refs that leave
|
||||
the folder and a rig still named `starter`.
|
||||
- **`rig daemon start`:** a backlog of queue wakes no longer holds startup past the CLI's readiness wait. When the
|
||||
database-conflict check can't tell (no `lsof`, a timeout, a listener that doesn't answer), startup warns that
|
||||
ownership is unknown, under either `daemon.db_conflict` setting.
|
||||
- **Proof and scope:** proof-source monitoring watches the missions root, not the whole workspace parent. `rig proof
|
||||
add <absolute path>` finds a slice in another catalogued project. `rig scope mission create` writes
|
||||
`metadata.name`; an older mission without it needs the line added before it compiles. `rig queue whoami` resolves a
|
||||
project-tagged row against its catalogued project.
|
||||
- **Slack:** a registered person's reply to a question parked on someone else reaches the agent as their own message
|
||||
and leaves the question parked.
|
||||
- **`rig context get reference/acp-agents.md`** now returns the guide `rig acp run --help` names.
|
||||
- **TUI and dashboard:** the focused Table and Overview pages load health findings instead of staying `Not assessed`,
|
||||
the activity stream keeps its connection state across pages, and the dashboard shows "boot not tracked" for a skipped
|
||||
kernel boot.
|
||||
- **The kernel operator's guidance** puts a project task on the chosen team's queue rather than doing it itself.
|
||||
- **`rig auth switch` and `rig provider switch`'s printed undo** say a restored Codex login isn't verified and may need
|
||||
a fresh sign-in.
|
||||
|
||||
## Output that changed shape
|
||||
|
||||
- **`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.
|
||||
- **`rig capture` and the MCP capture tool** add `promptPane: { width, height, hint }` when a seat at a prompt has a
|
||||
pane smaller than 80x24, and the CLI prints the resize hint on stderr.
|
||||
- **`rig seat status`** shows the seat's runtime home and where it came from, any ignored home, a Claude token
|
||||
reference and member tool rules. `--json` always has `permissions.account.advisories`, empty unless a note applies.
|
||||
- **`rig stream show`** lists each reader's current action, flags `parallelWork`, and reads `readers: "unavailable"`
|
||||
from an older daemon. The expired-cursor 410 says how `--since` resumes.
|
||||
- **Slack posts start with their sender** (`From <seat>`, with ` on <machine>` when known), and a wake-rung escalation
|
||||
reads `Waiting on <seat>: <summary>`. Rows from Slack read `<address> via Slack: …`.
|
||||
- **A wake from a row OpenRig created** ends with "↩ Sent by OpenRig itself, not by a seat. Act on the row it names."
|
||||
- **Stuck-sweep findings** name the rule that chose their recovery seat, and a stalled-after-claim age counts from the
|
||||
stall, not from the claim.
|
||||
- **The parked diagnosis** reports `activity.since`.
|
||||
- **Cross-host queue writes:** a timed-out forward's 502 carries `outcome: "indeterminate"`, the CLI's headline reads
|
||||
"queue request got no answer in time (routing to host '…')", and a read-back reports "The create committed after all:
|
||||
row … (read back by its ID)".
|
||||
- **`rig queue update --help` and the `closure_fields_not_admitted` refusal** say how to cancel a row.
|
||||
- **`rig ps --nodes --json`** carries `login_required` for a Codex seat at sign-in; a restore result marks a plain
|
||||
terminal seat `noConversation`.
|
||||
- **A send to a seat mid-switch** is held at once: `outcome: retained`, `reason: runtime_switch_in_progress`.
|
||||
- **New codes:** `permission_rules_unavailable` (a fork whose source rules can't be read), `occupant_retired_by_switch`
|
||||
and `switch_not_committed` (the two processes of a runtime switch), `runtime_unchanged` and the `runtime_switch_*`
|
||||
refusals, `session_not_created`, `session_unconfirmed`, `compose_missing`, `rex_outcome_unknown`,
|
||||
`invalid_claude_token_selection`, and input checks on `rig stream act`.
|
||||
- **Help:** 14 subcommands that had no summary have one, and `rig skill audit` ends by saying it doesn't judge whether
|
||||
a refresh is safe, naming the read-only `rig skill loadout` check.
|
||||
|
||||
## What to stop doing
|
||||
|
||||
- **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.
|
||||
- **Don't pass `CLAUDE_CONFIG_DIR` or `CODEX_HOME` with `rig up --env`.** Declare the home in `env:`.
|
||||
- **Don't rebuild a team to move one seat** to the other runtime or another login. Use `rig seat set-runtime` or
|
||||
`rig provider switch --seat`.
|
||||
- **Don't close and re-create to pass work on over MCP.** Use `rig_queue_handoff`.
|
||||
- **Don't cancel with `--state canceled --closure-reason canceled`.** Use `--state done --closure-reason canceled`.
|
||||
- **Don't take the first `qitem-…` from `rig queue create`'s merged output.** The request ID on stderr isn't a row:
|
||||
read `qitemId` from stdout and check the exit code.
|
||||
- **Don't retry a cross-host queue write that timed out.** Reconcile by its ID first.
|
||||
- **Don't reopen a Herdr view with `--provider herdr` to get back to it.** Focus it, or use `--window` from inside Herdr.
|
||||
- **Don't put one member's tool rules in a shared project's Claude settings.** Use `claude_permissions`.
|
||||
|
||||
## Known gaps in 0.7.1
|
||||
## Known gaps in 0.7.2
|
||||
|
||||
- **On Linux, a seat now gets the daemon's runtime directory with a session bus address that can't connect** (#2036).
|
||||
When the daemon has an absolute `XDG_RUNTIME_DIR`, a seat launched, restored or handed over gets it with
|
||||
`DBUS_SESSION_BUS_ADDRESS=disabled:`, so its D-Bus clients don't find your session bus on their own. This is a
|
||||
default, not isolation: `systemctl --user` reaches your user service manager from a seat. A shell startup file that
|
||||
sets only `XDG_RUNTIME_DIR` now leaves the seat with `disabled:`; to use a session bus, set the address in the rig's
|
||||
or member's `env:`. A Codex seat's stdio MCP server still needs `env_vars = ["XDG_RUNTIME_DIR"]` in Codex's
|
||||
`config.toml` to see the variable. macOS, a daemon started without `XDG_RUNTIME_DIR` and
|
||||
`terminal.inherit_tmux_environment` on are unchanged.
|
||||
- **Per-member Claude tool rules** (#1914): member denies aren't claimed to hold in bypass mode (`full_bypass`).
|
||||
Handover and restore carry the rules, but aren't proven to keep the same conversation. Checked natively: two seats in
|
||||
one folder each enforce their own rules, a managed policy wins, and fresh, resume and fork launches enforce them.
|
||||
- **Going back to 0.7.1:** like any version before #1914, it drops the stored member rules from launch, so set
|
||||
equivalent native rules first (`docs/reference/rig-spec.md`, "Downgrade"). It also refuses a community preset that
|
||||
sets `first_input`. No other way back to 0.7.1 has been tested.
|
||||
- **Pi dialog recognition is a bounded screen check.** #2015 recognizes Pi's selector by its own chrome; with more than
|
||||
six non-blank rows after its border, or under a widget that draws its own border, a send goes through as before.
|
||||
#2026 adds Pi v1.4.0's extension input and editor dialogs; clipped or incomplete help, help wrapped over more than
|
||||
three rows and custom extension screens keep their earlier reading, and a draft that copies a dialog's whole frame
|
||||
reads as that dialog. Each was checked natively on its own cases (#2015 its confirmation dialog, #2026 the input and
|
||||
editor dialogs on its own build), not on every layout. Check the pane before sending to an adopted Pi seat that may be
|
||||
at a dialog. Neither
|
||||
check was repeated on the final 0.7.2 build.
|
||||
- **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. 0.7.2 treats the "paste again to expand" hint as part of
|
||||
the composer, but the original failure didn't reproduce in its native check, so this isn't confirmed fixed; #14
|
||||
remains open.
|
||||
- **An idle Claude seat whose recent output quotes prompt text** can still read as at a prompt and refuse sends, as can
|
||||
a seat with a draft, a mode bar OpenRig doesn't recognise or an unframed input box. Check the pane.
|
||||
- **A Claude startup refusal that arrives after OpenRig's last check can still race Enter.** If a seat with an expired
|
||||
login shows staged text in its input box, sign in, then clear or send that text yourself.
|
||||
- **A managed Claude restore can still ask for a Read approval** when `policies.claude_compaction.message_file_path` is
|
||||
relative, or for a packet file after a resume changed the conversation id. The session transcript isn't granted. A native
|
||||
check on Claude 2.1.309 passed the Read grants with no approval prompt; a complete restore with no help isn't claimed.
|
||||
- **`rig seat set-runtime`:** Claude launch additions such as an agent file aren't applied to a switched seat, startup
|
||||
slash commands aren't run, and a person typing into the old pane of a draft-aware seat stops the switch (the current
|
||||
occupant keeps the seat). A message sent during a switch is held and must be sent again.
|
||||
- **A seat's own account:** a conversation saved under a seat's earlier home doesn't resume in its new one, and live
|
||||
usage refresh (`rig provider status --refresh`) still reads only the daemon's own logins. `rig provider switch --seat`
|
||||
moves a Codex seat on Linux only. Other seats on that Codex home keep the old login until their next token refresh,
|
||||
then stop: run the restart command the switch printed for each.
|
||||
- **Seat account and runtime moves weren't run with real accounts before this release** (#1895, #1937, #1972, #1980).
|
||||
The tests use stubs; check a moved seat's account and conversation yourself after the first move.
|
||||
- **A Codex seat whose queue-state folder or team workspace path holds a space** can still read as not resumed after an
|
||||
ordinary resume or restore. An in-place restart is fixed.
|
||||
- **Choice-prompt alerts go to a person one at a time per machine.** While the first one's seat stays blocked, later
|
||||
choice alerts wait unposted; they're visible as rows.
|
||||
- **Herdr's own status for a seat can read idle or unknown while OpenRig shows it waiting** (`needs-input`). Use
|
||||
OpenRig's view or the Feed to find waiting seats.
|
||||
- **Proof-source watching still recurses through a large missions tree,** and the reported Linux daemon stalls (#1813)
|
||||
weren't reproduced to confirm that the narrower watch helps.
|
||||
- **A Claude seat signed in with a long-lived token (`claude setup-token`) gets no automatic usage reading;** the reader
|
||||
reports `not_subscription_login`. Read one on demand with `rig provider status --refresh --provider claude
|
||||
--claude-token-file <path> --credential-label <label>`, which spends a model turn.
|
||||
- **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
|
||||
@@ -172,21 +295,11 @@ after an upgrade it can still hold the previous version's note.
|
||||
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`.
|
||||
--provider herdr`, which adds a new workspace; close the old one.
|
||||
- **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`.
|
||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/daemon",
|
||||
@@ -7069,7 +7069,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@openrig/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -7104,7 +7104,7 @@
|
||||
},
|
||||
"packages/daemon": {
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.13.0",
|
||||
@@ -7146,7 +7146,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@openrig/ui",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"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,6 +1,6 @@
|
||||
{
|
||||
"name": "openrig-core",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.19",
|
||||
"description": "OpenRig canonical skills and hooks for cross-runtime agent topology coordination, continuity, and operating discipline.",
|
||||
"author": {
|
||||
"name": "Mike Schwarz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openrig-core",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.19",
|
||||
"description": "OpenRig canonical skills and hooks for cross-runtime agent topology coordination, continuity, and operating discipline.",
|
||||
"author": {
|
||||
"name": "Mike Schwarz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { ProjectionPlan, ProjectionEntry } from "../domain/projection-plann
|
||||
import { assessNativeResumeProbe } from "../domain/native-resume-probe.js";
|
||||
import { resumeProbeEvidence } from "../domain/native-resume-probe-evidence.js";
|
||||
import { resolveReadinessTimeoutMs } from "../domain/readiness-timeout.js";
|
||||
import { subtreeNamesClaudeExecutable, claudeCommandSessionToken, listNativeProcesses, observeClaudeDelivery, observeClaudePaneProcess, observeClaudePaneRuntime, observeCodexPaneProcess, verifyClaudePaneProcess, verifyCodexPaneProcess, verifyPaneIdleShell, witnessedClaudeVersionLabel, type ClaudeLaunchedProcess, type NativeProcessLister, type VersionLabelWitnessWait } from "../domain/native-process-lineage.js";
|
||||
import { subtreeNamesClaudeExecutable, claudeCommandSessionToken, listNativeProcesses, observeClaudeDelivery, observeClaudePaneProcess, observeClaudePaneRuntime, observeCodexPaneProcess, verifyClaudePaneProcess, verifyCodexPaneProcess, verifyPaneIdleShell, type ClaudeLaunchedProcess, type NativeProcessLister } from "../domain/native-process-lineage.js";
|
||||
import { isPerSeatRoleGuidance, mergeManagedBlock, perSeatRoleSkipWarning, DEFAULT_CLAUDE_MANAGED_BLOCK_FILE, type ClaudeManagedBlockFile } from "../domain/managed-blocks.js";
|
||||
import { claudeResumeLaunchEnv, claudeResumeLaunchPrefix } from "./claude-resume-launch.js";
|
||||
import { shellQuote } from "./shell-quote.js";
|
||||
@@ -66,7 +66,6 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
private nodeExecutable: string;
|
||||
private autoDriveProviderPrompts: boolean;
|
||||
private listProcesses?: NativeProcessLister;
|
||||
private resolvePermissionMode?: (nodeId: string) => string | undefined;
|
||||
private readinessTimeoutMs?: number;
|
||||
private autoLaunches = new Map<string, { binding: NodeBinding; token: string; freshInPlace?: boolean; executable?: string; scriptLaunch?: boolean; fingerprint?: string; process?: ClaudeLaunchedProcess }>();
|
||||
readonly claudeManagedLaunch?: ClaudeManagedLaunch;
|
||||
@@ -90,9 +89,6 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
nodeExecutable?: string;
|
||||
autoDriveProviderPrompts?: boolean;
|
||||
listProcesses?: NativeProcessLister;
|
||||
/** #86 — the seat's persisted managed permission mode (NativePermissionStore.resolve), undefined when
|
||||
* unmanaged; throws when it can't be read. Absent → every seat counts as managed for the version-label witness. */
|
||||
resolvePermissionMode?: (nodeId: string) => string | undefined;
|
||||
/** Bound on a slow resume's wait, from the launch (default: runtime.readiness_timeout_seconds). */
|
||||
readinessTimeoutMs?: number;
|
||||
claudeManagedLaunch?: ClaudeManagedLaunch;
|
||||
@@ -116,7 +112,6 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
this.nodeExecutable = deps.nodeExecutable ?? process.execPath;
|
||||
this.autoDriveProviderPrompts = deps.autoDriveProviderPrompts ?? false;
|
||||
this.listProcesses = deps.listProcesses;
|
||||
this.resolvePermissionMode = deps.resolvePermissionMode;
|
||||
this.readinessTimeoutMs = deps.readinessTimeoutMs;
|
||||
this.claudeManagedLaunch = deps.claudeManagedLaunch;
|
||||
this.activityRelayPath = deps.activityRelayPath ?? null;
|
||||
@@ -449,13 +444,6 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
|
||||
// -- Private helpers --
|
||||
|
||||
/** #86: only a launch binding carries permissionMode, so the seat's persisted selection decides for every other
|
||||
* caller. Unmanaged needs both to say so; a missing or unreadable selection counts as managed. */
|
||||
private isUnmanaged(binding: NodeBinding): boolean {
|
||||
if (binding.permissionMode !== undefined || !this.resolvePermissionMode) return false;
|
||||
try { return this.resolvePermissionMode(binding.nodeId) === undefined; } catch { return false; }
|
||||
}
|
||||
|
||||
/** A fresh in-place launch keeps the retiree's scrollback. Qualify a history-based ready result
|
||||
* with the current frame, only for the exact launch that requested it. Unavailable observations
|
||||
* retain the old admission (including its ambiguity); visible gates and conflicting identity do not. */
|
||||
@@ -494,16 +482,9 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
return withIdentity.status === "resumed" && occupant !== "absent" ? historyProbe : current;
|
||||
}
|
||||
|
||||
private async assessManagedProbe(binding: NodeBinding, paneCommand: string | null, paneContent: string, options?: ReadinessOptions, witnessWait?: VersionLabelWitnessWait) {
|
||||
private async assessManagedProbe(binding: NodeBinding, paneCommand: string | null, paneContent: string, options?: ReadinessOptions) {
|
||||
const input = { runtime: "claude-code", paneCommand, paneContent };
|
||||
const probe = assessNativeResumeProbe(input);
|
||||
// #86: an unmanaged native install's version label reads as `claude` once its OS executable is witnessed. The
|
||||
// witness runs only when the label would change the result; a managed seat keeps its exact proof below.
|
||||
const asClaude = assessNativeResumeProbe({ ...input, paneCommand: "claude" });
|
||||
if (probe.status !== "resumed" && asClaude.status === "resumed" && this.isUnmanaged(binding) && binding.tmuxSession
|
||||
&& await witnessedClaudeVersionLabel(paneCommand, { target: binding.tmuxSession, tmux: this.tmux, listProcesses: this.listProcesses }, witnessWait)) {
|
||||
return asClaude;
|
||||
}
|
||||
// Headerless managed launches (including bypass mode) use the same readiness
|
||||
// precedence as resume. This candidate result is returned only after the
|
||||
// exact launch identity below is proved; screen text alone is insufficient.
|
||||
@@ -623,13 +604,11 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
const checkEvery = 5;
|
||||
let attempts = windowAttempts;
|
||||
let slowStart = false;
|
||||
// #86: one record per wait; after an actual path-read timeout the rest of this wait skips the witness.
|
||||
const witnessWait: VersionLabelWitnessWait = { pathReadTimedOut: false };
|
||||
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
const paneCommand = await this.tmux.getPaneCommand(tmuxSession);
|
||||
const paneContent = (await this.tmux.capturePaneContent(tmuxSession, 40)) ?? "";
|
||||
const probe = await this.assessManagedProbe(binding, paneCommand, paneContent, undefined, witnessWait);
|
||||
const probe = await this.assessManagedProbe(binding, paneCommand, paneContent);
|
||||
|
||||
if (probe.code === "no_conversation_found") {
|
||||
return {
|
||||
@@ -716,7 +695,7 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
|
||||
const finalCommand = await this.tmux.getPaneCommand(tmuxSession);
|
||||
const finalContent = (await this.tmux.capturePaneContent(tmuxSession, 40)) ?? "";
|
||||
const finalProbe = await this.assessManagedProbe(binding, finalCommand, finalContent, undefined, witnessWait);
|
||||
const finalProbe = await this.assessManagedProbe(binding, finalCommand, finalContent);
|
||||
|
||||
if (finalProbe.status === "resumed") {
|
||||
return { ok: true };
|
||||
|
||||
@@ -9,7 +9,7 @@ import { shellQuote } from "./shell-quote.js";
|
||||
import { claudeResumeLaunchEnv, claudeResumeLaunchPrefix } from "./claude-resume-launch.js";
|
||||
import { claudePostureFlag, claudeClassicRendererEnvPrefix } from "./yolo-mode.js";
|
||||
import { assessNativeResumeProbe } from "../domain/native-resume-probe.js";
|
||||
import { verifyClaudePaneProcess, verifyPaneIdleShell, witnessedClaudeVersionLabel, type ClaudeLaunchedProcess, type NativeProcessLister, type VersionLabelWitnessWait } from "../domain/native-process-lineage.js";
|
||||
import { verifyClaudePaneProcess, verifyPaneIdleShell, type ClaudeLaunchedProcess, type NativeProcessLister } from "../domain/native-process-lineage.js";
|
||||
import { observeClaudePermission, type AppliedLaunchObservation } from "../domain/permission-drift.js";
|
||||
import { unresolvedClaudePermissionModes } from "../domain/native-permission-selection.js";
|
||||
import type { ClaudeManagedLaunch } from "../domain/claude-managed-launch.js";
|
||||
@@ -139,7 +139,7 @@ export class ClaudeResumeAdapter {
|
||||
return { ok: false, code: "resume_failed", message: keyResult.message };
|
||||
}
|
||||
|
||||
const result = await this.verifyResume(tmuxSessionName, resumeToken!, !managed).catch((error): ResumeResult => ({
|
||||
const result = await this.verifyResume(tmuxSessionName, resumeToken!).catch((error): ResumeResult => ({
|
||||
ok: false,
|
||||
code: "attention_required",
|
||||
message: `Claude resume observation unavailable; launch retained: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -147,29 +147,20 @@ export class ClaudeResumeAdapter {
|
||||
return result.ok ? { ...result, appliedLaunch, ...(tokenWarnings.length ? { warnings: tokenWarnings } : {}) } : result;
|
||||
}
|
||||
|
||||
/** #86: as the launch adapter's probe, an unmanaged native install's version label reads as `claude` once its OS
|
||||
* executable is witnessed. The witness runs only when the label would change the result. */
|
||||
private async assessProbe(target: string, paneCommand: string | null, paneContent: string, unmanaged: boolean, witnessWait: VersionLabelWitnessWait) {
|
||||
const input = { runtime: "claude-code", paneCommand, paneContent };
|
||||
const probe = assessNativeResumeProbe(input);
|
||||
const asClaude = assessNativeResumeProbe({ ...input, paneCommand: "claude" });
|
||||
if (unmanaged && probe.status !== "resumed" && asClaude.status === "resumed"
|
||||
&& await witnessedClaudeVersionLabel(paneCommand, { target, tmux: this.tmux, listProcesses: this.options.listProcesses }, witnessWait)) return asClaude;
|
||||
return probe;
|
||||
}
|
||||
|
||||
private async verifyResume(tmuxSessionName: string, resumeToken: string, unmanaged: boolean): Promise<ResumeResult> {
|
||||
private async verifyResume(tmuxSessionName: string, resumeToken: string): Promise<ResumeResult> {
|
||||
const pollMs = this.options.pollMs ?? 200;
|
||||
const maxWaitMs = this.options.maxWaitMs ?? 5_000;
|
||||
const sleepFn = this.options.sleep ?? sleep;
|
||||
const attempts = Math.max(1, Math.floor(maxWaitMs / Math.max(pollMs, 1)) + 1);
|
||||
// #86: one record per wait; after an actual path-read timeout the rest of this wait skips the witness.
|
||||
const witnessWait: VersionLabelWitnessWait = { pathReadTimedOut: false };
|
||||
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
const paneCommand = await this.tmux.getPaneCommand(tmuxSessionName);
|
||||
const paneContent = (await this.tmux.capturePaneContent(tmuxSessionName, 40)) ?? "";
|
||||
const probe = await this.assessProbe(tmuxSessionName, paneCommand, paneContent, unmanaged, witnessWait);
|
||||
const probe = assessNativeResumeProbe({
|
||||
runtime: "claude-code",
|
||||
paneCommand,
|
||||
paneContent,
|
||||
});
|
||||
|
||||
if (probe.code === "no_conversation_found") {
|
||||
return {
|
||||
@@ -204,7 +195,11 @@ export class ClaudeResumeAdapter {
|
||||
|
||||
const finalCommand = await this.tmux.getPaneCommand(tmuxSessionName);
|
||||
const finalContent = (await this.tmux.capturePaneContent(tmuxSessionName, 40)) ?? "";
|
||||
const finalProbe = await this.assessProbe(tmuxSessionName, finalCommand, finalContent, unmanaged, witnessWait);
|
||||
const finalProbe = assessNativeResumeProbe({
|
||||
runtime: "claude-code",
|
||||
paneCommand: finalCommand,
|
||||
paneContent: finalContent,
|
||||
});
|
||||
|
||||
if (finalProbe.code === "no_conversation_found") {
|
||||
return { ok: false, code: "retry_fresh", message: "Claude resume failed: no conversation found for the requested session" };
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { TmuxAdapter } from "../adapters/tmux.js";
|
||||
import type { TranscriptStore } from "./transcript-store.js";
|
||||
import { startTmuxTranscriptCapture } from "./transcript-capture.js";
|
||||
import { deriveResumeToken, type ResumeTokenCaptureDeps } from "./resume-token-capture.js";
|
||||
import { verifyClaudePaneRuntime, verifyCodexPaneProcess, witnessedClaudeVersionLabel, type NativeProcessLister } from "./native-process-lineage.js";
|
||||
import { verifyClaudePaneRuntime, verifyCodexPaneProcess, type NativeProcessLister } from "./native-process-lineage.js";
|
||||
import {
|
||||
observeSolePane,
|
||||
paneObservationVerdict,
|
||||
@@ -113,7 +113,7 @@ interface ClaimServiceDeps {
|
||||
};
|
||||
/** #421 — start time of the pane's current Claude process; a sidecar sampled earlier is skipped. */
|
||||
claudeProcessStartedAt?: ResumeTokenCaptureDeps["claudeProcessStartedAt"];
|
||||
/** Process census for the runtime proof in `reconcileSession` and #86's native version-label witness; defaults to the live process table. */
|
||||
/** Process census for the runtime proof in `reconcileSession`; defaults to the live process table. */
|
||||
listProcesses?: NativeProcessLister;
|
||||
resumeTokenCapturer?: {
|
||||
captureCodexThreadId(sessionName: string): Promise<string | undefined>;
|
||||
@@ -575,10 +575,11 @@ export class ClaimService {
|
||||
const projectionDrift: string[] = [];
|
||||
const paneObservation = await this.observeBindingPane(sessionName);
|
||||
if (!paneObservation.ok) projectionDrift.push(paneObservation.detail);
|
||||
const paneTarget = paneObservation.ok ? paneObservation.pane : sessionName;
|
||||
let paneCommand: string | null = null;
|
||||
try {
|
||||
paneCommand = await this.tmuxAdapter.getPaneCommand(paneTarget);
|
||||
paneCommand = await this.tmuxAdapter.getPaneCommand(
|
||||
paneObservation.ok ? paneObservation.pane : sessionName,
|
||||
);
|
||||
} catch { paneCommand = null; }
|
||||
if (nodeRow.runtime) {
|
||||
const expectation: Record<string, string[]> = {
|
||||
@@ -590,10 +591,7 @@ export class ClaimService {
|
||||
if (!paneCommand) {
|
||||
projectionDrift.push(`runtime unverified: pane command unreadable; node declares runtime "${nodeRow.runtime}"`);
|
||||
} else if (expected && expected.length > 0 && !expected.includes(paneCommand)
|
||||
// #86: a native install's version label confirms claude-code only with its OS executable witness.
|
||||
&& !(nodeRow.runtime === "claude-code" && await witnessedClaudeVersionLabel(paneCommand,
|
||||
{ target: paneTarget, tmux: this.tmuxAdapter, listProcesses: this.listProcesses }))
|
||||
&& !await this.runtimeProvenInPane(nodeRow.runtime, paneTarget)) {
|
||||
&& !await this.runtimeProvenInPane(nodeRow.runtime, paneObservation.ok ? paneObservation.pane : sessionName)) {
|
||||
// A script-launched runtime reads as its launcher (`sh`); only a pane whose process tree shows no such runtime is drift.
|
||||
projectionDrift.push(`runtime unverified: pane command "${paneCommand}" does not confirm runtime "${nodeRow.runtime}"`);
|
||||
}
|
||||
|
||||
@@ -18,16 +18,9 @@ function run(argv) {
|
||||
}));
|
||||
}`;
|
||||
|
||||
/** execFile's own timeout kill (Node sets killed with the kill signal); a maxBuffer overflow or a failed run isn't one. */
|
||||
function isTimeoutKill(error: unknown): boolean {
|
||||
const e = error as { killed?: unknown; signal?: unknown } | null;
|
||||
return e?.killed === true && e.signal === "SIGTERM";
|
||||
}
|
||||
|
||||
/** Optional OS executable witnesses. Unavailable, exited and over-budget PIDs
|
||||
* have no witness; this never substitutes argv or a version string for a path.
|
||||
* `onTimeout` hears only an actual read timeout, never any other missing path. */
|
||||
export async function readNativeExecutablePaths(requested: number[], opts: { onTimeout?: () => void } = {}): Promise<Map<number, string>> {
|
||||
* have no witness; this never substitutes argv or a version string for a path. */
|
||||
export async function readNativeExecutablePaths(requested: number[]): Promise<Map<number, string>> {
|
||||
const pids = [...new Set(requested.filter(pid => Number.isInteger(pid) && pid > 0 && pid <= 2147483647))].slice(0, 1024);
|
||||
const result = new Map<number, string>();
|
||||
if (pids.length === 0) return result;
|
||||
@@ -49,9 +42,6 @@ export async function readNativeExecutablePaths(requested: number[], opts: { onT
|
||||
if (typeof path === "string" && path.startsWith("/") && !path.includes("\0")
|
||||
&& !path.split("/").some(part => part === "." || part === "..")) result.set(entry[0], path);
|
||||
}
|
||||
} catch (error) {
|
||||
// A missing OS witness is unknown, not positive identity.
|
||||
if (isTimeoutKill(error)) opts.onTimeout?.();
|
||||
}
|
||||
} catch { /* A missing OS witness is unknown, not positive identity. */ }
|
||||
return result;
|
||||
}
|
||||
@@ -485,28 +485,22 @@ function mayBeClaudeRuntime(row: NativeProcessRow): boolean {
|
||||
|
||||
/** The same OS observation serves menu input, restore proof and periodic identity.
|
||||
* Older callers may carry only pid/ppid/command; that is insufficient positive Codex proof. */
|
||||
export async function listNativeProcesses(opts: NativeProcessListOptions = {}): Promise<NativeProcessRow[]> {
|
||||
export async function listNativeProcesses(options: { claudeLaunchPaths?: boolean } = {}): Promise<NativeProcessRow[]> {
|
||||
try {
|
||||
// Copies: the table is shared with concurrent readers, and executable paths are added below.
|
||||
const rows: NativeProcessRow[] = (await runAsyncSite("codex.runtime.list_processes", readProcessTable)).map((row) => ({ ...row }));
|
||||
// `claudeLaunchPaths`: the caller is proving a launch through a `#!` script, which needs the OS path of the
|
||||
// foreground path-named Claude too. Every other listing skips that read (on macOS it starts a helper process).
|
||||
const candidates = rows.filter(row => needsClaudeExecutablePath(row) || (opts.claudeLaunchPaths === true && mayBeClaudeAfterScript(row)));
|
||||
const candidates = rows.filter(row => needsClaudeExecutablePath(row) || (options.claudeLaunchPaths === true && mayBeClaudeAfterScript(row)));
|
||||
if (candidates.length > 0) {
|
||||
const pids = candidates.map(row => row.pid);
|
||||
const onTimeout = opts?.onPathReadTimeout;
|
||||
const paths = await (onTimeout ? readNativeExecutablePaths(pids, { onTimeout }) : readNativeExecutablePaths(pids));
|
||||
const paths = await readNativeExecutablePaths(candidates.map(row => row.pid));
|
||||
for (const row of candidates) row.executablePath = paths.get(row.pid);
|
||||
}
|
||||
return rows;
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
/** `claudeLaunchPaths`: also read the OS path of a foreground path-named Claude (a `#!` script launch).
|
||||
* `onPathReadTimeout` (#86): told when the OS path read actually timed out. */
|
||||
export interface NativeProcessListOptions { claudeLaunchPaths?: boolean; onPathReadTimeout?: () => void }
|
||||
|
||||
export type NativeProcessLister = (opts?: NativeProcessListOptions) => NativeProcessRow[] | Promise<NativeProcessRow[]>;
|
||||
export type NativeProcessLister = () => NativeProcessRow[] | Promise<NativeProcessRow[]>;
|
||||
|
||||
/** One OS process, by pid and `ps` start time: a reused pid has another start time. */
|
||||
export interface ClaudeLaunchedProcess { pid: number; startedAt: string }
|
||||
@@ -779,30 +773,6 @@ export async function verifyClaudePaneRuntime(input: Parameters<typeof observeCl
|
||||
return second?.fingerprint === first.fingerprint ? second : null;
|
||||
}
|
||||
|
||||
const NATIVE_VERSION_LABEL = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/;
|
||||
|
||||
/** One wait's record that the optional path witness timed out, so the rest of that wait skips it. */
|
||||
export interface VersionLabelWitnessWait { pathReadTimedOut: boolean }
|
||||
|
||||
/** #86: the native installer names Claude's process, and so the pane label, after its versioned file. Whether that
|
||||
* label may stand for `claude`: only when the pane's one stable foreground Claude is the version-named process
|
||||
* itself, witnessed by its OS executable path (`claudeProcess`: argv0 claude, the installer's versions layout, a
|
||||
* basename equal to the OS name), and the pane's pid is unchanged. The label alone is never identity, and a path
|
||||
* read that fails or times out is no witness. Like a `claude` label, this says nothing about the conversation.
|
||||
* Within one `wait`, once a path read actually times out, the witness isn't tried again; a new wait starts fresh. */
|
||||
export async function witnessedClaudeVersionLabel(paneCommand: string | null,
|
||||
input: Omit<Parameters<typeof observeClaudePaneRuntime>[0], "selectedExecutable">, wait?: VersionLabelWitnessWait): Promise<boolean> {
|
||||
const label = paneCommand?.trim() ?? "";
|
||||
if (!NATIVE_VERSION_LABEL.test(label) || wait?.pathReadTimedOut) return false;
|
||||
const lister = input.listProcesses ?? listNativeProcesses;
|
||||
const onPathReadTimeout = () => { if (wait) wait.pathReadTimedOut = true; };
|
||||
const native = await verifyClaudePaneRuntime({ ...input, listProcesses: () => lister({ onPathReadTimeout }) });
|
||||
// argv0 `claude` keeps the OS path the deciding evidence: a versioned argv0 matches the OS name without it.
|
||||
if (!native || executableName(tokens(native.process.command)[0] ?? "") !== "claude"
|
||||
|| native.process.executableName !== label || !native.process.executablePath) return false;
|
||||
return await input.tmux.getPanePid(input.target).catch(() => null) === native.panePid;
|
||||
}
|
||||
|
||||
/** The `ps` start time (lstart, local time) of the one stable Claude foreground
|
||||
* process. This makes no claim about its conversation token. */
|
||||
export async function observeClaudePaneStartedAt(input: Parameters<typeof observeClaudePaneRuntime>[0]): Promise<string | null> {
|
||||
|
||||
@@ -28,7 +28,6 @@ import { migrate } from "./db/migrate.js";
|
||||
import { ALL_MIGRATIONS } from "./db/all-migrations.js";
|
||||
import { RigRepository } from "./domain/rig-repository.js";
|
||||
import { SessionRegistry } from "./domain/session-registry.js";
|
||||
import { NativePermissionStore } from "./domain/native-permission-store.js";
|
||||
import { deriveCanonicalSessionName, isHumanSeatSessionRef, parseSessionName } from "./domain/session-name.js";
|
||||
import { resolveExternal } from "./domain/gateway/external-admission.js";
|
||||
import { makeTopologyDestinationCheck } from "./domain/queue-destination-check.js";
|
||||
@@ -916,7 +915,7 @@ export async function createDaemon(opts?: DaemonOptions): Promise<DaemonResult>
|
||||
|
||||
const startupOrchestrator = new StartupOrchestrator({ db, sessionRegistry, eventBus, tmuxAdapter, readFile: (p: string) => fs.readFileSync(p, "utf-8") });
|
||||
const runtimeSettings = new ContextPackSettingsStore().resolveConfig();
|
||||
const claudeAdapter = new ClaudeCodeAdapter({ tmux: tmuxAdapter, seatLaunchEnvironment, claudeManagedLaunch, resolvePermissionMode: (nodeId: string) => new NativePermissionStore(db).resolve(nodeId, "claude-code").permissionMode, restoreReadPolicy, fsOps: { readFile: (p: string) => fs.readFileSync(p, "utf-8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c, "utf-8"), exists: (p: string) => fs.existsSync(p), mkdirp: (p: string) => fs.mkdirSync(p, { recursive: true }), copyFile: replaceFile, listFiles: (dir: string) => { const r: string[] = []; /** Collect relative file paths recursively. */ function w(d: string, pre: string) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { if (e.isDirectory()) w(nodePath.join(d, e.name), nodePath.join(pre, e.name)); else r.push(pre ? nodePath.join(pre, e.name) : e.name); } } w(dir, ""); return r; }, readdir: (dir: string) => fs.readdirSync(dir), statMode: (p: string) => fs.statSync(p).mode, chmod: (p: string, m: number) => fs.chmodSync(p, m), homedir: os.homedir() }, stateDir: OPENRIG_HOME, collectorAssetPath: nodePath.resolve(import.meta.dirname, "../assets/claude-statusline-context.cjs"), autoDriveProviderPrompts: runtimeSettings.recoveryAutoDriveProviderPrompts, activityRelayPath: nodePath.resolve(import.meta.dirname, "../assets/plugins/openrig-core/hooks/scripts/activity-relay.cjs"), claudeHooksManifestPath: nodePath.resolve(import.meta.dirname, "../assets/plugins/openrig-core/hooks/claude.json"), recordProjection: (targetPath: string, content: string) => projectionManifestStore.record({ targetPath, lastHash: hashContent(content), writtenAt: new Date().toISOString() }) });
|
||||
const claudeAdapter = new ClaudeCodeAdapter({ tmux: tmuxAdapter, seatLaunchEnvironment, claudeManagedLaunch, restoreReadPolicy, fsOps: { readFile: (p: string) => fs.readFileSync(p, "utf-8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c, "utf-8"), exists: (p: string) => fs.existsSync(p), mkdirp: (p: string) => fs.mkdirSync(p, { recursive: true }), copyFile: replaceFile, listFiles: (dir: string) => { const r: string[] = []; /** Collect relative file paths recursively. */ function w(d: string, pre: string) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { if (e.isDirectory()) w(nodePath.join(d, e.name), nodePath.join(pre, e.name)); else r.push(pre ? nodePath.join(pre, e.name) : e.name); } } w(dir, ""); return r; }, readdir: (dir: string) => fs.readdirSync(dir), statMode: (p: string) => fs.statSync(p).mode, chmod: (p: string, m: number) => fs.chmodSync(p, m), homedir: os.homedir() }, stateDir: OPENRIG_HOME, collectorAssetPath: nodePath.resolve(import.meta.dirname, "../assets/claude-statusline-context.cjs"), autoDriveProviderPrompts: runtimeSettings.recoveryAutoDriveProviderPrompts, activityRelayPath: nodePath.resolve(import.meta.dirname, "../assets/plugins/openrig-core/hooks/scripts/activity-relay.cjs"), claudeHooksManifestPath: nodePath.resolve(import.meta.dirname, "../assets/plugins/openrig-core/hooks/claude.json"), recordProjection: (targetPath: string, content: string) => projectionManifestStore.record({ targetPath, lastHash: hashContent(content), writtenAt: new Date().toISOString() }) });
|
||||
const codexAdapter = new CodexRuntimeAdapter({ prepareTeamWorkspace, prepareSeatFolder, tmux: tmuxAdapter, seatLaunchEnvironment, fsOps: { readFile: (p: string) => fs.readFileSync(p, "utf-8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c, "utf-8"), exists: (p: string) => fs.existsSync(p), mkdirp: (p: string) => fs.mkdirSync(p, { recursive: true }), listFiles: (dir: string) => { const r: string[] = []; /** Collect relative file paths recursively. */ function w(d: string, pre: string) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { if (e.isDirectory()) w(nodePath.join(d, e.name), nodePath.join(pre, e.name)); else r.push(pre ? nodePath.join(pre, e.name) : e.name); } } w(dir, ""); return r; }, statMode: (p: string) => fs.statSync(p).mode, chmod: (p: string, m: number) => fs.chmodSync(p, m), homedir: daemonHome }, codexHome: configuredCodexHome, seatHome: (nodeId: string) => seatRuntimeHome(db, nodeId, "codex").home, launchPath: process.env.PATH, detectDaemonSupport: codexDaemonSupportProbe(process.env.PATH, undefined, configuredCodexHome), readNetworkDefault: readCodexNetworkDefault, activityRelayPath: nodePath.resolve(import.meta.dirname, "../assets/plugins/openrig-core/hooks/scripts/activity-relay.cjs"), resolveCorePluginDir: () => vendoredCorePluginDir(), resolveCorePluginSelected: (nodeId: string) => Boolean(seatCodexCorePluginDir({ id: nodeId, runtime: "codex" }, db)), resolveHooksEnabled: async () => {
|
||||
const { SettingsStore } = await import("./domain/user-settings/settings-store.js");
|
||||
return new SettingsStore().resolveOne("runtime.codex.hooks_enabled").value as boolean;
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
// #86 — Claude's native installer names its process, and so tmux's pane label, after its versioned file (2.1.300).
|
||||
// An unmanaged seat whose label is a version reads as `claude` only when the OS executable path witnesses native
|
||||
// Claude. Every case runs through existing seams, so the same file runs unchanged on main: the witnessed cases fail
|
||||
// there (timeout, no verified resume, reconcile drift) and the refusals already hold.
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { ClaudeCodeAdapter, type ClaudeAdapterFsOps } from "../src/adapters/claude-code-adapter.js";
|
||||
import { ClaudeResumeAdapter } from "../src/adapters/claude-resume.js";
|
||||
import type { TmuxAdapter } from "../src/adapters/tmux.js";
|
||||
import type { NodeBinding } from "../src/domain/runtime-adapter.js";
|
||||
import type { NativeProcessLister, NativeProcessRow } from "../src/domain/native-process-lineage.js";
|
||||
import type { ClaudeManagedLaunch } from "../src/domain/claude-managed-launch.js";
|
||||
import { ClaimService } from "../src/domain/claim-service.js";
|
||||
import { createFullTestDb, createTestApp } from "./helpers/test-app.js";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createDb } from "../src/db/connection.js";
|
||||
import { migrate } from "../src/db/migrate.js";
|
||||
import { ALL_MIGRATIONS } from "../src/db/all-migrations.js";
|
||||
import { RigRepository } from "../src/domain/rig-repository.js";
|
||||
import { SessionRegistry } from "../src/domain/session-registry.js";
|
||||
import { NativePermissionStore } from "../src/domain/native-permission-store.js";
|
||||
import { ContextUsageStore } from "../src/domain/context-usage-store.js";
|
||||
import { ContextMonitor } from "../src/domain/context-monitor.js";
|
||||
|
||||
const VERSION = "2.1.300";
|
||||
const NATIVE_PATH = `/fixture/.local/share/claude/versions/${VERSION}`;
|
||||
const startedAt = "Fri Oct 16 01:00:00 2026";
|
||||
|
||||
// The staging subshell exec'd Claude, so Claude leads the pane's foreground group under the pane shell.
|
||||
// Also used as a process lister, which may be handed list options: only a string names a command.
|
||||
function rows(command?: unknown): NativeProcessRow[] {
|
||||
if (typeof command !== "string") command = "claude --permission-mode acceptEdits --resume tok --name seat";
|
||||
return [
|
||||
{ pid: 10, ppid: 1, pgid: 10, tpgid: 12, executableName: "zsh", command: "-zsh", startedAt },
|
||||
{ pid: 12, ppid: 10, pgid: 12, tpgid: 12, executableName: VERSION, executablePath: NATIVE_PATH, command: command as string, startedAt },
|
||||
];
|
||||
}
|
||||
|
||||
function mockTmux(label: string, panePid: () => Promise<number | null> = async () => 10): TmuxAdapter {
|
||||
return {
|
||||
sendText: vi.fn(async () => ({ ok: true as const })),
|
||||
sendKeys: vi.fn(async () => ({ ok: true as const })),
|
||||
sendShellCommand: vi.fn(async () => ({ ok: true as const })),
|
||||
hasSession: vi.fn(async () => true),
|
||||
getPaneCommand: vi.fn(async () => label),
|
||||
// Claude hasn't drawn its prompt yet: no screen text can prove the resume.
|
||||
capturePaneContent: vi.fn(async () => ""),
|
||||
getPanePid: vi.fn(panePid),
|
||||
listPanes: vi.fn(async () => []),
|
||||
createSession: vi.fn(async () => ({ ok: true as const })),
|
||||
killSession: vi.fn(async () => ({ ok: true as const })),
|
||||
listSessions: vi.fn(async () => []),
|
||||
listWindows: vi.fn(async () => []),
|
||||
startPipePane: vi.fn(async () => ({ ok: true as const })),
|
||||
stopPipePane: vi.fn(async () => ({ ok: true as const })),
|
||||
setSessionOption: vi.fn(async () => ({ ok: true as const })),
|
||||
getSessionOption: vi.fn(async () => null),
|
||||
} as unknown as TmuxAdapter;
|
||||
}
|
||||
|
||||
const fsOps = {
|
||||
exists: () => false, readFile: () => "", writeFile: () => {}, mkdirp: () => {}, copyFile: () => {}, listFiles: () => [],
|
||||
} as unknown as ClaudeAdapterFsOps;
|
||||
|
||||
function binding(extra: Partial<NodeBinding> = {}): NodeBinding {
|
||||
return { id: "b1", nodeId: "n1", tmuxSession: "dev-impl@rig", tmuxWindow: null, tmuxPane: null,
|
||||
cmuxWorkspace: null, cmuxSurface: null, updatedAt: "", cwd: "/project", ...extra };
|
||||
}
|
||||
|
||||
// The seat's persisted managed permission mode; by default the seat has no managed selection.
|
||||
type PermissionModeReader = (nodeId: string) => string | undefined;
|
||||
const adapter = (tmux: TmuxAdapter, listProcesses: () => NativeProcessRow[] | Promise<NativeProcessRow[]>,
|
||||
resolvePermissionMode: PermissionModeReader | null = () => undefined) =>
|
||||
new ClaudeCodeAdapter({ tmux, fsOps, listProcesses, sleep: async () => {}, ...(resolvePermissionMode ? { resolvePermissionMode } : {}) });
|
||||
|
||||
describe("#86 unmanaged launch wait", () => {
|
||||
it.each([
|
||||
["OpenRig's own resume command", rows()],
|
||||
["a flag from the user's own claude, which exact proof can't parse", rows("claude --strict-mcp-config --permission-mode acceptEdits --resume tok --name seat")],
|
||||
])("resumes a witnessed native version label: %s", async (_name, processRows) => {
|
||||
const result = await adapter(mockTmux(VERSION), async () => processRows)
|
||||
.launchHarness(binding(), { name: "seat", resumeToken: "tok" });
|
||||
expect(result).toMatchObject({ ok: true, resumeToken: "tok" });
|
||||
});
|
||||
|
||||
it("checkReady reads a witnessed native version label as Claude", async () => {
|
||||
expect(await adapter(mockTmux(VERSION), rows).checkReady(binding())).toEqual({ ready: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("#86 refusals that stay refused", () => {
|
||||
const awaiting = { ready: false, code: "awaiting_runtime" };
|
||||
const cases: [string, NativeProcessRow[] | (() => NativeProcessRow[]), string?, (() => Promise<number | null>)?][] = [
|
||||
["a path outside the native layout", rows().map(r => r.pid === 12 ? { ...r, executablePath: `/opt/other/${VERSION}` } : r)],
|
||||
["a basename that isn't the OS name", rows().map(r => r.pid === 12 ? { ...r, executablePath: NATIVE_PATH.replace("300", "299") } : r)],
|
||||
["argv0 that isn't claude", rows("node /fixture/cli.js --resume tok")],
|
||||
["a version-only argv0", rows(`${VERSION} --resume tok`)],
|
||||
["two candidates", [...rows(), { ...rows()[1]!, pid: 13 }]],
|
||||
// listNativeProcesses leaves the path unset when the OS read fails or times out.
|
||||
["a path read that failed or timed out", rows().map(r => ({ ...r, executablePath: undefined }))],
|
||||
["argv0 naming the native path, with no OS path read", rows(`${NATIVE_PATH} --resume tok`).map(r => ({ ...r, executablePath: undefined }))],
|
||||
["argv0 naming the native path, with an unrelated OS path", rows(`${NATIVE_PATH} --resume tok`).map(r => r.pid === 12 ? { ...r, executablePath: "/opt/other/claude" } : r)],
|
||||
["a background process", rows().map(r => r.pid === 12 ? { ...r, pgid: 88 } : r)],
|
||||
["a label naming a different version", rows(), "2.1.301"],
|
||||
["a pane pid change after the samples", rows(), VERSION, vi.fn<() => Promise<number | null>>()
|
||||
.mockResolvedValueOnce(10).mockResolvedValueOnce(10).mockResolvedValue(11)],
|
||||
];
|
||||
it.each(cases)("%s", async (_name, processRows, label = VERSION, panePid) => {
|
||||
const list = typeof processRows === "function" ? processRows : () => processRows;
|
||||
expect(await adapter(mockTmux(label, panePid), list).checkReady(binding())).toMatchObject(awaiting);
|
||||
});
|
||||
|
||||
it("a process that changes between the two samples", async () => {
|
||||
const list = vi.fn<() => NativeProcessRow[]>().mockReturnValueOnce(rows())
|
||||
.mockReturnValue(rows().map(r => r.pid === 12 ? { ...r, startedAt: startedAt.replace("01:00:00", "01:00:01") } : r));
|
||||
expect(await adapter(mockTmux(VERSION), list).checkReady(binding())).toMatchObject(awaiting);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#86 results that don't change", () => {
|
||||
it("a managed seat keeps its exact proof; the witness isn't consulted", async () => {
|
||||
const list = vi.fn(rows);
|
||||
expect(await adapter(mockTmux(VERSION), list).checkReady(binding({ permissionMode: "auto" })))
|
||||
.toMatchObject({ ready: false, code: "awaiting_runtime" });
|
||||
expect(list).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each<[string, PermissionModeReader | null]>([
|
||||
["a persisted managed selection, through a binding without permissionMode", () => "auto"],
|
||||
["an unreadable selection", () => { throw new Error("Invalid persisted native permission selection; launch refused."); }],
|
||||
["no selection reader", null],
|
||||
])("the witness isn't consulted with %s", async (_name, resolvePermissionMode) => {
|
||||
const list = vi.fn(rows);
|
||||
expect(await adapter(mockTmux(VERSION), list, resolvePermissionMode).checkReady(binding()))
|
||||
.toMatchObject({ ready: false, code: "awaiting_runtime" });
|
||||
expect(list).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("a shell label over an idle shell stays a return to the shell", async () => {
|
||||
// Claude has exited: the pane's shell leads the foreground group again, with nothing else in it.
|
||||
const shellAlone = () => [{ ...rows()[0], tpgid: 10 }];
|
||||
expect(await adapter(mockTmux("zsh"), shellAlone).checkReady(binding())).toMatchObject({ ready: false, code: "returned_to_shell" });
|
||||
});
|
||||
|
||||
it("a claude label stays ready by name, with no process read", async () => {
|
||||
const list = vi.fn(rows);
|
||||
expect(await adapter(mockTmux("claude"), list).checkReady(binding())).toEqual({ ready: true });
|
||||
expect(list).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#86 legacy restore resume", () => {
|
||||
const resumeAdapter = (tmux: TmuxAdapter, listProcesses: () => NativeProcessRow[], extra = {}) =>
|
||||
new ClaudeResumeAdapter(tmux, { listProcesses, pollMs: 1, maxWaitMs: 2, sleep: async () => {}, ...extra });
|
||||
|
||||
it("an unmanaged resume with a witnessed native version label is verified", async () => {
|
||||
expect(await resumeAdapter(mockTmux(VERSION), rows).resume("dev-impl@rig", "claude_id", "tok", "/project"))
|
||||
.toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it("an unwitnessed version label stays unverified", async () => {
|
||||
const list = () => rows().map(r => ({ ...r, executablePath: undefined }));
|
||||
expect(await resumeAdapter(mockTmux(VERSION), list).resume("dev-impl@rig", "claude_id", "tok", "/project"))
|
||||
.toMatchObject({ ok: false, code: "attention_required" });
|
||||
});
|
||||
|
||||
it("a managed resume keeps its exact proof; the witness isn't consulted", async () => {
|
||||
const list = vi.fn(rows);
|
||||
const claudeManagedLaunch = { prepare: async () => ({ command: (args: readonly string[]) => `claude ${args.join(" ")}`,
|
||||
assertCurrent: () => {}, configDir: "/fixture", executable: NATIVE_PATH }) } as unknown as ClaudeManagedLaunch;
|
||||
const result = await resumeAdapter(mockTmux(VERSION), list, { claudeManagedLaunch })
|
||||
.resume("dev-impl@rig", "claude_id", "tok", "/project", undefined, undefined, "auto", "n1");
|
||||
expect(result).toMatchObject({ ok: false, code: "attention_required" });
|
||||
expect(list).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#86 reconcile-session drift", () => {
|
||||
async function reconcile(listProcesses: () => NativeProcessRow[]) {
|
||||
const db = createFullTestDb();
|
||||
const tmux = mockTmux(VERSION);
|
||||
const setup = createTestApp(db, { tmux });
|
||||
const deps = { db, rigRepo: setup.rigRepo, sessionRegistry: setup.sessionRegistry, discoveryRepo: setup.discoveryRepo,
|
||||
eventBus: setup.eventBus, tmuxAdapter: tmux, listProcesses };
|
||||
const claims = new ClaimService(deps as ConstructorParameters<typeof ClaimService>[0]);
|
||||
const rig = setup.rigRepo.createRig("drift-rig");
|
||||
const node = setup.rigRepo.addNode(rig.id, "dev.impl", { runtime: "claude-code", cwd: "/work/repo", podId: null });
|
||||
setup.sessionRegistry.markDetached(setup.sessionRegistry.registerSession(node.id, "dev-impl@drift-rig").id);
|
||||
setup.sessionRegistry.updateBinding(node.id, { tmuxSession: "dev-impl@drift-rig" });
|
||||
const result = await claims.reconcileSession({ sessionName: "dev-impl@drift-rig" });
|
||||
db.close();
|
||||
if (!result.ok) throw new Error(result.message);
|
||||
return result.result;
|
||||
}
|
||||
|
||||
it("a witnessed native version label confirms the runtime; continuity stays unverified", async () => {
|
||||
const result = await reconcile(rows);
|
||||
expect(result.projectionDrift.join(" ")).not.toContain("runtime unverified");
|
||||
expect(result.continuity).toBe("unverified");
|
||||
});
|
||||
|
||||
it("an unwitnessed version label keeps the drift line", async () => {
|
||||
const result = await reconcile(() => rows().map(r => r.pid === 12 ? { ...r, executablePath: `/opt/other/${VERSION}` } : r));
|
||||
expect(result.projectionDrift.join(" ")).toContain(`runtime unverified: pane command "${VERSION}" does not confirm runtime "claude-code"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#86 recovery readiness (ContextMonitor.pollOnce) follows the seat's persisted selection", () => {
|
||||
async function poll(opts: { selection?: string; label?: string; rows: () => NativeProcessRow[] }): Promise<string> {
|
||||
const db = createDb();
|
||||
migrate(db, ALL_MIGRATIONS);
|
||||
const rigRepo = new RigRepository(db);
|
||||
const sessions = new SessionRegistry(db);
|
||||
const node = rigRepo.addNode(rigRepo.createRig("recovery-rig").id, "dev.impl", { runtime: "claude-code", cwd: "/project" });
|
||||
const session = sessions.registerSession(node.id, "dev-impl@recovery-rig");
|
||||
db.prepare("UPDATE sessions SET status = 'running', startup_status = 'attention_required' WHERE id = ?").run(session.id);
|
||||
const permissions = new NativePermissionStore(db);
|
||||
if (opts.selection) permissions.write(node.id, { runtime: "claude-code", mode: opts.selection }, "test", "managed seat");
|
||||
const stateDir = mkdtempSync(join(tmpdir(), "label-witness-"));
|
||||
const claude = new ClaudeCodeAdapter({ tmux: mockTmux(opts.label ?? VERSION), fsOps, listProcesses: opts.rows, sleep: async () => {},
|
||||
resolvePermissionMode: (nodeId) => permissions.resolve(nodeId, "claude-code").permissionMode });
|
||||
const monitor = new ContextMonitor(db, new ContextUsageStore(db, { stateDir, codexHomeDir: stateDir }),
|
||||
{ ensureContextCollector: () => {}, checkReady: (b) => claude.checkReady(b) });
|
||||
try {
|
||||
await monitor.pollOnce();
|
||||
return (db.prepare("SELECT startup_status FROM sessions WHERE id = ?").get(session.id) as { startup_status: string }).startup_status;
|
||||
} finally {
|
||||
monitor.stop();
|
||||
db.close();
|
||||
rmSync(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
it.each<[string, Parameters<typeof poll>[0], string]>([
|
||||
["managed auto, a version label, the wrong conversation", { selection: "auto", rows: () => rows("claude --permission-mode auto --resume other --name seat") }, "attention_required"],
|
||||
["managed auto, a version label, no OS path", { selection: "auto", rows: () => rows().map(r => ({ ...r, executablePath: undefined })) }, "attention_required"],
|
||||
["managed auto, the ordinary claude label, as on main", { selection: "auto", label: "claude", rows }, "ready"],
|
||||
["unmanaged, a witnessed version label", { rows }, "ready"],
|
||||
])("%s", async (_name, opts, expected) => {
|
||||
expect(await poll(opts)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#86 an actual path-read timeout ends the witness for the rest of one wait", () => {
|
||||
const unwitnessed = () => rows().map(r => ({ ...r, executablePath: undefined }));
|
||||
// As listNativeProcesses does when the OS read times out: the path stays unset and the caller is told.
|
||||
const timedOut: NativeProcessLister = (opts) => { opts?.onPathReadTimeout?.(); return unwitnessed(); };
|
||||
const launch = (claude: ClaudeCodeAdapter) => claude.launchHarness(binding(), { name: "seat", resumeToken: "tok" });
|
||||
// The witness's own reads carry its timeout listener; main's process checks call the same lister without one.
|
||||
const witnessReads = (list: ReturnType<typeof vi.fn>) =>
|
||||
list.mock.calls.filter(([opts]) => typeof opts === "object" && opts !== null && "onPathReadTimeout" in opts).length;
|
||||
// Main's result for the same process evidence: a managed seat never consults the witness.
|
||||
const mainResult = () => launch(adapter(mockTmux(VERSION), vi.fn(unwitnessed), () => "auto"));
|
||||
|
||||
it.each<[string, () => NativeProcessLister, number]>([
|
||||
["a first-sample timeout", () => vi.fn(timedOut), 1],
|
||||
["a second-sample timeout", () => vi.fn<NativeProcessLister>().mockImplementationOnce(() => rows()).mockImplementation(timedOut), 2],
|
||||
])("the launch wait stops reading after %s and ends with main's result", async (_name, makeList, reads) => {
|
||||
const list = makeList();
|
||||
const result = await launch(adapter(mockTmux(VERSION), list));
|
||||
expect(result).toEqual(await mainResult());
|
||||
expect(result.ok).toBe(false);
|
||||
expect(witnessReads(list as ReturnType<typeof vi.fn>)).toBe(reads);
|
||||
});
|
||||
|
||||
it("a missing path without a timeout isn't one: every observation still tries", async () => {
|
||||
const list = vi.fn(unwitnessed);
|
||||
const tmux = mockTmux(VERSION);
|
||||
expect(await launch(adapter(tmux, list))).toMatchObject({ ok: false });
|
||||
const observations = vi.mocked(tmux.capturePaneContent).mock.calls.length;
|
||||
expect(observations).toBeGreaterThan(16);
|
||||
expect(witnessReads(list)).toBe(observations);
|
||||
});
|
||||
|
||||
it("a later, independent launch wait uses a healthy witness", async () => {
|
||||
let read: NativeProcessLister = timedOut;
|
||||
const claude = adapter(mockTmux(VERSION), (opts) => read(opts));
|
||||
expect(await launch(claude)).toMatchObject({ ok: false });
|
||||
read = () => rows();
|
||||
expect(await launch(claude)).toMatchObject({ ok: true, resumeToken: "tok" });
|
||||
});
|
||||
|
||||
it("the legacy restore wait reads once after a timeout, and its next wait uses a healthy witness", async () => {
|
||||
let read: NativeProcessLister = timedOut;
|
||||
const list = vi.fn<NativeProcessLister>((opts) => read(opts));
|
||||
const resume = new ClaudeResumeAdapter(mockTmux(VERSION), { listProcesses: list, sleep: async () => {} });
|
||||
expect(await resume.resume("dev-impl@rig", "claude_id", "tok", "/project")).toMatchObject({ ok: false, code: "attention_required" });
|
||||
expect(list).toHaveBeenCalledTimes(1);
|
||||
read = () => rows();
|
||||
expect(await resume.resume("dev-impl@rig", "claude_id", "tok", "/project")).toMatchObject({ ok: true });
|
||||
});
|
||||
});
|
||||
@@ -34,24 +34,6 @@ describe("OS executable path witness", () => {
|
||||
else execute.mockResolvedValue({ stdout: mode === "malformed" ? "not json" : JSON.stringify([[12, "relative"], [13, "/a/../b"], [14, "/a\0b"], [999, "/a"]]) });
|
||||
expect((await readNativeExecutablePaths([12, 13, 14])).size).toBe(0);
|
||||
});
|
||||
it("tells a caller only about an actual read timeout (#86)", async () => {
|
||||
setPlatform("darwin");
|
||||
const onTimeout = vi.fn();
|
||||
// Node's own timeout kill: killed by Node with the kill signal.
|
||||
execute.mockRejectedValueOnce(Object.assign(new Error("Command failed"), { killed: true, signal: "SIGTERM", code: null }));
|
||||
expect((await readNativeExecutablePaths([12], { onTimeout })).size).toBe(0);
|
||||
expect(onTimeout).toHaveBeenCalledOnce();
|
||||
for (const error of [
|
||||
Object.assign(new Error("denied"), { killed: false, signal: null, code: 1 }),
|
||||
Object.assign(new Error("too much output"), { code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" }),
|
||||
]) {
|
||||
execute.mockRejectedValueOnce(error);
|
||||
expect((await readNativeExecutablePaths([12], { onTimeout })).size).toBe(0);
|
||||
}
|
||||
execute.mockResolvedValueOnce({ stdout: "not json" });
|
||||
expect((await readNativeExecutablePaths([12], { onTimeout })).size).toBe(0);
|
||||
expect(onTimeout).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("does no work for an empty request or unsupported platform, and caps batch size", async () => {
|
||||
setPlatform("darwin");
|
||||
expect((await readNativeExecutablePaths([])).size).toBe(0);
|
||||
|
||||
@@ -53,12 +53,4 @@ describe.each(Object.entries(SOURCES))("from %s", (source, serve) => {
|
||||
expect(paths).toHaveBeenCalledExactlyOnceWith([20, 21]);
|
||||
expect(rows.find(r => r.pid === 20)?.executablePath).toBe("/nix/store/x-claude-code-2.1.280/bin/.claude-unwrapped");
|
||||
});
|
||||
it("forwards a caller's path-read timeout listener only when one is given (#86)", async () => {
|
||||
serve([row(15, "2.1.289", "claude --resume token")]);
|
||||
const onPathReadTimeout = vi.fn();
|
||||
paths.mockImplementation(async (_pids: number[], opts?: { onTimeout?: () => void }) => { opts?.onTimeout?.(); return new Map(); });
|
||||
await listNativeProcesses({ onPathReadTimeout });
|
||||
expect(paths).toHaveBeenCalledExactlyOnceWith([15], { onTimeout: onPathReadTimeout });
|
||||
expect(onPathReadTimeout).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/ui",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
Reference in new issue
Block a user