Bundle create read every vendored agent-package file, and the declared
culture, docs and startup files, as UTF-8 text and wrote it back as UTF-8.
Any byte sequence that is not valid UTF-8 became U+FFFD, and the integrity
manifest was hashed from the changed copy, so inspect and install verified
the corrupted bytes as intact.
Copy those files as bytes: the pod assembler reads them with readFileBuffer
and writes the same bytes, and the install-side skills and workflow-spec
routers copy with fs.copyFileSync. Rewriting rig.yaml and agent.yaml import
refs stays a text operation.
Refs #245
* fix(daemon): keep a blocked row's park timer through a seat handover
At a seat swap, the occupant invalidator stops every watchdog job the retiring
generation registered. A park timer (rig queue block --wake-after, with or without
--wake-max) is registered by the parking seat, so a handover stopped it too. Nothing
re-armed it, and the row stayed blocked with no timer, which is the strand reported
in korallis/agent-stack#61.
The rig's parked-owner consumer eventually noticed the dead wake and sent one generic
wake once the new occupant was idle. The recorded wake time, message and backoff
cadence were still lost.
A blocked row's current queue-generated timer wakes the seat that owns the row, and
the successor inherits that row, so the swap now keeps it. The queue lists those
timers: each is still the row's current armed wake, is still active, and still
targets the row's destination. The watchdog drop leaves them running.
Every other job the retiring generation registered still stops: operator-attached
watchdogs, superseded or stale timers, timers of rows rerouted to another seat, and
occupant-only jobs. Re-parking still retires the kept timer, so no duplicate is
created. A failed handover never reaches the invalidator, as before.
* fix(daemon): retire a park timer when its row is rerouted
routeToFallback moved a blocked row to a new owner but left the row's park timer
active and aimed at the old owner, so it could fire to the old owner. The
pre-delivery check only refuses timers whose rows are all terminal. That was already
reachable on main (park, reroute, fire), and keeping park timers through a handover
also let it happen after a swap (review-r2).
The reroute now retires the row's current park-generated timer through the existing
retireParkGeneratedTimer path, reason park_rerouted, the same one re-park and exit
use. The new owner has no dedicated wake from a reroute, as before; the retired timer
leaves the row's wake dead, so the parked-owner backstop covers it.
* fix(daemon): leave a repeating queue wait running when its row is rerouted
The previous commit retired every park timer on reroute. A repeating queue wait
already delivers to the row's current owner (evaluateQueueWait sends to view.owner),
so it woke the new owner correctly, and retiring it took that wake away. Only a
periodic park timer is aimed at the old owner, so only that is retired now.
Tests now fire through the engine with resolveQueueWait wired, as startup.ts wires
it. After a periodic reroute the row shows no live park timer, and the new owner's
parked diagnosis sees it as unhealthy, so the backstop covers the new owner. A
repeating wait survives the reroute, with or without swaps, and delivers only to the
new owner.
---------
Co-authored-by: OpenRig contributors <[email protected]>
createHerdrSocketRpc decoded each socket chunk on its own, so a
multi-byte character split across two chunks became U+FFFD while the
JSON line still parsed. Setting the socket encoding to UTF-8 uses Node's
streaming decoder, which carries an incomplete character into the next
chunk. Newline framing, id matching and error, end and timeout handling
are unchanged.
The new test drives the real RPC over a private unix socket and splits
the reply inside 2-, 3- and 4-byte characters, plus a case where
another id's line arrives first.
Co-authored-by: OpenRig contributors <[email protected]>
Both native process listers run `ps` with an `lstart` column and parse it with
an English date pattern. `ps` formats that column through the locale, so a
daemon that inherited a non-English locale (for example LANG=de_DE.UTF-8,
LC_TIME=fr_FR.UTF-8 or LC_ALL=ja_JP.UTF-8) saw every row fail the pattern:
process detection and resume metadata silently came back empty.
Each `ps` call now runs with a child-only `LC_ALL=C`, spread over the existing
environment. The parent environment, the column sets, the parsers and every
identity requirement are unchanged.
On macOS, the C locale also makes `ps` print non-ASCII bytes in the command
column as escapes (for example `\240`) instead of raw UTF-8. Token boundaries
and every ASCII identity token (executable name, session and resume ids) are
unchanged.
Claude-Session: https://claude.ai/code/session_01V8RzdrMhGkYLV4rn96kNMN
Co-authored-by: v-openrig-build <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
listNativeProcesses reads `ps -Ao pid,ppid,pgid,tpgid,ucomm,lstart,command`
and matched ucomm with \S+. ucomm can contain spaces on both platforms:
macOS app helpers ("Slack Helper", "QuickTime Player"), and on Linux task
names that prctl(PR_SET_NAME) and process.title rewrite ("tmux: server",
"node (vitest 1)"). Every such row was silently dropped from the process
table, which is why the real-ps tests could not find their own (renamed)
vitest worker on Linux. lstart always begins with a weekday word and runs to
the year, and ucomm (16 bytes at most) is too short to contain such a date,
so matching ucomm lazily up to the first date is exact.
Co-authored-by: Martin <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* refactor: argv-exec seam in TmuxAdapter (Windows/psmux path)
Every multiplexer invocation is now built as an argv array first.
A new optional ArgvExecFn dep runs it WITHOUT a shell
(execFile-style: argv[0] is the binary, rest verbatim, no quoting
layer) — the Windows/psmux path, where POSIX shells, single-quote
quoting and /dev/null redirections do not exist.
Default behavior is byte-identical: without argvExec every method
renders the exact legacy shell string (the existing suite pins all
of them). startServer (-D detach/redirection) and signalPaneProcess
(Unix kill, not a mux command) stay on the string path deliberately.
New tmux-argv-exec suite pins exact argv per op; live smoke
(/tmp, throwaway) drove real tmux 3.5a 7/7 through execFile with
the string path rigged to throw.
* Wire shell-free tmux execution without changing runtime adapters
---------
Co-authored-by: MTG-Thomas <[email protected]>
readProcessState shelled out to `ps -o state= -p <pid>` to reject zombies.
Windows has no ps and no zombie state, so isProcessAlive returned false for a
live daemon child and startup failed with "liveness could not be confirmed".
On win32 the process.kill(pid, 0) probe now decides on its own. POSIX behavior
is unchanged.
Refs #172
Co-authored-by: Claude Sonnet 5.5 <[email protected]>
* test(cli): skip release-surface tag diffs when v0.3.1/v0.3.2 are absent
The worked-example (AC-2) block called generateSurfaceDiff in the describe
body, so in a fork or a shallow/--no-tags clone the throw during collection
failed the whole file, including the six unit tests that need no tags.
Check for the two tags up front and skip only the AC-2 and AC-4 blocks when
they are missing, with a hint to fetch tags. Move the AC-2 diff into
beforeAll so a skipped block does no git work. Under CI (which clones with
full history) a missing tag still fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* test(cli): only treat CI=true as CI in the release-surface tag guard
A truthiness check read CI=false as CI, so a tagless local run with
CI=false failed instead of skipping. Match scripts/ci-macos-process-probe.mjs
and GitHub Actions, which set CI=true.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Coder8124 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
stripCodexActivityHooks applied a global /\n{3,}/ collapse (and a
leading-newline trim) to the entire ~/.codex/config.toml document, not
just to the excision seam. Disabling the managed activity-hooks block
silently rewrote unrelated user content, including blank lines inside
TOML multiline strings, changing parsed values in a file the function
documents as touched only at the managed block.
The \n* prefix in the excision pattern already absorbs the blank-line
separator the block was appended with, so replacing the match with a
single newline restores the seam exactly. Drop the document-wide
collapse and the leading trim, and add regression tests that pin
byte-exact preservation of newline runs between user sections and
blank lines inside multiline strings.
Co-authored-by: hobostay <[email protected]>
projectEntry probes whether a source is directory- or file-shaped with
listFiles(entry.absolutePath). In production, startup.ts wires listFiles
to a recursive fs.readdirSync walk, which throws ENOTDIR for file-shaped
entries such as subagents (.claude/agents/<name>.yaml). The throw was
caught by project()'s per-entry handler, so every file-shaped entry
silently landed in ProjectionResult.failed and was never installed; the
single-file copy branch was unreachable outside tests. Unit tests missed
this because their mock listFiles returns [] for file paths instead of
throwing, diverging from the production wiring.
Treat a listFiles throw as not-a-directory in all four runtime adapters
(claude, codex, pi, stub) so file-shaped entries fall through to the
existing file copy branch, and add regression tests that drive a
file-shaped subagent through project() with readdirSync-backed fsOps.
Co-authored-by: hobostay <[email protected]>
* fix(daemon): rig remove never kills another rig's live seat; seat refs skip archived rigs (#174)
An archived duplicate of a live rig shares its name and its seats'
canonical session names.
- `rig remove <archived-rigId> <node>` killed the tmux session by name,
so it could end the live rig's seat. Removal now asks whether another
node owns that session name, using the handover's live-ownership check,
now shared as session-owner.ts. If another node owns it, removal
leaves the session running, doesn't block on or reroute the queue
work addressed to it, and reports the owner as `sessionKeptFor`. The
node itself is still removed.
- Seat refs (`member@rig`) for seat launch, stop, model and handover
resolved rigs by name including archived ones, so they failed with
"matched multiple nodes". They now resolve unarchived rigs only, as
default reads do since migration 042. Other findRigsByName callers
are unchanged.
* fix(daemon): archived rigs don't claim a live seat's session or input target (#174)
Review follow-up. Archiving a rig keeps its bindings, so the archived twin
still names the live seat's session:
- Removing the live node found the archived twin as the "other owner",
kept the session and removed the node's open work. Removal now ignores
owners in archived rigs, so the live node kills its own session and
still refuses open work without --fallback. The handover check is
unchanged.
- The delivery guard needs exactly one match for a target, and the twin's
binding made it two, so delivery to the live seat failed. Unarchived
matches now win, and archived ones count only when nothing else
matches. Exactly one match is still required, so a truly ambiguous
live target still refuses.
- `rig remove` and `rig shrink` human output now says when a session was
kept, and for whom.
* fix(daemon): a YAML import archives the stopped same-name rig it replaces (#141)
After the seats' tmux server died, `rig up <same rig.yaml>` created a
second, unarchived rig with the same name. Every seat name then matched
two nodes, so launches and kills were refused.
When an explicit YAML import (PodRigInstantiator.instantiate) finds an
unarchived rig with the same name, it first confirms that rig is stopped:
- no running session rows (the existing running-name guard); and
- for every session name it used, this daemon's tmux reports positive
absence or "no server running".
It then archives that rig in the same database transaction that creates
the replacement. The earlier rig's records are kept, and the output names
it with `rig unarchive <id>`. If the rig can't be confirmed stopped, the
import creates nothing and says why.
If creating the replacement fails, the service hook refuses it, or every
replacement node fails and the replacement is removed, the earlier rig
is un-archived. An adapter that cannot probe keeps today's behavior.
* fix(daemon): one YAML import per rig name while it may archive a generation (#141)
Review follow-up to the stopped-generation archive.
- Concurrency. The stopped check awaits tmux, and a second same-name
import could run during that wait. It could replace the same old rig
too, or archive the first import's in-progress replacement, which has
no running sessions yet. The YAML import now takes a per-database,
per-name in-flight claim, and a same-name import arriving meanwhile is
refused (generation_unconfirmed). Unrelated names are unaffected. The
create transaction also rechecks that the same-name set is the one it
checked, and that each archive changed its row, so a rollback restores
only archives this import made.
- Responses. generation_unconfirmed is now a 409 conflict with its
message on POST /api/up and POST /api/rigs/import, not a 500. The
archive notice also rides the attention_required outcome, where the
replacement is kept.
* fix(cli): print the #141 archive notice on the attention 409 and the generation_unconfirmed message
rig up returned from its status >= 400 block before either warnings loop, so the
archive notice (old rig id and its unarchive command) on an attention 409 reached
--json only. Print the response warnings after the attention nodes; that path returns
early, so nothing prints twice. Render generation_unconfirmed verbatim like
rig_name_running instead of the validate-your-spec fallback. JSON output and exit
codes are unchanged.
* fix(daemon): archived generations don't block the replacement's handover or pod shrink
A YAML re-import archives the stopped earlier generation, which keeps its bindings
to the session names the live replacement now uses.
Handover: a fresh, fork or rebuild successor reuses the seat's own session name,
so the archived binding made finalize return successor_already_managed after the
successor had replaced the process. The owner check skips archived rigs for
composer-launched successors, as removeNode does. A discovered successor is still
blocked by any other owner.
Pod shrink: shrinkPod counted and, with --fallback, rerouted queue work addressed
to member session names that another unarchived node owns. It now applies
removeNode's ownership rule first, so shrinking the archived pod neither stops on
nor moves the replacement's work.
* fix(daemon): keep a shrink target's own shared-name work in the up-front check
C3's shrinkPod ownership filter excluded only the node being checked, so when two
members of the pod being shrunk shared a session name, each counted the other as its
owner. Their open queue work then dropped out of the up-front check. Without
--fallback the shrink removed one member and failed on the last. With --fallback the
work wasn't rerouted.
findOtherSessionOwner gains an optional excludeNodeIds, applied in both the binding
and the session-row lookups before the first match, and its default is unchanged.
shrinkPod passes the whole target, so only an unarchived owner outside the target
takes a name's work out of the check. Before C3, shrinking such a pod refused up front
or rerouted with --fallback, and that behavior is restored.
---------
Co-authored-by: OpenRig contributors <[email protected]>
* fix: provider-aware Codex readiness for non-OpenAI providers (#194)
`rig setup` and the kernel runtime probe treated `codex login status` as the
only Codex authentication check. A user whose Codex selects a custom provider
with `requires_openai_auth = false` and an `env_key` (the reported Amazon
Bedrock bearer-token provider) failed setup and was never offered a Codex
kernel, even with the provider's credential set.
Both checks now read the provider selected in `$CODEX_HOME/config.toml`. For
an explicit provider entry with `requires_openai_auth = false` and an
`env_key`, a nonblank variable counts as local credential availability; the
kernel probe still requires the Codex executable. Every other case keeps the
OpenAI login check, including a missing file, a parse error, a legacy
top-level `profile`, and built-in or undeclared providers, so an unresolved
config never earns readiness. The setup message says this is a local
credential check, not remote authentication or inference.
`AWS_BEARER_TOKEN_BEDROCK` joins the known provider-auth names, so an operator
who names it in `recovery.provider_auth_env_allowlist` can forward it to
managed seats. Nothing is forwarded without that opt-in.
Other Codex config layers (project, system or managed config, requirements,
`-c`, `--profile`) are not resolved.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01V8RzdrMhGkYLV4rn96kNMN
* docs: narrow the #194 unresolved-config wording
An unresolved Codex config does not take the new env-key shortcut; readiness
still follows the existing login result. The comment and one test title now say
that, instead of implying OpenRig validates every configuration. No behavior
change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01V8RzdrMhGkYLV4rn96kNMN
---------
Co-authored-by: v-openrig-build <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Adds a 'Before a security or integration PR' section to CONTRIBUTING.md, a
security question block to the PR template, and a trust-model bullet to
SECURITY.md's scope notes. Additive only; existing scope text is unchanged.
Co-authored-by: v-openrig-build <[email protected]>
* fix(daemon): rig remove never kills another rig's live seat; seat refs skip archived rigs (#174)
An archived duplicate of a live rig shares its name and its seats'
canonical session names.
- `rig remove <archived-rigId> <node>` killed the tmux session by name,
so it could end the live rig's seat. Removal now asks whether another
node owns that session name, using the handover's live-ownership check,
now shared as session-owner.ts. If another node owns it, removal
leaves the session running, doesn't block on or reroute the queue
work addressed to it, and reports the owner as `sessionKeptFor`. The
node itself is still removed.
- Seat refs (`member@rig`) for seat launch, stop, model and handover
resolved rigs by name including archived ones, so they failed with
"matched multiple nodes". They now resolve unarchived rigs only, as
default reads do since migration 042. Other findRigsByName callers
are unchanged.
* fix(daemon): archived rigs don't claim a live seat's session or input target (#174)
Review follow-up. Archiving a rig keeps its bindings, so the archived twin
still names the live seat's session:
- Removing the live node found the archived twin as the "other owner",
kept the session and removed the node's open work. Removal now ignores
owners in archived rigs, so the live node kills its own session and
still refuses open work without --fallback. The handover check is
unchanged.
- The delivery guard needs exactly one match for a target, and the twin's
binding made it two, so delivery to the live seat failed. Unarchived
matches now win, and archived ones count only when nothing else
matches. Exactly one match is still required, so a truly ambiguous
live target still refuses.
- `rig remove` and `rig shrink` human output now says when a session was
kept, and for whom.
---------
Co-authored-by: OpenRig contributors <[email protected]>
* fix(daemon): a snapshot refresh probe never leaves its tmux session behind (#188)
`rig down` probes Claude resumability in a private `rigged-refresh-*`
tmux session. Two paths left it running, and every retry added another:
- After a seats' tmux server restart, the new server reuses pane ids,
so a stale seat binding could name the probe's brand-new pane. The
probe's kill then asked the guard to resolve the helper by name, which
threw, and the unawaited write let the throw escape: the snapshot
failed ("Cannot establish managed input target rigged-refresh-...")
and the helper was never killed.
- When the new pane could not be proven, the probe returned failure
without removing the session it had just created.
createProbeSession now refuses to use a probe whose new pane is named by
a managed record, or whose pane it cannot prove. It removes that helper
immediately, by the name this call just created. The probe write is
awaited, so a later refusal returns as a result instead of throwing.
Managed or changed targets are still never killed.
* fix(daemon): roll back an unusable probe only by the id its create returned (#188)
Review follow-up. The allocation rollback killed the helper by name after
awaiting creation and pane observation, which could kill a session that a
managed record had adopted meanwhile, or a different session that had taken
the name.
The probe is now created with `new-session -P -F '#{session_id}'`, and the
rollback kills only that session id. It leaves the helper in place, and
says so, when the id is unknown or a managed record now claims the helper's
name. A binding that names only the reused pane id under another session
name is stale and does not block the rollback. A cleanup failure is
reported in the result. The awaited probe write is unchanged.
* fix(daemon): check the helper's identity in the same tmux command that removes it (#188)
Review follow-up. A tmux session id is unique only for one server's
lifetime. If the server restarted between creating the refresh helper and
rolling it back, `kill-session -t $N` could remove an unrelated session
that the new server gave the same id.
The helper's create now returns its id and creation time. The rollback
runs `if-shell -F -t $N` with a check on the session's name and creation
time, and kills only when both match, in the same tmux command. A
mismatch, or an id that no longer resolves, leaves everything in place
and says so in the result.
---------
Co-authored-by: OpenRig contributors <[email protected]>
When a seat's agent runtime has failed or exited, its pane shows a bare
shell, and any text typed there runs as shell commands. The
parked-owner watchdog wake was delivered anyway (`zsh: bad pattern:
[OpenRig`).
The transport now refuses a send when the pane's foreground is a shell
and the seat's runtime is an agent runtime, before anything is written.
It uses the shared shell classifier. A terminal node's shell is its
runtime and still receives text. An unknown or unreadable pane stays
advisory, as before.
The watchdog records the refusal as a failed delivery on the open row
and sends no second wake that episode. A running runtime still gets its
wake.
Co-authored-by: OpenRig contributors <[email protected]>
* fix(bundle): resolve bundle paths on the client and launch v2 installs from --target
`rig bundle create/inspect/install` sent spec, output, bundle and target paths
unresolved, so the daemon resolved them against its own cwd. The CLI now
resolves them against the operator's cwd (files are still read on the daemon's
host; nothing is uploaded).
A schema-version-2 bundle apply launched from the temp extraction dir, which is
removed right after instantiate, and ignored targetRoot: members with cwd "."
and the spec/agent refs pointed at a deleted directory. Apply with a targetRoot
now copies the verified extraction into the target and launches from there.
A target with different content at any bundle path is refused with
target_conflict and nothing written; identical files are accepted so a
reinstall works. Plan mode, explicit cwdOverride, legacy v1 bundles and
applies without a targetRoot are unchanged.
Docs: rig-bundle.md install/inspect/--plan/--target/path behavior.
* fix(cli): resolve local rig up --target and align its help with the v2 target contract
On the local route, `rig up <bundle> --target <rel>` now resolves the
target against the client's cwd before sending it, matching `rig bundle
install`. The --target help now states the accepted contract: a v2 bundle
is materialized into the target (default: current directory), relative
member cwds resolve against it, and --cwd still overrides the launch cwd.
Remote --host and topology routing are unchanged: --host sends --target
as given, and it must exist on that host.
Docs: drop the "pass an absolute --target" workaround for the local route.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: v-openrig-build <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
#107 refuses to report a fresh pod-aware launch as fresh-primed without the
node's startup context and a runtime adapter. Two fixtures still expected a
successful launch without either, and failed on main:
- launch-node-subset: the single-node launch now seeds the target's startup
context and passes a ready claude-code adapter, and asserts the harness
was launched. The non-target assertions are unchanged.
- sessions-routes: the launch-subset success case seeds both nodes' startup
contexts and opts into route runtime adapters, as startup wires them.
createTestApp gains an opt-in wireRuntimeAdapters option. It is off by
default, so every other route test keeps no route adapters. No production
change.
Co-authored-by: OpenRig contributors <[email protected]>
terminal-ws.ts had a local constantTimeEqual that returned early on
length mismatch. The shared helper in auth-bearer-token.ts already
handles unequal lengths with a fixed-cost dummy compare. Replace the
local copy with an import of the shared helper for consistency.
Carries the tests and scope note from #137, which was folded into #135:
- round trip with the stampSelfHostSuffix producer;
- 4-segment names, @local, a near-miss host id, legacy flat names and
virtual refs pass through unchanged;
- the origin-unknown marker and the P18 wire-over-body rule hold after
the strip, for both sender helpers.
The comment records that only the sender is canonicalized: the CLI
cannot fix this at the source, and self-suffixed destinations keep the
C4 refusal. No behavior change.
Co-authored-by: OpenRig contributors <[email protected]>
Co-authored-by: Yi-111-a <[email protected]>
Claude Code seats can already receive ANTHROPIC_API_KEY and
ANTHROPIC_AUTH_TOKEN through recovery.provider_auth_env_allowlist, and
Codex seats can receive OPENAI_BASE_URL, but ANTHROPIC_BASE_URL was not a
known name and was dropped silently. A key issued by an
Anthropic-compatible endpoint (a local Ollama server, a hosted gateway)
then reached the seat without its endpoint, so Claude Code sent it to
api.anthropic.com.
Refs #140
Signed-off-by: FenjuFu <[email protected]>
* fix(daemon,ui): fix timezone flake, macOS realpath in test, and dumb term runner editing
- packages/ui/test/artifacts-navigator.test.tsx: use noon UTC for fixture mtime to ensure consistent local date formatting across all timezones
- packages/daemon/test/s03-bound-launch.test.ts: canonicalize mkdtempSync root via realpathSync to match macOS /private/var path resolution
- packages/daemon/src/adapters/pi-runner.ts: preserve VT100 line editing in createRunnerInput when TERM=dumb by deleting dumb-mode override
* test(daemon,ui): add focused TERM=dumb runner test and compute local mtime dynamically
- packages/daemon/test/pi-runner.test.ts: add focused unit test asserting line editing (type, backspace, retype) under TERM=dumb while verifying normal terminal and piped input still behave as before
- packages/ui/test/artifacts-navigator.test.tsx: compute expected date dynamically from fixture mtime in local time, ensuring 100% timezone invariance across UTC+12..+14
Under load the CLI's loopback locality probe can time out, and the client
then host-qualifies its sender with this daemon's own self-host id
(member@rig@<selfHostId>). Every strict actor compare then refused the seat's
own work; for example, `rig queue claim` returned claim_destination_mismatch.
The transport identity now passes through one canonicalizer
(canonicalSenderSession / transportSenderSession) before any actor compare or
record. A trailing @<selfHostId> resolves to the bare local seat. Foreign host
qualifiers, unqualified names and pre-boot (null self id) requests are
unchanged. Covered call sites: requireSenderIdentity and
resolveActorWithDeferral (queue create/claim/unclaim/update/handoff/inbox/
outbox and the other attributed routes), transport send/broadcast, the seat
preference audit, and the activity caller scope.
Fixes#131
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* docs(readme): link the guide, help, Q&A and updates on X near the top
OpenRig is on GitHub's daily trending list. A stranger reading the first
screen found no link to the getting-started guide, the help guide, the Q&A
start-here thread or where updates and demos are posted. One line after the
introduction adds all four.
* docs(readme): offer the next walkthrough to visitors not installing today
For trending visitors who can't stop to install now: a secondary call to action
near the top and again after the getting-started steps, pointing to the
existing openrig.dev/follow signup. Nothing about installing the software is
gated.
* docs(readme): show the demo and a Start here link before the install steps
Trending visitors read the install prerequisites before seeing what OpenRig
looks like. Move the existing 'See it running' recording above the install
section, add a Start here link to the guided first-use path, and keep the
follow line after it. The install section, its requirements and the
disclosure of what OpenRig changes are unchanged and in the same order.
---------
Co-authored-by: v-openrig-build <[email protected]>
In a linked git worktree <cwd>/.git is a file, and passing it to Codex as
--add-dir breaks the Linux sandbox for every command. The fresh launch now
resolves the git metadata directories: an ordinary repository keeps
<cwd>/.git; a .git file resolves through git rev-parse to the worktree git
dir and the common git dir (existing directories only); anything
unresolvable adds nothing. A missing .git keeps the previous argument.
Resume and fork are unchanged.
Reported with a reproduction and the suggested rev-parse approach by
mgall-ibizdigital in #121.
Co-authored-by: orch-advisor <orch-advisor@openrig-build>
Reported-by: Hexgunner69 (issue #116, recognition portion). Includes screen-derived regressions contributed by Aummadour in PR #120; the admitted generic recognizer covers these cases without widening the unguarded legacy branch.
Co-authored-by: OpenRig Contributors <[email protected]>
boot_at is SQLite datetime('now'): UTC with no zone marker. Date.parse reads
that form as local time, so on any non-UTC host the boot time shifts by the
UTC offset:
- GET /api/sessions/:name/generation-record refused every Codex seat with
record_identity_unverified west of UTC (the identity observation looked
older than the boot) and accepted stale pre-boot observations east of UTC.
- ContextUsageStore dropped current readings as prior-generation west of UTC.
- resolveIdentityVerifiedClaudeRecord (model-divergence) rejected a fresh
Claude identity as "predates occupant generation" for the first hours after
boot west of UTC; its tests passed an ISO boot time, which hid it.
Add parseSqliteUtcMs and use it at all three sites. The 12 walk-consumption
tests that failed on a UTC-7 host now pass under UTC, UTC-7 and UTC+9, and a
new current-generation-record test covers the SQLite stamp under UTC-7.
Co-authored-by: Coder8124 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
spike/tui-drivability was the Phase-0 agent-drivability harness from
2026-08-02: ten self-contained files importing only each other and Node
built-ins. No test command, config, workflow or published package
reaches it. The Context7 'spike' exclusion stays because
packages/tui/spike still holds a tracked fixture.
Co-authored-by: orch-advisor <orch-advisor@openrig-build>
* ci: add installed baton scenario and regression control to PR tests
* ci: retain healthy return after the durability fault control
* Bind seeded failure to queue observation and propagate pack errors
* test: repair hosted fixture drift and bound temporary IPC
* ci: record a bounded self-process probe before package suites
* test: keep hosted process and transport probes confined
---------
Co-authored-by: dev-driver <dev-driver@openrig-build>
Co-authored-by: dev-qa <dev-qa@openrig-build>
* docs: point agents to the OpenRig help route when OpenRig misbehaves
Add a short pointer to https://www.openrig.dev/help/agents in the
managed start guidance every seat reads, the openrig-skills index (all
three identical copies, digests regenerated) and the README 'For agents'
line. The route sends a symptom to version-matched docs and known
issues and explains how to reach the team. rig --version and rig doctor
help when available, but a broken install is not a reason to skip the
page. No new command.
* docs: one help guide for agents, served by rig context get help
Agent help now lives in one file, docs/reference/help.md. It ships with every
install, so it matches the installed version, and Context7 indexes it. A new
static context pack, help, serves the same file through `rig context get help`
by symlinking to it. The pack build copies the link's target, so installs get a
real file.
The pack generator accepts a static-pack symlink only when it points to a file
under docs/reference/. Any other link fails the build before anything is
written. Tests cover the help pack's bytes and both symlink cases.
The startup guidance, the openrig-skills index (all three copies), the
onboarding capability map, README and SUPPORT now point to `rig context get
help`, falling back to the installed file or openrig.dev/help/agents (the same
text) when `rig` won't run.
* docs(help): say contact options, not a contact form
The website's help page lists email and public Q&A; it has no contact form.
* docs(help): load linked guides on demand; correct the installed fallback path
F1: `rig context get help` served help.md alone, so its links to
getting-started, instance-layout and rig-spec had nothing to resolve against.
A new static pack, reference, symlinks exactly those three files from
docs/reference/. help.md keeps its source-relative links and adds the
`rig context get reference/<file>[#section]` address beside each, so a guide
loads only when it's needed and a help read stays small.
F2: the broken-rig fallback named docs/reference/help.md "in the installed
package". The package build copies these files to daemon/docs/reference/ in
@openrig/cli. Corrected in help.md, the startup guidance and the openrig-skills
index (all three copies, digests regenerated).
Tests: the reference pack ships real files equal to their sources; every
address help.md teaches resolves through the daemon's address route, section
and whole-file; the help preview contains none of the three manuals; a fixture
proves the address check fails on a missing target; source links and the
installed-path text match build-package.sh.
---------
Co-authored-by: v-openrig-build <[email protected]>
.evidence held 23 internal build receipts (test runs, spec drafts, fix-round
notes, 156 KB) committed during 0.5.4 work on 2026-08-26. Nothing in the
product or package uses them. Remove the folder, ignore it at the repo root,
and drop its now-unneeded Context7 exclude.
Co-authored-by: v-openrig-build <[email protected]>
* fix(specs): give the vault-user skill the frontmatter every shipped skill needs (OPR.0.6.1.6)
vault-user/SKILL.md was the only shipped SKILL.md without a name/description
frontmatter block, so it failed the daemon's own parseSkillFrontmatter
contract and external skill syncers rejected it. Adds name + description and
leaves the body byte-identical.
Adds a regression check that runs parseSkillFrontmatter over every SKILL.md
under packages/daemon/specs and packages/daemon/assets, the two skill roots
scripts/build-package.sh copies into the npm artifact.
* test(daemon): correct the shipped-skill check's context-packs scope note (OPR.0.6.1.6)
The projection's default source is packages/daemon/specs/agents/shared/skills,
already inside the checked specs root. Manifest validation is not SKILL.md
frontmatter validation. Comment only; test behavior unchanged.
---------
Co-authored-by: v-openrig-build <[email protected]>
Scaffolded mission manifests (and mission.yaml files without an explicit
metadata block) omit the top-level metadata key. Previously,
readMissionReadiness called mapping(doc.metadata, 'mission metadata')
unconditionally, throwing a JudgmentError ('mission metadata: expected a
mapping') when doc.metadata was undefined. This error was caught at the
top level of readMissionReadiness, causing it to abort composition slice
resolution, leave slices empty ([]), and set state to 'unknown'.
Safely default metadata to {} when doc.metadata is nullish, while
retaining strict mapping validation when doc.metadata is specified. Also
add unit test coverage for missions omitting metadata as well as
malformed metadata negative control.
Fixes#72.
Bring the shipped macOS arm64/Node 24 SQLite caveat (0.5.15 known
compatibility limitation, repeated in the 0.5.17 notes) to the install
line, the Requirements list and Getting Started. State the platform
posture: macOS or Linux; native Windows is not supported yet; WSL2 has
not been tested. The declared Node range is unchanged.
Co-authored-by: v-openrig-build <[email protected]>
6553aaef moved the Codex process-table read into native-process-lineage.ts
and made Codex identity require the pane's foreground process group, start
time and executable name. Two tests still described the old shape and failed
on every platform:
- slow-op-recorder looked for the codex.runtime.list_processes site in
codex-runtime-adapter.ts; it now lives in native-process-lineage.ts.
- lifecycle-scenario-matrix Scenario 6 stubbed ps rows with pid/ppid/command
only, which selectCodexProcess deliberately rejects as insufficient proof.
The stub now carries the full columns, as restore-orchestrator.test.ts does.
Co-authored-by: Martin <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* fix(codex): launch Codex seats with --no-daemon when supported (#69)
Before each managed Codex launch, run `codex --help` once, asynchronously, in the
seat's cwd with its launch PATH. Codex usage listing --no-daemon: fresh, fork,
pod resume and restore resume start with `codex --no-daemon`. Codex usage
without it: the command is unchanged. Failure, timeout or non-Codex output:
refuse the launch and send nothing. Suggested manual resume commands are
unchanged.
Reported by @reisalbuquerque.
* Bound Codex capability probe decision time (#69)
---------
Co-authored-by: OpenRig contributors <[email protected]>
* release: prepare 0.5.17
Set the version to 0.5.17 and add the changelog entry and release notes for
installing with Bun (#66, #68).
* docs: attribute the Bun verification to the installation fix
---------
Co-authored-by: OpenRig contributors <[email protected]>
* fix(package): install without the unpublished @openrig/daemon (#66)
The CLI package declared the private @openrig/daemon in dependencies and
shipped it as a bundled dependency. npm used the bundle, but package managers
that resolve every dependency from the registry, such as Bun, failed with a
404.
The package already ships the daemon under daemon/. The package build now
rewrites the compiled CLI and TUI imports of @openrig/daemon/<subpath> to that
copy, using the daemon's exports map, and fails on any unmapped or unstaged
subpath or any daemon import left afterwards. The dependency, the bundled
copy and their lockfile entries are removed. Source imports and development
resolution are unchanged.
Reported by @drewpayment.
* fix(package): rewrite daemon imports from the syntax tree, not text
Parse each staged file with the TypeScript compiler and rewrite only real
import/export specifiers and literal import()/require() arguments. Text in
comments, strings and templates is no longer changed, and imports that carry
comments or use template literals are no longer missed. Unparseable files and
non-literal module arguments naming the package now fail the build.
* docs: note Bun installation and its postinstall limit
Bun can install the CLI, but OpenRig still runs on Node.js and Bun may
block the package's postinstall check.
* fix(package): run the daemon import rewrite from any checkout path
The direct-run check compared import.meta.url with a hand-built file:// URL,
which never matched when the path needed percent-encoding (spaces, #) or went
through a symlink. The script then exited 0 without rewriting anything. Compare
the decoded module path with the real path of argv[1] instead.
---------
Co-authored-by: OpenRig contributors <[email protected]>
* feat(daemon): let a rig choose CLAUDE.local.md for Claude managed blocks (#25)
A rig spec can now declare
managed_blocks:
claude-code: CLAUDE.local.md
so OpenRig writes its managed instruction blocks for Claude Code members to
CLAUDE.local.md instead of a git-tracked CLAUDE.md. The default stays
CLAUDE.md and Codex members stay on AGENTS.md.
- Validation accepts only the claude-code key with CLAUDE.md or
CLAUDE.local.md, and rejects anything else before a member launches.
- The selection is stored on the rig (migration 085) and bound once in
StartupOrchestrator.startNode, so launch, restore replay, relaunch,
continue and added members all write the same file. Startup
guidance_merge, profile managed_block projection and the conflict
target all use it.
- Teardown cleans only the selected file. The other file is never edited,
moved or deleted; blocks already written to CLAUDE.md stay until the
user removes them, as the reference docs explain.
- Export and the bundle rig.yaml rewrite round-trip the field.
Reported and proposed by @hvpaiva.
* docs(daemon): say accurately which paths deliver the Claude managed-block file
Handover launches the successor directly and writes no guidance, so the
startNode and migration comments no longer list it among the delivery paths.
Comment-only change.
* release: prepare 0.5.16
Set the version to 0.5.16 and finalize the changelog and release notes for
the Claude instruction-file option (#25), the advisory portability report (#56)
and the README update (#54).
* docs: keep uncommitted edits safe when removing old Claude blocks
Recommend deleting only the managed sections by hand, and warn that
git restore CLAUDE.md discards every unstaged change to the file. Say that
portability findings never fail the check while operational errors still do.
---------
Co-authored-by: OpenRig contributors <[email protected]>
* ci: add a portability report for pull requests
* ci: word the empty portability report as a scan result
---------
Co-authored-by: orch-advisor <orch-advisor@openrig-build>
Use raw TTY input and the Node line editor; retain explicit paste submission, cancellation and bounded oversized-input recovery.
Authored-by: dev50-guard (GPT-6 Astra)
Enable bracketed paste in the runner and retain embedded newlines until
Enter submits the complete message. Separate human lines submit immediately;
message routing remains prompt, steer, follow-up or abort as appropriate.
Implemented-by: Dev50-Guard (Codex GPT-6 Astra)
Add an explicit guarded retry from the complete original member fragment.
Preserve the existing node, history and other seats, and document the
operator responsibility for source completeness.
Implementation and composition: Dev50-Guard.
Independent source review: Review-R2; original-fragment completeness and
concurrent losing-request limits remain documented in retained evidence.
Add contribution, conduct, security, and support guidance plus bug,
feature, and pull request templates. Set the project conduct contact
and use the canonical documentation domain.
Community drafts by the Planner agent; source, privacy, and final
preparation corrections by the Guard agent.
Co-authored-by: OpenRig agent (dev-planner) <[email protected]>
Pi 0.84.0 changed JSON and RPC message_update events to carry only the
assistantMessageEvent delta and dropped the cumulative message, so the
pi-runner mirrored no assistant text and Pi seats (and rig transcript)
showed only the user's lines and tool summaries.
Mirror text_delta deltas instead. Older Pi sent the same deltas next to
the cumulative message, so this also stops the reply from being appended
again on every update there. Thinking and tool-call argument deltas stay
out of the pane, matching the previous text-only mirror.
Resolve the generated skill digest conflict by regenerating from the accepted S05 and S15 source trees, preserving both openrig-upgrade and openrig-skills entries.
Founder journey repair for OPR.0.5.8.4 at installed 92a2b17dc. The mission page
rendered every slice as a five-position glyph string (○✓???), an abbreviated proof
cell (p4/4) and an empty assignee placeholder (@—), and called every wave WAITING
because the daemon cannot reach a repository — turning one shared evidence gap into
a work-state verdict for twenty slices the slice files declare done.
Presentation model (packages/tui only, no backend change):
- Header states the DECLARED counts from the slice files and the LIVE count from
claimed lanes as two facts, plus a sources drill.
- One mission-level "evidence gap" line names the cause in plain words with the
unconfirmed rungs and affected count; it opens a page with the git basis and each
blind spot's basis and member slices. Unknown is never rendered as waiting or done.
- Waves remain the spine. Each slice row is ordinary words: a state word (a live
problem, the lane's activity, else the declared status), the slice name when it
fits, and only real facts — assignee when a lane exists, the highest confirmed
evidence rung (built <sha> / reviewed / merged / live, or "no candidate
recorded"), proof N of M, what unlocks next. Narrow panes drop whole facts by
priority instead of clipping mid-word; WAITING and glyph blobs are gone.
- Wave headers count state words; the +N door and every drill page are retained.
- The slice detail strip shows declared vs evidence, real assignment only, and
opens the evidence page; the rung ladder page uses words with each basis.
Regression: a fixture matching the installed state (all declared done, no lanes,
no reachable repository) asserts no WAITING, no glyph blob, no p4/4 or @—, one
evidence-gap line with a drill, and per-wave declared counts.
R2 finding on 1dc508e09: each execution group caps at five rows and the
"+N more" line was inert text, so hidden slices could not be discovered or
drilled. The overflow row now opens a group page listing every row of that
group, each still its own drill; esc returns to the overview. Built from the
same group rows the overview renders, through the existing execution-open
state — no pagination, scrolling framework, backend, or data change.
Regression: a >5-row fixture asserts the door's action, that the page lists
exactly the hidden rows as drills, and that esc closes it.
QA finding on 639ea3789: NEXT mapped the projection's structured
blocked_on_rows entries ({ qitem_id, blocked_on }) through String, so a
blocked slice read "blocked on [object Object]". The overview row and the
slice page now render "<row> waits on <blocker>" from the existing payload.
Regression test added; no schema, backend, or data change.
OPR.0.5.8.4 UX pass over the shipped execution projection. The baseline listed
every slice's ladder as a glyph string, hid NEXT reasons behind "(none)", and
repeated one INDETERMINATE basis once per slice, so a human could not tell what
was done, why nothing was next, or which single blind spot explained twenty
question marks.
- Header names the mission, derivation time, daemon build and slice count, and
opens a sources page listing every projection source cell verbatim.
- DONE counts true rungs only, lists slices highest-rung-first with an aligned
ladder column and a legend, and summarises not-started slices in one row
that is never hidden behind the overflow.
- NOW passes the arbitrated activity and its decider through verbatim, with
the needs-input reason inline.
- NEXT lists eligible slices with their met dependencies; when none are
eligible the projection's own reasons are grouped and counted, never hidden.
- ATTENTION lists needs-input and parked rows individually, groups fragile
joins by basis, and names each INDETERMINATE basis once at the first
undetermined rung with the affected slice count.
- Every row opens a detail page built from the projection's bases (slice
ladder with per-rung basis, lane with oracle and repo join, basis groups
with their members); esc returns to the overview. Rows are clamped to the
pane width so the open affordance always survives.
- The input decoder now reports a held bare Esc and main flushes it after a
50 ms quiet gap, so the esc the screens advertise is delivered instead of
waiting for the next keystroke.
No new endpoint, tracker, poller, scoring, deadlines or dependency. Tests
cover grouping, ordering, clamping, drill pages, esc close and the decoder.
R2 blocking finding at ea0e80d2. --wake-watchdog accepts any active job whose
target matches the parked owner, including the job another row's --wake-after
just produced, and that second binding persists as wake_kind='watchdog' against
the same wake_ref. The S1c lookup selects timer-kind rows only, so a legacy
terminal timer row made resolveWatchdogPreDeliveryTerminalReason claim the whole
job, and the engine terminals it before transport. The still-blocked operator
attachment could never wake.
The resolver's own contract already said this -- "a timer still bound to any
actionable row, AND EVERY OPERATOR-ATTACHED WATCHDOG, remains deliverable" --
and the code implemented only the first half.
This is an ownership question, not a staleness one. The backstop may retire a
job only when the job is SOLELY a park-generated timer. park_timer_target_terminal
speaks for the timer's rows; it has no authority over a watchdog binding it does
not own, so the presence of any armed watchdog-kind binding withdraws the claim.
Pins: the shared-job composition is RED at ea0e80d2. The paired control -- a
solely-park-generated timer with a terminal row is STILL refused -- is green on
both sides and guards the fix against disarming the residue defense it exists
for; it is a control, not a repro.
Both fixtures create the legacy residue by direct SQL, as R2's probe does.
Closing the row through repo.update fires S1b's transition-time retirement and
leaves no residue to guard, which is why an earlier version of these fixtures
proved nothing.
Two blocking contract seams returned by independent R2 at 13531c17.
F1 — a mode-only local modification was overwritten. The helper inventories
each file's mode, but same() compared kind and hash only, so a live file whose
bytes still matched the packaged ancestor while the operator had chmodded it
classified refresh-safe, and apply then installed the packaged mode over their
change. A chmod is a local modification, and the skill promises local
modifications are never overwritten. Equality is now the complete inventoried
state: kind, hash, AND mode.
The conservative direction is bounded, not blanket. A packaged mode advance
still lands when the live file matches the ancestor in bytes and mode both,
which is pinned alongside the refusal so the fix cannot freeze a file the
operator never touched.
F2 — the post-upgrade seat check silently narrowed to one rig. Step 9 taught
`rig ps --nodes --json`; the live CLI defines --nodes as current rig only and
requires -A for all rigs. A daemon upgrade protects seats across every rig on
the host, so the narrow form reported a verified upgrade while seats outside
the caller's rig were never looked at. The inspect helper already used -A; the
check that runs AFTER the mutation did not, and only the helper's argv was
pinned, which is why the contradiction stayed green.
Both shipped copies carry both repairs, and the new seat-check pin asserts
EVERY shipped copy: the existing byte-for-byte mirror pin covers the three
helper scripts, not SKILL.md, so a divergent public copy would otherwise ship
unnoticed.
Verified: R2's own hash-checked probe returns NOT_REPRODUCED with the live mode
preserved at 600 and no write, and both new pins are RED against the candidate
artifacts.
review50-r2 NOT-CLEAR at 0bbb9d9e2, reproduced against the exact policy.
The re-ruled S2 contract names TWO blocker axes: a substantive transition on the
gated row's blocker, OR the blocker reaching terminal. My digest carried only the
blocker's identity and current state, so it implemented the second and silently
dropped the first. A content-bearing note on a blocker that stayed `in-progress`
produced an identical digest and was suppressed as `gate_condition_unchanged`.
Each row's condition facts now include a substantive-transition marker for its
blocker, computed with the same wake-machinery exclusion — so a wake recorded on
the blocker cannot justify the next wake either, which is the same trap one table
over.
The reason this survived my own review is worth naming: my "MATERIAL transition
re-wakes" pin changed the gated ROW only. The contract's quantifier covers two
routes and my pin exercised one, so it read as covered. That is the third time in
this slice that an unbounded phrase was satisfied by a single example, and the
enumeration owed was of the CONTRACT's clauses, not just of the code's callers.
Two pins added: the same-state blocker transition (RED without this change), and
a blocker-side wake-machinery marker staying non-material (a guard on the new
axis, which cannot fail without it since the axis did not exist).
dev50-qa NOT-CLEAR at f610eec7d, reproduced on an isolated candidate daemon: the
first attempt was sent/FAILED yet stored the fingerprint, the pane was then made
reachable without changing the row, and no retry ever came — the seat stayed
silent until an unrelated transition changed the fingerprint.
My repair was worse in kind than the bug it fixed. The original over-woke, which
is noise. This suppressed indefinitely on one transport failure, which is a
dropped obligation.
The cause is in my own justifying comment: I wrote the receipt during evaluate(),
"mirroring how the engine stamps last_fire_at on the send path regardless of
delivery outcome". That mirrored the timing and inverted the consequence.
`last_fire_at` gates a window that EXPIRES, so a failed delivery self-heals; the
condition gate holds until the gated set materially changes, so it never does.
Copying a mechanism's write timing without asking what BOUNDS the state it writes.
The policy now PROPOSES the fingerprint on its send evaluation; the engine banks
it only when delivery returned "ok". Failed delivery leaves the receipt untouched
and the next scan retries. That also means the layer claim from the previous
commit does not survive — suppression cannot be decided in the policy alone,
because only the engine knows whether the wake arrived. The engine change stays
policy-agnostic: a policy that proposes no receipt is untouched.
Pins moved onto the ENGINE path, because the defect lives in the join and a
policy-only pin asserts a decision rather than an outcome. Six pins are RED
without the banking, including QA's exact case: failed delivery banks nothing,
retries, then banks and suppresses once delivery succeeds.
One thing measured rather than assumed, after I got it wrong twice: following a
send, EVERY evaluation returns active_wake_not_due until the window elapses — the
throttle branch preserves `actionable`, unlike the generic skip branch which
clears it. The pins now advance a controllable clock past that window so a skip
can only be the condition gate, instead of passing on the throttle and proving
nothing.
An unchanged human-blocked row re-woke its orchestrator on a timer. Reproduced on
an isolated daemon before any of this was written, two distinct routes:
R-1 window expiry: wakes at +0s and +120.3s, exactly the configured
active-wake window, with the row's only transitions being `created` and
its park.
R-2 flicker: anchor fire, seat briefly active (observed driving actionable=0),
seat idle again, re-fire 60.1s into a 120s window.
R-2 is the route that explains the field report. The active-wake window was never
a cooldown: watchdog-policy-engine clears `actionable` on EVERY skip, and the
throttle's precondition includes `job.actionable`, so a moment of seat busyness
skips the window entirely. At the production 900s window a flickering seat can be
re-woken as fast as the 60s scan — which window expiry alone cannot produce, and
which matches the reported ~3 minutes.
The gate now fires once per MATERIAL STATE of the gated set. A fingerprint over
the qualifying row ids and each row's material state — row state, blocker and
blocker state, claim, and the latest transition that is NOT wake machinery —
is compared against the one the job last fired for.
LAYER: the policy, not the engine. The engine keeps today's throttle semantics
for every other policy by construction rather than by assurance — the only engine
edit is adding one skip-reason string to the quiet list, which changes no
behaviour and which no other policy emits. The window remains beneath the
fingerprint gate as a floor.
The memory keys to the CONDITION, never to seat activity: skips for seat_active,
seat_needs_input and activity_stale_unknown return before it is read or written,
so a flicker can no longer launder a fresh wake out of an unchanged row.
Wake-machinery markers (wake-attempt:, escalation-rung:, ladder-exhausted:,
ladder-suspend:/resume:, park wake fired:, idle-gate:) are excluded from
materiality — if they counted, every wake would justify the next one.
State: one nullable column, overwritten, never appended to. A per-wake ledger is
the bookkeeping this repair is not allowed to add. Suppression stays derivable
without one: the row is untouched and still appears in held/escalations, and the
job carries the exact condition it fired for.
Two existing assertions are AMENDED, not loosened: both asserted the skip reason
`active_wake_not_due`, which is now `gate_condition_unchanged` because a stricter
gate decides first. Their subject — fires once, delivers once — is unchanged and
still asserted by the delivery counts. The new reason is asserted exactly rather
than relaxed to "some skip", because the two are not interchangeable: one expires
with the window and is bypassed by a flicker, the other holds until the gated set
materially changes.
review50-r2 held 8dfe8a3a: `handoff()` is its own transaction and never routes
through `update()`, so the S1b retirement never ran for it — and that is the exact
route the founding specimen took. Row b7a70333 went handed-off at 10:02:03Z and
its timer still fired at 10:18:07Z. Every door was fixed except the one the
specimen walked through.
Repaired as a class rather than an instance. The decision now lives in one
private helper, `retireParkGeneratedTimer(qitemId, reason)`, called inside the
state-transition transaction of every writer that can move a row out of a
timer-backed park.
ENUMERATION, and how it was searched, so the absence claim is scoped: grepped
`SET state` across queue-repository.ts and attributed each hit to its enclosing
method. Nine writes in nine sites across eight methods. Of those, six can move a
row out of `blocked`:
handoff() -> handed-off [r2's finding, the specimen route]
handoffAndComplete() -> done [r2 named, not executed]
closeCrossHostHandoffSource() -> terminal [r2 named, not executed]
claim() -> in-progress [NOT previously named]
propagateBlockerCompletion() -> pending [NOT previously named]
updateInTransactionalContext() -> any [already hooked]
The two not previously named matter on their own terms. `claim()` IS the
"claim-resume" the story contract listed: a blocked row is claimable, and my
earlier pin exercised that outcome through `update()` — a different spelling of
the same end state, which is precisely why the gap read as covered.
`propagateBlockerCompletion()` is auto-unpark, and a row parked with
`--on X --wake-after 20m` carries BOTH a blocker and a timer, so the blocker
doing its job used to leave the timer firing afterwards at an unparked row.
The remaining three state writes cannot exit a park: `unclaim()` and
`releaseClaimsByGeneration()` act on in-progress rows, and the second `claim()`
write is the same transition in its non-generation branch.
Each route is pinned through its REAL method, never another spelling of
`update()`. Five of the six are RED at 8dfe8a3a; the sixth was already hooked.
An operator's attached `--wake-watchdog` surviving every one of those exits is
pinned too, since retiring jobs it does not own is the way a six-site change
could do harm.
Third and last repeat route for a park timer. Re-parking — blocked -> blocked
with a fresh --wake-after — armed a second job while the first stayed active, so
one row carried two live timers, each firing on its own cadence forever.
Per the planner's ruling, a new park episode supersedes the old: arming a new park
wake ends the prior park-generated timer with reason `park_superseded` before the
new one arms. The survivor is then measured from its OWN arming at its OWN
duration, which the pin asserts in both directions rather than only at the
boundary.
The prior wake is read BEFORE the new armed row is recorded, because getStatus
returns the most recent armed wake and would otherwise return the one being armed.
Deliberately not gated on the previous state. The live+timer test is the real
safety, and leaving it ungated also clears a stale park-generated timer left by
any path that did not unpark cleanly. It cannot over-reach: an already-terminal
job fails the live test, and an operator's attached --wake-watchdog fails the kind
test — pinned for the re-park case as well as the park-ending case.
Completes the set with the two repairs in the previous commit: a park timer now
fires once, at its requested time, and only while its park episode is live.
Two defects, one root: nothing ever ended a park-generated timer.
ONE-SHOT. The arming path registers `periodic-reminder`, which repeats every
intervalSeconds forever, so an unstopped park timer wakes its owner again at +2
intervals, +3, indefinitely.
PARK LIFETIME. No path stopped the job when the row left `blocked`. Specimen: job
01M1E6F3QG41N76Y1CDX48P766 fired at 10:18:07Z for a row that went handed-off at
10:02:03Z — sixteen minutes terminal, and the wake still told the seat to resume
the recorded continuation. Done must never read as owed.
Both repairs widen behaviour this file already had for provider-limit parks — the
same shape as the S1 repair immediately before it. The fire path already called
markTerminal after firing, gated on the usage-limit tag; the ordinary case now
does the same, WITHOUT the provider-limit blocker resolution, which is a
provider-limit outcome rather than a timer one. The S16 path is otherwise
untouched and its existing pin still passes unchanged.
Lifetime binding is enforced AT THE TRANSITION rather than by checking row state
at fire time, because the fire path delivers before it consults the queue:
`recordWatchdogWakeAttempt` runs after delivery and merely returns early when the
row is gone, so a check there would audit a wake that had already been sent.
Only park-GENERATED timers (wake kind "timer") are stopped. A watchdog the
operator attached with `--wake-watchdog` is theirs, may target other rows, and
survives the park ending — pinned.
No new scheduler and no per-wake bookkeeping. S1's two-durations pin stays green.
Blocker-resolution (`--on`) wakes are untouched; existing coverage passes.
R1 story-boundary review, three findings against b2abc85d6.
F2 (blocking, comment-only). The header claimed "Resolution runs BEFORE any
counting, and it is failure-first." The module does not do that: the within-row
conflict check counts first and those rows never reach resolution. I wrote that
line in an earlier round and then added the conflict branch ahead of it without
updating the claim, so the file's most-trusted artifact was false about its
central invariant — precisely how the next editor would reintroduce the class
this slice spent three rounds removing. The header now separates the two checks:
within-row malformed rejection up front, cross-row resolution-before-counting
after.
The stated reason for the within-row refusal was also wrong. It claimed the
module "cannot prove two spellings are the same thing before resolving them",
which this file disproves — resolveRow plus the byPath dedupe do exactly that
across rows, and a test pins it. The behaviour is right for a different reason:
a single row naming its mission twice, differently, is MALFORMED, and refusing
malformed input is this module's job. Resolving it would be repairing a caller's
bad row and calling the repair an answer.
That rationale had been written in two places and the first correction only
fixed one. The regression beside the behaviour still taught the disproven
version, contradicting the production comment next to it — a contradiction no
green suite can see. Both teaching sites now give the malformed-row reason and
state plainly that cross-row spelling resolution IS supported.
F1 (concerning). "no typed in-progress work" was true about the query and false
about the world: a seat whose only typed baton is blocked holds real work, and
"you hold nothing" points somewhere different from "your work is parked". The
refusal now names its scope. The in-progress filter itself is a documented
ruling and does not move.
F3 (concerning). Two refusals named values but never which row, though the
production call site passes full queue items. TaggedRow takes an optional
qitemId and the conflict and unresolved refusals name the row when it is
supplied, degrading to "a row" when it is not.
R1 ruled the scope question in favour of keeping the unbounded query: reading
the capped mixed-state projection turns a required refusal into a confident
answer, which is an acceptance violation rather than an inefficiency.
Proof: 122/122 across six suites, daemon tsc no-emit clean, daemon build clean,
real-data ambiguity matrix re-run. No behaviour change; refusal text only.
A returning agent held a typed mission/slice baton the daemon could name, while
the refocus trace reported "current work node is unresolved" — because the trace
only ever learned the work node from OPENRIG_REFOCUS_WORK_NODE, which no live
seat carries. The resolver was already sound; only the connection was missing.
Two touchpoints on existing seams:
1. `queue whoami` gains a derived `currentWork {mission, slice, workNodePath,
basis}` plus an always-present `currentWorkBasis`. It reads the UNBOUNDED
in-progress set via the new QueueRepository.listInProgressForDestination,
never `asDestination.recent` — that projection is capped at 25 and mixes
states, so a second baton past the cap would be invisible and the ambiguity
refusal would silently become a confident answer.
2. refocus.cjs derives --work-start from that field when
OPENRIG_REFOCUS_WORK_NODE is unset, using the same spawnSync posture and
timeout as the existing `rig context get` call. The explicit variable always
wins and short-circuits the consult; any failure falls through to today's gap
line rather than guessing.
The derivation answers only when the seat's typed work is unambiguous, and every
other outcome carries a named basis:
- per-prefix tag values are deduped, and a row carrying conflicting mission or
slice values refuses — array order is not data and must not pick the answer;
- any typed row that fails to resolve refuses outright, because an unresolved
baton is unknown rather than irrelevant, and disclosing it in the basis does
not discharge it (consumers read workNodePath, not the prose beside it);
- cross-form dedupe collapses rows naming one node only when every typed row
resolved;
- mission and slice each join by directory name or SPEC frontmatter id. The
canonical tag pair is mission:<directory> + slice:<dot-id>; the id-form
mission tag is historical compatibility for rows already on the board and is
labelled as such wherever it surfaces.
Untouched: trace-to-root.py's existing env/CWD logic, and no ledger, schema,
per-seat config, threshold, or TUI surface. Current work is never inferred from
CWD, recent files, or an untyped row.
Proof: 121/121 across six suites, daemon tsc no-emit clean, daemon build clean,
plus a real-data probe of the ambiguity matrix against the live workspace tree.
No live-daemon leg: the route wiring is reviewed at source, not observed running.
An explicit `rig queue block --wake-after <duration>` fired almost immediately
instead of at the requested time. Measured through the public seam on the running
0.5.7 daemon:
requested 20m -> fired 0.69s after arming
requested 2h -> fired 0.77s after arming
The duration was never lost. `interval_seconds` held 1200 and 7200 correctly. It
simply was not what the scheduler measured against: `isDue` treats a job with no
`last_evaluation_at` as due, so a freshly armed timer is due on the scheduler's
very first pass whatever its interval. The ~34-40s in the reported specimens was
how long until the first scan reached those jobs — a symptom of scan cadence, not
a fixed latency and not the mechanism.
The correct behaviour already existed. S16 seeded `last_evaluation_at =
registered_at` at arming so the due boundary becomes registeredAt + interval, and
recorded that it applied to provider-limit parks only. Widening that one
condition is the entire repair: same mechanism, already proven by the
provider-limit path, no new scheduler and no per-wake bookkeeping.
Verified base-vs-candidate through the same public seam, candidate running as an
isolated daemon on its own OPENRIG_HOME, DB and port:
base last_fire_at = registered_at + 0.69s
candidate last_evaluation_at = registered_at, NEVER FIRED at +98s
One existing pin is AMENDED rather than rewritten to pass. Its subject — a park
arms a timer atomically and records its generated watchdog id — is unchanged and
still asserted; only two incidental lines described the defect as expected
behaviour. It now asserts the boundary in both directions. The assertion that
ordinary timer parks gain no expiry field is deliberately kept, since this repair
adds no bookkeeping.
Follow-up to the lead ruling on OPR.0.5.8.15. No behaviour change.
1. The standalone-parse JSDoc was orphaned. Inserting assertFragmentOpensWithTable
ahead of assertFragmentParsesStandalone left that comment describing the
function two definitions below it. Moved back onto its own function.
2. dropCollidingFragmentTables still documented root keys as a remaining
inherited limit that "pass through" to the render check. OPR.0.5.8.15 refuses
that shape upstream, so the note described behaviour that no longer exists.
Replaced with what is now true: every block this function sees is a table.
3. RETRACTED: I claimed assertRendersAsLoadableToml had become unreachable. It is
live. When the user's own config does not parse, dropCollidingFragmentTables
discriminates nothing, every block is retained, and the still-invalid render
hits the guard. The counterexample was already in my own test file — the pin
"leaves the fragment intact when the user's config does not parse" walks
exactly that path.
I searched my own imagination for a reaching input, failed to find one, and
reported that failure as a property of the code. The honest statement was "I
could not construct a reaching input", not "none exists". The claim is
retracted in the test comment, in PROGRESS.md, and in an appended correction
to the proof artifact, with the wrong version left visible in each.
The guard stays.
A fragment's leading root-level keys never reached document root. The managed
block is appended to the end of the user's config.toml, and TOML has no syntax
for reopening document root after a table has been declared — so a root key
silently joined whatever table the user's file ended inside:
fragment: matrix = [[1, 2]]
user ends: [mcp_servers.other]
result: mcp_servers.other.matrix (root.matrix absent, no warning)
Their own table gained a key they never wrote, the file stayed valid TOML, and
no guard or receipt reported anything.
Preserving the author's intent is not larger work inside this seam, it is
impossible: prepending the managed block inverts the same bug onto managed
content, and re-serializing the document destroys the user's comments and
formatting. So projection refuses, and the error names the author's fix.
Refusal is DETERMINISTIC and never inspects the user's file — including the case
where their document happens to end at root and the key would have bound
correctly. A fragment author cannot see user state, and a contract that passed or
failed on someone else's file is one they could never reproduce.
Detected from the fragment's preamble rather than from parsed value shapes. The
disposition ruling proposed the latter; the preamble is the same intent with a
tighter edge, because a root key holding an inline table (`x = [{a=1}]`) is
indistinguishable from an array-of-tables once parsed, and would slip through
while binding into the user's table like any other root key.
Two existing pins are AMENDED rather than rewritten to pass:
- R2's nested-array repro (OPR.0.5.8.12) placed its array at root, a shape now
refused for an unrelated reason. The array moves inside a table — the only
legal shape — and its depth claim is asserted unchanged, so the thing R2
cleared is still pinned.
- The duplicate-root-key pin no longer claims WHICH guard fires, because that
claim became false. The user-visible contract is unchanged and still pinned.
Also documented in docs/reference/agent-spec.md with accepted and refused
examples, since the public contract advertised TOML fragments without this
constraint.
review50-r2 held a760ec27 NOT-CLEAR. The fragment splitter treated every
outside-string line whose trimmed text began with `[` as a table header. A
continuation row of a multi-line array begins that way too:
matrix = [
[1, 2],
[3, 4],
]
so the array was torn into invented "blocks", tested independently, and the
render guard then refused a fragment that is perfectly valid TOML. The base
implementation projected it successfully, making this a regression I introduced.
The line's own text cannot answer the question. Depth can: at document level
TOML allows nothing but a table header to start with `[`, and inside an array or
inline table a `[` is always value syntax. The scanner now tracks bracket and
brace depth alongside the string state it already tracked, and a header is a
document-level line starting with `[`. Renamed to lineStartsAtDocumentLevel,
since that is now what it answers.
This is the third instance in this slice of the same mistake: a predicate that
could not discriminate, made to return a confident value anyway. The two earlier
ones were a lexer deciding string boundaries over user input, and a single
boolean carrying both "collides" and "could not parse".
Also pinned, and NOT introduced here: keys ahead of a fragment's first header
bind to whatever table the user's document was still inside, because the managed
block is appended at the end. Verified identical against the base raw-append
implementation on a non-colliding input. Documented and pinned rather than
changed; relocating the managed block is a different seam.
review50-r2 held ebfe60d9 NOT-CLEAR. The parser-backed collision predicate asks
whether the user's document still parses with a managed table appended, and
treats failure as a collision. That failure has two causes — the user owns a
conflicting path, or the block is malformed on its own — and the predicate could
not tell them apart.
So an intrinsically invalid fragment was classified as colliding and DELETED.
The render guard then passed precisely because the bad input was gone, the user's
file was modified, and the receipt reported the resource projected:
fragment: [mcp_servers.exa]\nurl =
result: projected ["codex-default-config"], failed []
written: an empty managed block
An authoring failure was indistinguishable from a successful projection with no
managed content, which is the opposite of what a fail-before-write guard is for.
A managed fragment is now required to be a valid TOML document on its own,
checked before anything is dropped. An invalid one throws: zero projected, one
failed entry naming the TOML error with line and column, and the existing config
left byte-unchanged.
Correction to my previous handoff: I told the reviewer that two fragment blocks
colliding with EACH OTHER was a real gap in the new mechanism. That was wrong.
The render guard already caught it, verified by the new pin passing against the
held candidate as well as this one. It is kept as a guard test and named as such,
not as a repair.
review50-r2 held the prior candidate NOT-CLEAR. The user-side header scanner
closed a multiline basic string on any raw `"""`, ignoring backslash escaping.
So in this valid TOML:
[profiles.notes]
text = """
\"""
[mcp_servers.exa]
this is still string data
"""
the escaped delimiter read as the string's end, the next line read as a declared
table, and the genuinely non-conflicting managed [mcp_servers.exa] was dropped
while projection reported success. The render guard cannot catch that direction,
because the wrong answer is still valid TOML.
The repair is not another lexer rule. Collision detection over USER input no
longer makes a lexical decision at all: for each table the fragment declares, ask
smol-toml whether the user's document still parses with that table appended. A
duplicate declaration is exactly what TOML rejects, so the parser's question is
the one we need — on the arbitrary input, where guessing is least defensible.
Consequences:
- The escaped-delimiter case now lands both managed tables, verified through the
real projection path (regression test asserts all four facts together: user
declares no exa, projection succeeds, exa AND context7 land, user string
unchanged).
- Matching [[array.of.tables]] on both sides is legal TOML and now correctly
KEPT rather than conservatively dropped; the previous caveat is gone.
- A user config that does not parse yields no collision decision, so nothing is
dropped and the render guard refuses the write. We do not repair a file we
cannot read.
The line scanner survives only to split OUR OWN shipped fragment, and its
multiline-basic-string escape handling is fixed too.
A projected config fragment was spliced into ~/.codex/config.toml as raw
text. A user who already owned a table the fragment also names got a
duplicate table, and Codex refused to start before the agent existed:
Error: failed to load bootstrap configuration
config.toml:9:14: duplicate key
Reproduced and fixed against codex-cli 0.147.0 (exit 1 -> exit 0).
At the existing projection seam:
- Table headers the fragment declares that are already declared outside
this managed block are dropped from the fragment. User values are never
merged, rewritten or overwritten; the managed table yields.
- The rendered result must parse before it is written. If it does not,
applyRuntimeResource throws, project() records the entry as failed, and
the existing file is left untouched.
Collision is compared on DECLARED header paths, not on parsed objects: a
parse cannot distinguish a table declared by its own header from one
implied by a child, and only declared tables collide. A user's
[mcp_servers.foo] and the fragment's [mcp_servers.exa] share an implied
parent and are legal together.
Adds smol-toml (parse-only, zero dependencies) to the daemon, and mirrors
it into the cli, which vendors the daemon and must declare a superset of
its runtime dependencies.
Delta commits 3c24b69a0 ('docs: make shipped references self-contained')
and d83f84d70 ('fix(s12): classify packaged derived artifacts') modified
packages/daemon/policies/builtin/standard.policy.md and yolo.policy.md
content but did not update the corresponding entries in the packing-
policies test's AUTHORITY_SHA256 constant. Aligns the test's expected
hashes with the actual shipped-content hashes so the T7 byte-equal
check reflects the current shipped surface.
No product code change; no policy-content change; only the test's
expectation is being brought forward to match commits already in the
delta.
v0.5.6 — world-building as context engineering. Three themes: the context
system (mission install, lore routing, taxonomy, verifiable public world
pack, complete skill index, substance gate); continuity (compaction_strategy
policy, apprentice-handover, managed compaction with honest restores,
usage-threshold watchdog primitive); the human layer (destination resolver,
delivery receipts on the row, notification levels + dials, delivery-rules
engine, Slack files as work). Plus operational honesty fixes and the SDLC
conventions component menu.
Three new migrations: 074_context_usage_watchdog +
075_context_usage_watchdog_generation + 076_owner_notification_levels
(head advances 073 to 076); 41 shipped context packs (up from 39).
Bounded version bump across the release-owned files (root package.json,
packages/cli|daemon|ui/package.json, and the matching root+workspace entries
in package-lock.json). packages/tui remains at its independent 0.1.0.
The reserved-era closed enum {harness_native, pod_continuity} becomes the four-mode
vocabulary {default-compaction, managed-compaction, handover, apprentice-handover}
via MAPPED SUPERSESSION per the locked A1 rule, never naive replacement:
- agent-manifest.ts: canonicalCompactionStrategy() is the ONE vocabulary site
(exported; resolvers import it — one-shared-resolution-path). Deprecated aliases
validate with a deprecation ADVISORY on the shipped fail-open
ValidationResult.advisories channel (OPR.0.5.3.3 — the existing field, no schema
change; disclosed design-cell revision from the drafted warnings[]).
harness_native→default-compaction (same meaning, 0.5.2 source map §1/§4);
pod_continuity→handover (derived: 'the handover practice is pod_continuity's
implementation, not a new concept', source map §4). custom_prompt rejection
BYTE-PRESERVED. normalizeLifecycle maps aliases; unspecified normalizes to
default-compaction — today's behavior under the new name, F-6 authority; the
seat-class default remains an open founder cell per A1.
- types.ts: LifecycleDefaults union widened to the canonical four;
RigSpecPodMember gains optional compactionStrategy (the member override).
ExpansionPodFragment deliberately NOT touched: that transform leg's traversal is
slice 23's class-closure census territory (silent-erasure class) — named there,
not absorbed here.
- profile-resolver.ts: resolveCompactionStrategy sibling — most-specific-WINS
(spec default < profile < member), the restore-policy PATTERN without its
narrowing lattice (the four modes are unordered; desk-concurred planner call,
disclosed on baton bd7eef84). Invalid values error naming the level; aliases
normalize at every level. ResolvedNodeConfig carries compactionStrategy.
- docs/reference/agent-spec.md: example rows, value table, and validation rule 13
updated in lockstep (deprecated aliases documented with their targets).
Stale-literal sweep: zero harness_native/pod_continuity literals remain in src
outside the vocabulary site. NOT RUN: no install/test executed in this
authoring-only window — RED/GREEN execution rides the admitted test atom; the RED
commit (f6895c2af) precedes this one at the same base.
Committed at pristine base f7301d6ba BEFORE the implementation, per the wave
discipline. These specify the locked A1 compatibility rule (harness_native →
default-compaction alias; pod_continuity → handover alias, derived from the
0.5.2 source map; custom_prompt teaching rejection byte-preserved; advisories
via the shipped ValidationResult.advisories channel) and override-wins layering
(the modes are unordered; the restore_policy precedent lends the layering shape,
not its narrowing lattice). Execution deferred to the admitted test atom —
authoring-only window; no test has been RUN.
The component menu's graduation note is discharged: planning-dial, wave-sdlc,
release-boundary (carrying the capability-delta lifecycle law), and
product-management-pass land as full reference documents, split per the ruled
per-sentence test - generic halves here, institutional halves in the originating
team's playbook. Menu rows link their references; a dated known-gaps section names
the remaining distance to an end-to-end SDLC with a route each. Additive only: no
existing component removed or overwritten.
The product-management pass — the layer above planning — was the SDLC's
missing component, worked through live during 0.5.6 preparation. The menu
row summarizes it; the full SOP graduates in via the canonization slice.
The conventions SSOT now opens with a component menu: the SDLC is assembled
per mission from components per phase (planning dial P0-P4; simple flow,
wave model, or assigned rigorous overlay for build; release ceremony;
release boundary), with multiple valid options coexisting. Summaries are
the shipped description until fuller reference docs graduate in.
The six slice-template SOP blurbs restated the full rigorous flow verbatim,
which the SSOT header itself forbids (restatement is how drift is born) and
which taught the heavy path as the default. They now point at the menu and
carry only the universal floor: progress tracked, evidence via rig proof
add, done means evidenced outcomes, verified by rig scope audit.
Bounded version bump across the release-owned files (root package.json,
packages/cli|daemon|ui/package.json, and the matching root+workspace entries
in package-lock.json). packages/tui remains at its independent 0.1.0.
verifyRequiredListeners takes a TRI-STATE probe (healthy | unhealthy |
indeterminate) with NO catch-collapse; ANY indeterminate listener returns
the gate's own indeterminate arm (precedence over missing — an incomplete
evidence set never authorizes a kill). startDaemon's probe preserves
provenance: explicit non-OK or ECONNREFUSED (err.code / err.cause.code) =
positive evidence "unhealthy"; timeout or any other exception =
"indeterminate". Only ok===false SIGTERMs; indeterminate falls through
healthy — the implementation now matches its own stated contract and the
handoff claim the reviewer refuted.
Receipts (from watched RED, 5 pins at the exact semantics): the
reviewer's reproduction shape as a start-level control — one-shot probe
timeout resolves the start with ZERO kill calls; refused listener still
SIGTERMs loudly; gate-unit indeterminate pin; dropped-listener and
both-verified controls re-earned on the tri-state probe. Full affected
suites 138/139 (1 pre-existing skip); cli tsc 0; repo lint 0. One fixture
iteration recorded: the fake answered healthz pre-spawn and read as
already-running — gated on spawn like the up harness.
The r2 blocker's exact chain closed: commands/start.ts and commands/up.ts
now resolve bind intent via the SHARED resolveBindIntent (no fourth
predicate) — an env-sourced daemon.host (ENV_MAP ← OPENRIG_HOST) creates
NO intent through either auto-start entry point; only a FILE-sourced
daemon.host or OPENRIG_BIND_HOST does (auto-start has no flag). The stale
positive pin in up.test.ts that REQUIRED injected OPENRIG_HOST to survive
into the spawn env is replaced by the S20 control (ruling cited in-pin);
equivalent rig-start coverage added (spawn-env capture with an in-spawn
abort so the kernel/restore phases — real-URL polls — never run; start
gains the preflightExec test seam mirroring up's). The contradictory
shell-level-opt-in comment at the buildDaemonEnv opts.host doc is
corrected to the S20 contract.
Receipts: up+start+provenance+lifecycle suites 135/135 green from the
watched RED (both new pins failed at the exact conversion with the
distinct-value discriminator); cli tsc 0; repo lint 0. One start-pin
harness iteration recorded honestly: the first shapes hit the real-URL
kernel wait — the landed pin aborts at its own subject.
Dedicated surface: OPENRIG_BIND_HOST (new env, no legacy alias) + the
--host flag + a FILE-sourced daemon.host are the ONLY bind-intent
channels. resolveBindPlan (new pure module) computes the effective plan;
index.ts consumes it — an inherited OPENRIG_HOST never selects single-bind
and its ignore is LOGGED (the provenance line), never silent. The
auth-bearer-tailscale-trust invariant rides the dedicated surface
unchanged.
Route B closed: buildDaemonEnv scrubs ROUTING_ENV_SCRUB
(OPENRIG_HOST/RIGGED_HOST) from the daemon env unconditionally; declared
intent exports OPENRIG_BIND_HOST plus a COHERENT OPENRIG_HOST (the
startup.ts routing consumer — activity URL + seat env — follows the
declared bind); inherited OPENRIG_BIND_HOST passes through (dedicated =
unambiguous). The passthrough loop's contract states the census rule for
future edits.
Route A closed: commands/daemon.ts resolves intent via resolveBindIntent —
an env-sourced daemon.host (ENV_MAP maps it from OPENRIG_HOST) never
creates intent; flag/file/dedicated-env do.
Gate restored: healthz exposes the effective bind plan (additive; absent =
legacy body); startDaemon derives the REQUIRED listener set from the
effective mode and probes every listener's own /healthz — a silently
dropped listener (default mode + tailscale detected + one listener, the
0.5.3-receipt shape) kills the start LOUDLY; gate acts only on positive
evidence of a bad bind (its own probe failure never fails a healthy
start); older daemons without the payload skip unchanged.
The old shell-env-wins pin re-grounded with the ruling cited (it encoded
the dead premise; the dedicated-channel half preserved beside it).
Receipts: bind-plan 4/4 + provenance 10/10 from watched RED b75917960;
lifecycle suites 88/88; startup 24/24; daemon+cli tsc 0; repo lint 0.
performWakeSend classifies the destination BEFORE any transport touch: a
virtual @external destination (parseSessionName kind "external" — the
lexical classifier; human-*@kernel seats with real panes are NOT this
class) short-circuits to classified=indeterminate with the honest record
"gateway-owned: … delivery rides the gateway subsystem (Slack connector),
whose own ledger is the delivery record; tmux was not consulted (it can
never hold this address class)". Covers BOTH wake paths through the one
shared core (create-path maybeNudge and the durable handoff wake-intent
drain). The failed: literal disappears for the class, so findUndelivered
stops reading delivered founder messages as failed wakes (the
dogfood-aggregation caveat's mechanism).
Receipts: fix suite 5/5 green from RED (3 defect pins at the defect layer
+ agent and human-@kernel controls); queue-repository 58/58 unchanged;
queue-routes + outbox + p34 neighbors 87/87; daemon tsc 0; repo lint 0.
B1: routes/seat.ts (the sole production constructor, census-confirmed)
passes the context's seatActivityService as activityOracle — a real
committed swap now reaches declareOccupantSwap; the route pin captures the
production deps per R2's own method.
B2: /api/activity/parked resolves ONE declared rig scope (seat@rig
coordinate > ?rig= > caller session identity; canonical and host-stamped
forms both parse), verifies the rig exists (404 teaches the known set),
adds the rig predicate to the census SQL, and NAMES the resolved scope in
both response forms (AM-3). No resolvable scope = 400 teaching — never a
silent fleet fold. CLI carries the coordinate (--rig, else the shell's
seat identity; seat@rig self-scopes) and renders the scope line.
INDETERMINATE/truncation/explicit-seat semantics preserved (pins).
B3: refresh.ts singleFlight gains dirty/trailing coalescing — an
overlapping request marks the flight dirty and the runner performs at most
one trailing pass per overlap window; every push window is represented,
quiet refreshes do one pass, task errors cannot break the coalescer, and
callers' promises settle after their trailing pass.
Receipts: three-seam suites GREEN from RED e654088c5 (daemon B1 1/1 +
B2 5/5; tui B3 4/4); neighbors green (parked-query 13, parked-route 3
scope-updated fakes, handover 54, live-events 6, hydrate 21 = 102 daemon+
tui total this run); cli parked-verb 5/5 incl. the carried-coordinate pin;
daemon+cli tsc 0.
B1: the real seat route constructs the handover service WITH the context's
seatActivityService as activityOracle (candidate captured undefined —
mocked constructor captures production deps, per R2's method). B2: real
sqlite two-rig fixture through the real route — caller from rig-a sees
only rig-a and NOT-PARKED while rig-b owes work; explicit ?rig= scopes;
no-scope refuses with teaching; unknown rig teaches known set;
seat-coordinate self-scoping pinned as preserved semantics (passes today —
declared). B3: R2's blocked-hydrate discriminator — a push mid-flight
lands exactly one TRAILING hydrate with the fresh snapshot; many pushes
coalesce to one; quiet stays zero (passes today — declared); error
recovery through the trailing path.
Watched RED: daemon 5/6 failed (B1 1, B2 4), tui 3/4 failed — each at its
production seam.
The S16 architectural pins caught the leg honestly: (1) HTTP moved into
daemon-client (openActivityEvents — FR-8 one-module rule), live-events now
consumes an OPENER and parses frames only; (2) FEATURE-DETECTION: a
non-OK/non-event-stream answer returns null and the leg disables
PERMANENTLY (one probe, zero retries — an older daemon gets exactly S16's
click-to-refresh behavior; new pin proves one hit against a JSON server
across many reconnect windows); reconnect only follows a genuinely
ESTABLISHED stream, with doubling backoff to 30s, unref'd; main.ts comment
reworded off the route-string grep.
Receipts: tui full 397 tests — 396 green + refresh-activity cadence pin
passing ISOLATED (its 5.3s idle window is contention-sensitive in the full
run; the S19 leg holds it: one feature-detect request in initialReads,
zero idle reads after); daemon-client + live-events suites green; tui tsc 0.
Oracle: arbitrate() emits seat.activity_changed (identity + seq + at) on
every arbitrated CHANGE and on occupant swaps — notification only, no
vocabulary in the payload. Route: GET /api/activity/events (SSE, the
desk-accepted endpoint) relays seat.activity_changed + seat.rung_health
off the event bus; pure relay, zero timers, unsubscribe on disconnect,
honest 503 without a bus. Handover: the SOLE ruling-01530 narrow call —
post-commit, this.activityOracle?.declareOccupantSwap(node.id,
successorSessionId) — optional dep in the ghost-stage idiom; pre-commit
failures never declare. TUI: live-events.ts consumes the stream (SSE frame
parse, reconnect backoff = connection maintenance only, unref'd timer) and
main.ts wires pushes to live.refresh() — the open view rehydrates the same
/api/ps projection, closed on quit.
Receipts: daemon events-stream 5/5 + handover 54/54 (incl. both S19 swap
pins and all pre-existing) + ladder/ingest regressions green (73 total);
tui live-events 5/5 (open-view-updates-itself by effect, zero-idle-polling
quiet window, reconnect, no-second-mechanism trace); daemon+tui tsc 0.
One RED-fixture fix (seedSeat return, wrong helper name), pin unchanged.
Pins: the oracle emits seat.activity_changed on transitions ONLY (identity
+ seq — notification, never derivation) and on swaps; GET
/api/activity/events streams filtered pushes as SSE with unsubscribe on
disconnect and no display/vocabulary fields in the payload; the handover
COMMIT declares the occupant swap to the oracle seat-keyed by node id with
the successor tenure identity, and a pre-commit failure never declares
(both in the ruling's named fixture, beside the ghost-stage pins); TUI:
subscribeActivityEvents drives the refresh owner — open view updates
itself by effect with ZERO manual refreshes, ZERO idle polling in a quiet
window, reconnect-after-drop as connection maintenance, and the
no-second-mechanism trace (main wires pushes to live.refresh; the module
carries no activity vocabulary and no setInterval).
live-events.ts shipped UNWIRED. Watched RED: events-stream 4/5 (the
nothing-emitted-on-non-transition pin passes vacuously at RED — declared),
handover 1/49 failing = exactly the swap pin (48 existing green — the deps
addition regresses nothing), tui live-events 5/5 failed.
node-inventory enrichment serves `activityState` (arbitrated activity +
needs-input + decidedBy + seq + lastSwap) with `display` pre-derived
through the ONE bridge (deriveDisplayActivity) — consumers render, never
re-arbitrate. Resolution by canonical session name via the new
getSeatStateBySession (state stays seat-nodeId-keyed). Honest ladder:
oracle-has-nothing => null; no service / partial injected double =>
undefined (the pre-taxonomy shape — NO FLAG DAY: terminalActive and
agentActivity behave exactly as before, proven by the untouched slice-15
enrichment suite).
cli ps: the ACTIVITY column renders the served display (needs-input as
count), legacy mapping survives only as the cited pre-taxonomy fallback.
tui hydrate: the served display decides needs_input/active/idle first; the
inline terminalActive x agentActivity mixing (the base RED's surface-local
arbitration specimen) survives only as the fallback rung, per
ladder-as-migration.
Receipts: A8 pins 5/5 (enrichment behavior + consumption trace); slice-15
enrichment + node-inventory suites 65/65 unchanged; daemon+cli tsc 0; tui
tsc 0.
Inventories live as ONE data source in activity-taxonomy.ts (claude: four
standing authoritative rungs; codex: hooks-at-TRIAL per AM-2 + sampling,
NO self-report — verified on-box; generic tmux floor for everything else).
readClaudeSelfReportEvidence (claude-code-adapter): sessions/<pid>.json
resolved by the file's canonical `name` field (no pane-pid plumbing),
SELF-DATED from statusUpdatedAt; busy→working, idle/shell→idle-at-prompt,
waiting→needs-input count+reason; every failure shape returns null — the
ladder falls, never errors (undocumented-internal discipline).
Wiring: the 1Hz sweep now (a) binds every running tmux seat and
auto-declares its runtime's inventory, (b) feeds the sampling rung from
pollSeat, (c) consults the injected selfReportReader for seats staffing
self-report; the hook-ingest route auto-declares on first evidence (and
after a swap cleared the inventory — successors start unpromoted).
needs-input chain: chrome > hooks > self-report. startup.ts wires the real
reader (CLAUDE_CONFIG_DIR-aware) — one flagged out-of-census line, same
class as the cli registration, noted on the row.
DECLARED: these pins were written WITH the wiring (not watched-RED) — the
contract layer carried its watched REDs in A3; A5 is declaration data +
reader + plumbing over the pinned ladder. 11 new pins green; ladder/
ingest/service suites 46/46; daemon tsc 0.
diagnoseSeatParked/diagnoseRigParked: (idle-at-prompt OR needs-input) x
open obligations, derived at read time; HELD (blocked) rows surfaced but
never park-driving; activity-unknown or missing-oracle => INDETERMINATE,
never a guessed NOT-PARKED; obligation scope NAMED verbatim in the result;
limit guard returns truncation-possible (a positive verdict stands —
truncation only undercounts); rig verdict aggregates any-parked and
propagates indeterminate (an unreadable seat is never an all-clear).
Surface: GET /api/activity/parked[?seat=] mounted under the EXISTING
activity group (server.ts untouched — census discipline); honest 503 on
missing deps, 404-with-known-set on an unknown seat; read-only, zero queue
writes, oracle non-inference intact (the join lives in the query).
CLI: `rig parked [seat] [--json]` renders verdict + per-input confidence +
bounded obligation list; unreachable daemon refuses honestly (live-derived,
never cached). Registered in cli index (one line — census amendment noted
on the row).
Receipts: domain 13/13 from RED d45c31b50; route 3/3; CLI verb 4/4 from
verb-absent RED; daemon tsc 0 errors.
diagnoseSeatParked/diagnoseRigParked declared UNWIRED with the full result
contract (named obligation scope, per-input confidence, limit guard, held
surfaced-never-counted). Pins: both park causes (dropped-baton idle;
needs-input with obligations); working/clean not parked; HELD negative
control; held+pending mixed; activity-unknown and missing-oracle →
INDETERMINATE never guessed NOT-PARKED; scope named and single-read;
limit-guard truncation honesty (parked stands — truncation only
undercounts); derived-at-read-time; rig-level any-seat aggregation with
indeterminate propagation (never a false all-clear). Watched RED: 13/13
failed at the query layer.
routes/activity.ts: after the store records (raw-event recorder, startup
proof — unchanged), the store-NORMALIZED state translates to ladder
evidence via evidenceFromHookActivity (one event-name parser, no twin):
running→working, idle→idle-at-prompt, needs_input→count+reason on the
hooks rung (never an activity value), unknown→null (noise is not
evidence). Per-source monotonic seq minted at ingest.
SeatActivityService: sources now track latest AND latest-ACTIVITY-BEARING
evidence per source — a mid-turn PermissionRequest must not erase what the
source last said about working/idle; needs-input arbitration reads chrome
first then hooks (authoritative only), un-time-bounded by design (an
unanswered block persisting IS the founder-observed park cause and must
stay visible; rationale in-line).
Receipts: A4 8/8 green from RED c8d819f5b (incl. the killed-relay
specimen: hook silence goes rung-stale, sampling decides, never
idle-forever); A1 11/11, A3 11/11, existing seat-activity-service suite
green (46 total). One fixture fix: injected clock stamps evidence times
(real wall-clock in the fixture defeated the expiry pins).
evidenceFromHookActivity exported UNWIRED (store-normalized state is the
input — ONE event-name parser, no twin). Pins: running/idle/needs_input/
unknown mapping (needs-input as count+reason, unknown = never evidence);
the route drives the arbitrated state through the seam (Stop → instant
idle); PermissionRequest-shaped needs-input beside a working activity,
cleared by the next turn boundary; the KILLED-RELAY specimen (supplement
risk): hook silence goes rung-stale and sampling decides — never
idle-forever; chrome outranks hooks-carried needs-input (passes at RED via
A3's chrome path — declared, the hooks half is the RED). Watched RED: 7/8
failed at the seam layer.
SeatActivityService gains the seat-keyed ladder engine, additive; the
non-inference contract is untouched (nothing reads queue state; the parked
join stays in the query surface). Mechanics:
- rawCandidate walks EVIDENCE_RUNG_RANK over authoritative rungs only;
hook evidence is time-bounded (HOOK_AUTHORITY_WINDOW_MS) — expired
evidence falls through silently, never errors.
- Per-source monotonic seq: stale/reordered reports dropped at ingest.
- needs-input rides beside activity from chrome evidence; chrome never
decides working/idle.
- AM-1: checkContradiction degrades a fresh-working hook against
sustained sampler idle beyond CROSS_RUNG_CONTRADICTION_WINDOW_MS to
identity-only with a visible RungHealthEvent (listeners + event bus).
- Debounce bookkeeping counts SAMPLING OBSERVATIONS (regardless of the
current decider) so pre-expiry idle ticks count toward the flip; the
hold applies only when sampling would decide it; turn boundaries and
chrome bypass instantly. Numbers recorded vs our 1Hz/3s cadence.
- AM-2: trial evidence measured against the authoritative rung's RAW
candidate value (evidence vs evidence, never the debounced display —
a held display would otherwise read agreement as disagreement);
promotion at the stated count over the stated window, visibly.
- Swap: clears evidence/trust/promotion/debounce, marks lastSwap, bumps
seq (a visible event), requires inventory re-declaration; undeclared
seats default to sampling-authoritative + identity-only elsewhere
(partial-coverage honesty by default).
- waitForSeatState resolves past afterSeq incl. transients; timeout
resolves null.
Receipts: A3 11/11 + A1 11/11 green from RED 3af23068c; existing
seat-activity/structural/endpoint/relay suites 31/31; structural+store+
codex-hooks+claude-hook-delivery suites green; daemon tsc 0 errors.
One fixture arithmetic fix in the promotion pin (N agreements span N-1
intervals), pin intent unchanged.
activity-taxonomy.ts: typed axes (activity working|idle-at-prompt|unknown;
session present|detached|exited|absent; resumability live|resumable|
context-walled), NeedsInput as count+reason, deriveDisplayActivity as the
ONE bridge to the addendum's human-facing four-value list (loud refusal on
non-taxonomy input so retired vocabularies cannot leak back through
display). The addendum/enum reconciliation is documented in the module
header and the doc's mapping section.
docs/reference/agent-state-taxonomy.md: cites the ratified addendum as
canonical (no fork), the axes table, derived diagnoses (PARKED/HELD/
DONE-UNSEEN with the slice-04 seam cross-cite), the herdr/omnigent
reconciliation table, the rung inventory r1-r4 with per-rung retirement
conditions (symmetric admission stated), seat-keyed swap semantics, and
the dated reject receipts (transcript-quiescence with the permitted
refocus consumer; pane-scraping fallback-only).
Watched: A1 suite 11/11 green from RED f6f01cd01.
Vocabulary-layer pins: activity enum exactly working|idle-at-prompt|unknown
(needs-input/attention/blocked/waiting banned as status values — the SPEC's
adversarially-held binding); unknown first-class; session and resumability
axes orthogonal; needs-input as count+reason with deriveDisplayActivity as
the only render bridge to the addendum's human-facing value (loud refusal
on non-taxonomy input); reference doc pins (cites the ratified addendum as
canonical, dated transcript-quiescence + pane-scraping reject receipts with
the permitted refocus consumer, herdr/omnigent reconciliation table,
PARKED-derived/HELD-deliberate).
RED enumeration at base 79da54dfc (the receipt these pins replace):
terminalActive boolean (SeatActivityService), agentActivity.state
running|needs_input|idle|unknown (AgentActivityStore — needs_input AS an
enum value), hydrate.ts:183-198 inline display arbitration mixing both,
attention_required lifecycle vocab. Skeleton shipped UNWIRED; watched RED
11/11 at the vocabulary/doc layer.
F1 (R2 blocking 1): `rig gateway human add` refuses a second DISTINCT human
BEFORE any parse or write — teaching names the configured human, the
hand-authoring path (with the honest-display advisory), and 0.5.7 as where
multi-human management lives. Same-id --replace unaffected. An unverifiable
registry fails closed.
F2 (R2 blocking 2): daemonQueueRows enumerates to EXHAUSTION — completeness
is proven only by a window strictly larger than the result (1000, growing
x4 up to 6 attempts); a board outgrowing every window returns an honest
completeness refusal that the remove guard surfaces as INDETERMINATE. The
500-cap silent truncation is gone.
Receipts: cli lifecycle 13/13 (F1 + both F2 pins now green over a REAL
HTTP stub honoring `limit`; 501 orphans incl. qitem-load-500), neighbors
green (cli 22 total, daemon 55 total), repo lint 0 errors, cli build clean.
Against R2 BLOCKING (28fbe70b, at f08c0eace): F1 pins the add verb as the
single-human boundary — second DISTINCT add refuses with teaching (existing
human named, hand-authoring + 0.5.7 pointed at), zero fragment bytes
written; companion pins same-id --replace stays allowed. F2 pins
enumeration-to-exhaustion at cap+1 over a REAL HTTP stub daemon honoring
`limit`: daemonQueueRows must return all 501 or refuse honestly, and the
end-to-end forced remove must record 501 orphans incl. qitem-load-500 (the
row the 500-cap silently dropped). Advisory-receipt seeding switched to the
hand-authoring path (registry surface) since the verb now refuses it.
daemonQueueRows exported unwired (behavior unchanged) so the pin fails at
the cap layer, not module resolution. Watched RED: 3 failed (F1 + both F2)
/ 10 passed; daemon fs-half 501-orphan receipt passes by design (the
truncation never lived in the registry layer) — declared, not RED.
OPR.0.5.5.12 against the AMENDED pair (SPEC 570e27254ba98794 /
PRD c7cda1cbac61e7ae, founder R5 single-human narrowing, desk amendment A1):
- listHumans: the singular surface — one configured human renders; several
hand-authored fragments render honestly PLUS the 0.5.7 advisory
(MULTI_HUMAN_ADVISORY; enumeration is display, never management);
teaching empty-state at zero (CLI).
- showHuman: effective record with authored-vs-default provenance per pref
(raw-fragment read beside the validated one), per-binding
inboundResolvable, fragment path.
- setHumanField: display-name / delivery-class / away / binding.<n> edited
on the RAW mapping (authored shape survives), then the FULL add-time
validator + the cross-fragment handle-uniqueness pre-write check; atomic
write + immediate re-projection; refusals leave fragment and projection
byte-unchanged.
- removeHumanFragment: refuses while caller-assembled in-flight items
exist (teaching enumerates each; --force archives — rename, bytes
preserved, collision-safe names — and records every orphaned item in a
reconciliation JSON); unknown-human refusal teaches the known set.
- pendingConversationsFor: un-Acked dispatch-buffer decisions matched by
documented entityBindingRef rule = the open-conversation guard input.
- parseBindingSpec: the --binding spec parse moved to the registry surface
— ONE source for add and set binding.<n> (CLI local copy removed).
- CLI verbs wired lazily (dep rail intact); remove's queue-row half is
injectable (HumanRowsLookup); an unreachable board is an INDETERMINATE
refusal that --force does not override.
Receipts: daemon slice suite 22/22; cli slice suite 9/9; neighbors
(registry/handle/add-verb/slack-inbound) green; repo lint exit 0.
Full-suite A/B at base 353f04ab2: daemon 24 fails = 19 pre-existing at
base + 5 contention-only; cli 4 fails identical at base; 0 attributable.
OPR.0.5.5.12 proof-contract pins, written to fail before the verbs exist:
verb receipts against a seeded registry; projection-integrity ABSENCE
(regenerator-shaped only, hand-edit still refused); validation parity
(bad enum / malformed binding leave fragment+projection byte-unchanged,
allowed set named); remove guard with silent-orphan ABSENCE (teaching
refusal enumerates conversations+rows; --force archives, orphan record
reconciles before/after); effective-record honesty (authored vs default).
Helpers ship UNWIRED (registry stubs return not-implemented; CLI deps
type only) so every pin RUNS and fails at the behavior/verb-absent layer,
never module-not-found. Verified RED: daemon 20/20 failed, cli 8/8 failed
("unknown command", "expected undefined to be defined").
The honesty release: classified transport resolution, delivery verification by effect, supported single-seat lifecycle controls, and state-preserving truth surfaces.
Bounded version bump across the release-owned files (root package.json,
packages/cli|daemon|ui/package.json, and the matching root+workspace entries
in package-lock.json). packages/tui remains at its independent 0.1.0.
/api/up: an import_rig failure with detail.code rig_name_running is a CONFLICT
(409), its code promoted top-level and the teaching message promoted to error
(flat path: stage detail carries the full outcome; pod path: errors[0] carries
the message and the detail carries only the code — both read). Direct
instantiation route (rigspec.ts) maps the same variant 409 per r2's
consistency requirement. CLI rig up: dedicated rig_name_running branch renders
the self-describing teaching refusal verbatim — no unknown-error fallback, no
validate-your-spec noise.
Failure layers verified from committed bytes: the route pin fails 'expected 500
to be 409' against the REAL /api/up with a seeded running same-name rig and a
real on-disk pod spec (control green: fresh name unaffected); the CLI pin fails
because the generic branch wraps the teaching text in 'Up failed ... validate
your spec' noise. Effect assertions (rig count unchanged, zero createSession)
ride the route pin.
The guard (running-name-guard.ts, shipped unwired in the RED commit) is now
called at all three create sites before any preflight/create/launch: the flat
instantiate, the pod YAML instantiate (the rig-up path where the 08-26
duplicate specimen occurred), and materializeValidatedSpec's create branch
(expand/add_member via targetRigId are deliberately unguarded — they never
mint a name). RUNNING derives exactly as the daemon already derives it
(>=1 sessions row status='running' through the rig's nodes). Stopped-generation
name reuse pinned unchanged on both paths; flat preflight's blunt name-exists
check still owns the all-stopped case downstream. Additive rig_name_running
variant on InstantiateOutcome per orch-lead ruling; MaterializeOutcome variant
in-file.
Honest failure characterization, verified from the committed bytes: the two pod
pins fail on ok:true (second instantiate SUCCEEDS against a running same-name
rig — materializeValidatedSpec create branch and the YAML rig-up path); the
flat pin fails as a wrong-code refusal (preflight's blunt name-exists check,
not the teaching rig_name_running refusal). Mechanism discovery recorded in
the control: the flat path was ALREADY existence-guarded by RigSpecPreflight
regardless of running state — the unguarded path is the pod path, where the
08-26 specimen occurred. Controls green: stopped-generation behavior pinned
unchanged on both paths; expand (targetRigId) unaffected; helper contract.
The envelope's sent/failed now derive from the encoded per-recipient
outcomes: a staged-unresolved recipient is not sent or delivered in ANY
field of any encoding. One RED extended on the existing fan-out JSON
negative (envelope.sent === 1 for one consumed + one staged-not-consumed;
raw transport sent:2 rejected), minimal GREEN, 139/139 + tsc 0.
F1 (desk-binding): a staged-unresolved recipient gets ONE verdict — the
staged report replaces 'sent' entirely (no qualified compromise), the
delivered count never includes it (human: 'N/M delivered, K staged-not-
consumed'; JSON: the recipient's row reads ok:false /
outcome:staged-not-consumed, and a staged-unresolved single-send envelope
carries verified:false / outcome:staged-not-consumed beside effectCheck).
A recipient can no longer be simultaneously delivered and staged.
F2 (desk-binding): identity runs BEFORE placeholder attribution, from ONE
source (StagedIdentity: the same expectedStagedText/line-count the guarded
submit verifies). The literal-residual match keeps daemon-precheck
normalization; a pasted-text placeholder binds only when its line count
matches this send's bytes (±2 render variance, never for one-line
payloads). An unrelated placeholder is UNVERIFIABLE — reported honestly,
never staged-as-this-send, and the guarded submit never fires on foreign
content.
RED-first: 4 RED at c6263a47a (captured pre-implementation), GREEN 139/139
cli focused + tsc 0.
F2 — classification before output encoding/routing: classifyDeliveryEffect
runs BEFORE any encoder. Local JSON carries effectCheck in the envelope
(staged-unresolved exits 1); fan-out classifies each delivered recipient by
pane effect (guarded submit's expected text is the bare payload, contained
in the daemon's per-recipient wrap) and renders per-recipient effect lines /
effectChecks JSON; the http cross-host leg reports the effect UNCHECKED
honestly in both encodings (the ssh leg relays --verify to the remote CLI,
which runs the check itself once it carries S3). No path prints sent or
delivered as an effect claim from transport return alone.
F3 — staged evidence isolated to the CURRENT input region FIRST (from the
last prompt-marker line): a stale pasted-text placeholder in scrollback is
history, never evidence, and the guarded submit cannot fire on it. Identity
matching reuses the daemon submit-precheck's own normalization (strip all
whitespace, contiguous containment) so detector-positive is precheck-
compatible by construction.
F4 — contract-only: proof item 4 amended (dated, in the live SPEC and its
item-exact PRD mirror, snapshots + before/after hashes in .evidence/) to an
explicit CUTOVER GATE: owner = release desk at 0.5.4 cutover; read-back =
teaching-site owners confirm against the running install.
RED-first: 4 RED at pristine aacbc3362 (which carries S3 v1 — the three
original PROOF tests pass there as pins, so the REDs are exactly the four
findings); GREEN 135/135 cli + 43/43 daemon transport-routes + tsc 0 at the
frozen sha.
Evidence leg, not a defect fix (no RED: the mechanism was already correct).
Discriminator: node created at the runtime default, set-model to a valid
non-default; the successor's EFFECTIVE footer (real codex TUI, isolated -L
socket) renders the post-set-model value verbatim — only the updated
nodes.model threading through the real launcher produces it. Predecessor
session row and prior tenures intact; +1 handover generation; audit event
durable. Also carries the r1-requested evidence-class note for the original
compile-absence RED leg (see PRD notes update).
Evidence-class note: the F3 RED commit staged the older session's liveness on
the wrong fake (outer vs local), so its red fired via the proceeded-at-all
branch rather than the fully-staged older-live world; this commit corrects the
fixture and the pin now models the exact older-live/newer-dead relation.
Three supported verbs close KI-5.3-9: RigRepository.setNodeModel is the first
nodes.model UPDATE (resume/successor launch read it at call time, lineage
untouched); stopSeat kills exactly the target's own tmux session then marks
exited + clears binding; cleanSeat returns a dead seat to launchable (the
reconciler leaves bindings on death and launchNode refuses already_bound
forever) while preserving node row, session history, resume tokens, and the
occupant-tenure ledger. One shared seat resolution (SeatStatusService
semantics); every refusal names what was checked; three additive RigEvent
audit variants per orch-lead territory ruling on row 18eff9cd.
'Sent' must mean CONSUMED, never merely typed. The transport's verify cannot
see text sitting AT the prompt (the send-staging class), so send --verify now
runs walk's staged-evidence primitive itself:
- detectStagedAtPrompt: one post-send capture; POSITIVE evidence only — the
payload head rendered in the input-box region (on/after the LAST prompt
line; scrollback never counts) or the pasted-text placeholder. It proves
STAGED; it never claims to prove consumed (capture absence is not evidence),
so a non-staged answer leaves the transport verdict unchanged.
- staged → 'Verified: no' + a report naming what was checked and observed,
then the remedy: ONE guarded Enter through the existing submit path
(submitOnly + expectedStagedText — types nothing, cannot double-deliver),
one recheck, then loud. One submit is the contract; a blind repeat delivery
is never performed or suggested.
- capture unavailable → the transport verdict is labeled transport-level only.
Tests: PROOF-1/2/3 at proof-item altitude; base-RED re-captured with the
FINAL test bytes at pristine 366642f43 (2 RED + pin, exit 1); GREEN 88/88
(send+broadcast+send-header) + cli tsc 0. S2 behavior untouched (43/43 daemon
transport-routes green at base; no daemon edits in this slice). Lore
retirement recorded cutover-gated (receipts in .evidence/).
Stub capture route + staged/consumed pane fixtures + send-body recorder;
PROOF-1 (staged detected by pane effect) and PROOF-3 (single-submit remedy,
no double delivery) RED at clean main 886c3f1d5; PROOF-2 (consumed verifies,
never reported staged) pins today's correct half. RED capture in .evidence/.
Founder-descoped shape: no stores, no schema, no refusals — honesty at the
two ends of the message.
- routes/transport.ts: the three 401 unattributable_sender sites are retired.
Enveloped fan-out delivers with the canonical unknown-sender marker as its
From: (derived at module load from wrapPaneEnvelope — no third literal
definition; the specimen-5 rule is preserved: a body-claimed envelopeSender
never renders). --dangerously-interact proceeds; the needs_input override
audit's already-nullable actorSession records null. Raw sends stay
byte-verbatim. Every unattributed delivery's response carries the sign-it
notice (composes with existing transport warnings; attributed sends see no
change).
- cli send.ts + broadcast.ts: the fan-out and broadcast renderers surface
additive warnings (Advisory line) instead of dropping them — an env-less
operator sees the notice, never 'sent' lines alone. Single-send already
rendered warnings (the OPR.0.4.3.28 Advisory surface, pinned by test).
- Tests: 4 PROOF tests + 2 P21-pin rewrites (daemon), 2 renderer tests (CLI).
Base-RED at pristine 5c34da73c: daemon 6/43, cli 2/68 (captured in
.evidence/, post-dating the final test set); GREEN at candidate 43/43 +
68/68; tsc 0 both packages. Terminal captures + refusal sweep in .evidence/.
Territory honored: only routes/transport.ts, send.ts, broadcast.ts + their
focused tests. S3 seam (sent-means-consumed) untouched.
Bounded repair per dev-guard NOT-CLEAR (row 9a5d47d6), no behavior change:
- HIGH-1: the classifier comment (adapters/tmux.ts) and the lifecycle
invariant comment no longer teach transport-absence as session-absence;
both now describe classification at the adapter and the Reconciler's
explicit cold-start election. Source-wide prose re-census found two more
stale sites beyond the handed list — the reconciler cold-start test
comment/title and transport test 9's title (its mocked throw is the
fail-closed class, not the no-server class) — both corrected.
- HIGH-2: the verdict-direction tests now assert the EFFECT: after an
injected blip the seat stays projected running (PsProjectionService),
and genuine absence down-ranks — both directions.
- EVIDENCE-1: .evidence/ERROR-TEXTS-captured.txt holds ACTUAL emitted
specimens (all five classes, both verbs, real transport over real
adapter). Template file removed.
- F-EV1 (r1): fresh base-RED for the amended 19-test set captured at a
pristine be23eb200 worktree — 13 RED / 6 pin-pass, matching r1's
measurement. Stale 18-test capture removed.
Affected matrix 216/216, tsc exit 0.
The adapter's hasSession() collapsed three distinct error classes to false,
so send/capture/nudge/walk reported 'Session not found' for transport blips
and the tmux_unavailable branches were dead code for the no-server class.
- TmuxAdapter.probeSession(): present / absent (positive tmux evidence) /
transport_unavailable (no-server, socket-gone), using the existing
classifiers; unexpected probe failures still throw fail-closed.
hasSession() keeps the collapsed view for the unbound consumer population
(mini-req 6 disposition) and derives from the same classification source.
- send/capture gates branch on the classification; both terminal texts name
what was actually checked and never imply an unestablished conclusion.
- recordSessionMissingVerdict is reachable only from positive absence — a
transport blip can no longer fabricate a durable absence verdict against
a live seat.
- Reconciler opts into transport-absence-as-detachable at its own call site
(boot reconciliation semantics preserved, now by explicit choice).
- RED set inverted from the diagnosis repro (row 2b986f35): 12 RED at
be23eb200 captured in .evidence/, 19/19 GREEN after; full daemon suite
delta vs pristine base = 0 regressions (A/B evidence in .evidence/).
The slice-09 packaging guard at scripts/check-cli-daemon-freshness.test.mjs
still asserted existence of vendored routes/rig-policy.js and registration
of rigPolicyRoutes + /api/rig-policy after the rig-policy -> rig-mode
rename (verb, API path, identifiers) landed in the 0.5.3 delta. Renames
the assertions to match the shipped route name, so the guard reflects the
current packaging contract.
Companion to the delta's rig-mode rename commit. No product code change.
The 'context release': rig context get/list/profile/recap-write. Address
grammar for context, ask-then-ref discovery, durable seat recap, refocus
resolves refs. Composition by reference, not by copy.
No new migrations (head stays at 071). No CLI surface removals or renames.
Bounded version bump across the release-owned files (root package.json,
packages/cli|daemon|ui/package.json, and the matching root+workspace entries
in package-lock.json). packages/tui remains at its independent 0.1.0.
One condition: the closure wait now runs for EVERY verifiable piece
including the last; --pace stays N-1. The reviewer's discriminator
flips RED->GREEN (final piece open -> exit 1 naming piece 2/2, no
success summary); the positive (final closed -> success) holds. walk
20/20, tsc clean.
PACING-C (reviewer's discriminator): final piece consumed but never
closed must fail loud naming piece 2/2 with NO success summary —
currently walk exits 0 and prints the full success line while the last
turn is open, which is exactly where rerun 4's seat-issued GET would be
queued. PACING-D (positive, green both sides): final piece consumed and
closed succeeds. Locked it.fails against exact 315bfe12e.
After a piece's distinct-turn consumption is verified, walk polls the
generation record until the system/turn_duration closure record appears
AFTER the piece's user turn, then paces to the next send. A closure-wait
timeout (--turn-timeout, --pace grammar, default 300s) fails loud naming
the piece — refusing to send into an open turn, where the runtime queues
the text (enqueue/attachment) and it never becomes a distinct user turn
(the rerun's proven defect layer). Generation roll during the wait fails
loud. Unverifiable mode skips the gate (its named advisory already
covers it). PACING-A/B flipped RED->GREEN; multi-piece green fixtures
updated to close their turns (the pre-gate shape modeled a seat that
never finishes reading). The honest consequence stands: a six-piece walk
takes as long as the seat needs to read six pieces. walk 18/18, tsc
clean.
The rerun's generation record proved the next defect layer: a piece sent
while the seat still processes the prior turn gets QUEUED by the runtime
(enqueue/attachment) and never becomes a distinct user turn —
consumption verification correctly failed closed but could not save it.
PACING-A: with piece 1 consumed and its turn never closing, walk must
fail loud naming the piece with piece 2 NEVER sent (currently proceeds).
PACING-B: with closure arriving after consumption, piece 2 goes only
after the system/turn_duration record is visible (currently sends before
it). Locked it.fails against b3b96517d; new --turn-timeout flag rides
the pins.
Both separately staged preserved pieces are exact (130=130, 82=82); the
sum must EQUAL the hidden source boundary, no tolerance. +139 accepts
once; +138 and +140 refuse with zero Enter (both pins flipped
RED->GREEN). Everything else unchanged. primitives 21/21, tsc clean.
Both separately staged preserved pieces exhibit exact equality (piece 2:
sum 130 = boundary 130; piece 3: sum 82 = boundary 82) — the ±1
tolerance was an invention the renderer evidence does not support.
Pinned in the reviewer's shapes against exact e7ece9542: the 139-boundary
suffix accepts once at +139 (green today), and +138/+140 are locked
it.fails to REFUSE with zero Enter calls (both currently accept).
The placeholder sum must equal the hidden source boundary immediately
before the matched literal suffix — computed from the piece bytes by
walking normalized line lengths from the end until they cover the
residual (specimen: residual covers lines 130..141, boundary 130, sum
130). ±1 tolerance for a mid-line chunk split. A matched suffix with a
non-matching sum is a truncated or wrong prefix and refuses with zero
Enter: both reviewer probes (+1 and +100 with the piece's exact 85-char
literal tail, boundary 139) flip RED->GREEN as refusals. The unjoined
upper bound is gone WITH the identity-free path it patched; the
preserved single-piece acceptance, coalesced refusal, and every earlier
gate are unchanged. primitives 18/18, tsc clean.
The preserved bytes prove the relation the candidate discards: the
placeholder sum (130) identifies the hidden source boundary immediately
before the visible suffix (the 524-char residual begins after exactly
130 of the piece's 142 source newlines). Pinned in the reviewer's two
probe shapes: a +1 and a +100 placeholder each followed by the piece's
EXACT literal suffix (a true 85-char tail) must refuse with zero Enter
calls — both currently return ok:true and press Enter. Locked it.fails
against the exact R4 candidate 816020cfe.
The acceptance relation is now exactly the rendering's proven structure:
placeholders are the piece's HEAD chunks and the literal residual must be
the piece's normalized SUFFIX, at least 48 normalized chars (the
preserved specimen's is 524), with the segment sum bounded above by the
piece's line count (5%+2 slack). Everything weaker fails closed with
zero Enter: no residual (PROBE-A — plausible size alone), a short shared
phrase (PROBE-B — 'What are your options?' occurs in the piece and no
longer blesses anything), a non-suffix residual (coalesced/foreign).
The R1/R2 bare-placeholder acceptance is superseded and its test now
pins the refusal. Both reviewer probes flipped RED->GREEN; the specimen
single-piece acceptance, current-input scoping, numbered-prompt refusal,
one-attempt, reconcile-by-effect, and terminal auth are unchanged. Pane
fixtures re-encoded without trailing blank lines (diff --check clean).
PROBE-A: a bare unrelated placeholder with a plausible segment size
(+100 of 142) currently passes the sum-only path — pinned to REFUSE with
zero Enter calls (no residual means no identity; fail closed). PROBE-B:
an unrelated small placeholder plus a short phrase that occurs in the
piece ('What are your options?') currently passes the weak-anchor path —
pinned to REFUSE with zero Enter calls (a common substring cannot bless
another input). Both locked it.fails against the exact R3 candidate
a64fa2b40, in the reviewer's probe shapes.
Claude renders ONE staged piece as MANY placeholders whose displayed
counts are SEGMENT sizes, followed by the piece's own literal tail
wrapped across pane lines (tokens wrap too). The identity relation is
now true of that rendering: within the current input region (last
❯-marker through the input box's closing separator — hint-bar chrome
excluded), wrapping is collapsed, placeholder tokens and known chrome
are stripped, and the literal residual must be a CONTIGUOUS substring of
the walked piece — the visible words are the piece's words, so a foreign
residual (another piece, stale content) refuses, which is exactly what
the preserved COALESCED region does for piece 2. Sanity: the segment sum
must not exceed the piece's own line count (5%+2 slack) and, with no
residual anchor, must reach 60% of it. Walk passes the FULL piece bytes
as expectedStagedText (a 200-char head cannot anchor a tail residual).
Preserved verbatim: current-input binding, numbered-prompt refusal,
zero-Enter-on-ambiguity, one-attempt, reconcile-by-effect, terminal
auth. The specimen pin (real capture + real served piece) flips
RED->GREEN; the R2 synthetic refusals survive under the new relation
(sum bound). primitives 14/14, daemon seams 142, walk 16/16, tsc clean.
Fixtures are the PRESERVED bytes, not synthetic: the t1-mechanics run's
direct tmux capture (piece 2 staged as EIGHT placeholders with segment
counts 16,15,19,15,17,15,17,16 — sum 130 vs 142 source entries — plus
the piece's own wrapped literal tail; placeholder tokens themselves wrap
across pane lines) and the served piece source (8834 bytes). RED pin:
that single-piece rendering must accept and submit exactly once with the
piece's true line count supplied. Guard (green both sides): the
preserved coalesced region (pieces 2 AND 3) refuses for piece 2 with
zero Enters — submitting would fuse two pieces into one message.
HIGH-1: staged evidence is now bound to the CURRENT ACTIVE INPUT — only
the pane's last ❯-marker line through pane end counts (everything above
is history); a numbered-option line is a prompt selection and refuses;
a pasted-text placeholder is identity-qualified by the walked piece's
own line count (+X must match ±1) and more than one placeholder is
coalesced staging (refused). Stale scrollback can no longer authorize an
Enter at a later interactive prompt — the reviewer's discriminator
refuses with zero Enter calls. The R1 placeholder-accept test is
amended to carry the line count (unqualified placeholder is not piece
identity, per the required repair). HIGH-2: the generation-record route
sits behind terminalAuthGuard (401/401/200 pinned; null-token loopback
passes), and walk's CLI half carries terminalAuthHeaders on every
generation-record/transport call (pinned structurally). Route passes
expectedStagedLineCount through; walk supplies it from the piece bytes.
One-attempt and reconcile-by-effect rules unchanged.
walk 16/16, primitives 12/12, touched-seam regression 161 green, tsc
clean both packages.
HIGH-1 discriminators (all zero-Enter): a stale pasted-text placeholder
above a LATER interactive prompt refuses; a placeholder whose +X line
count does not match the walked piece is not piece identity; multiple
placeholders at the current input is coalesced staging and refuses.
HIGH-2 pins: with a terminal bearer token configured the raw
generation-record read is 401/401/200 for missing/wrong/correct bearer
(null-token loopback passes through); CLI half — every walk
generation-record/transport call must carry the terminal auth headers
option. SendOpts gains expectedStagedLineCount (type only in this
commit). Locked it.fails against the R1 candidate 2668272f2.
Send success means TYPED, not CONSUMED — the voided mechanics run proved
pieces coalesce as staged text when the TUI never accepts the Enter. Walk
now verifies EVERY piece by effect before pacing to the next: it reads
the seat's current-generation record (new GET
/api/sessions/:name/generation-record — identity + byte-addressed suffix
via the ContextUsageStore sidecar, refusing loud when no record resolves)
and requires the piece's normalized head inside a user-role record. On
verification timeout it captures the pane once; staged evidence (the
piece's own head, 24 normalized chars, OR the TUI's pasted-text
placeholder — large pastes render as [Pasted text #N +X lines], never
content) takes EXACTLY ONE submit retry through the new transport
submitOnly mode: a bare C-m that types nothing and refuses
(staged_mismatch) unless the pane shows the expected staged content — a
bare Enter at a permission prompt would approve it, so the gate is
safe-by-construction. Then re-verify, then fail loud naming the piece. A
thrown client send (timeout) reconciles BY EFFECT — never a re-send (the
fleet ledger's rule, productized); a definitive submit_failed takes the
retry path; a generation roll mid-walk fails loud. Seats with no record
deliver legacy-style with a NAMED consumption-unverified advisory, and
the JSON summary carries consumptionVerified.
Pins W1-W4 flipped RED->GREEN. Fixture corrections disclosed: the
scripted generation-record fake now byte-addresses its suffix like the
real route (chars-vs-bytes desynced on multibyte content — kept as a
regression case); the submitOnly retry posts NO text field so the pins'
piece-send discriminator holds unchanged. New daemon suite covers
submitOnly (content-head accept, placeholder accept, permission-prompt
refuse, invalid modes) and the generation-record route (identity,
multibyte-safe suffix, loud 409s). walk 15/15, primitives 7/7, touched
seams 61+113 green, both packages tsc clean.
W1: typed-but-never-consumed fails loud naming the piece, next piece
never sent. W2: staged-text-detected takes exactly ONE bare-Enter submit
retry (submitOnly), never a piece re-send. W3: a client timeout with
server-side completion reconciles by effect — no blind re-send. W4: a
seat with no generation record still receives pieces, with a NAMED
consumption-unverified advisory. Locked it.fails against current walk
(which verifies nothing after send success); pins go RED on both the
absent verification path and the absent --consume-timeout/--consume-poll
tuning flags.
HIGH-1: isExternalUserInput classifies by the native causal fields —
user-role records with top-level isMeta:true or a sourceToolUseID (a
loaded skill body) and tool_result records are the assistant's own tool
cycle, never intervening input; a plain text/string-content user record
beyond the prompt still fails the case closed. HIGH-2: the done-signal is
the runtime's own turn-closure record (system/subtype:turn_duration)
following at least one assistant record — never a terminal stop_reason on
the newest assistant chunk (the runtime emits multiple terminal records
per turn: thinking end_turn then text end_turn 92ms later in the
specimen); the graded value is the assistant text + tool commands of the
records before the closure. Both discriminators specimen-traced from the
preserved aborted generation and flipped RED->GREEN; guard added (Skill
continuation valid AND a second actual prompt still fails closed).
Fixtures updated to append the closure record. Classifier validated
read-only against the full real specimen: case window at line 43 counts
exactly one external input (the prompt); Skill records 48-49 internal;
no closure in the aborted window so capture times out loud instead of
returning the truncated fragment. 43/43 across the four suites; tsc
clean.
Both shapes traced from the preserved real generation
(convergence-test-a-d99e44672 .../99-aborted-full-generation.jsonl), not
invented: HIGH-1 — a Skill load writes a user-role tool_result record
THEN a separate user-role TEXT record carrying top-level isMeta:true +
sourceToolUseID (the skill body); role+content-type classification counts
it as intervening input and voids a legitimate tool cycle. HIGH-2 — a
terminal turn is thinking(end_turn) then text(end_turn) 92ms later then
system/turn_duration; returning on the first terminal assistant record
yields an empty capture. Locked it.fails against the exact R9 candidate;
plus the r2-required guard (Skill continuation valid AND a second actual
prompt still fails closed) which is green on both sides.
captureSince now PARSES the current-generation record suffix as message
lines instead of string-slicing: the done-signal is the suffix's last
assistant message carrying a TERMINAL stop_reason (end_turn /
stop_sequence / max_tokens — never tool_use, never null), replacing
content-stability; the graded value is assistant text + tool_use command
blocks only — the user prompt and every envelope byte are excluded by
construction. PIN 4 (desk ruling 6fa281f1): any extra user-role TEXT
record entering the generation before turn completion fails the CASE
closed, loudly — tool_result records (role user by schema) are the
assistant's own tool cycle and never trip the gate. PIN 5: probe sends go
raw (send --raw <seat> <prompt>) so no From/To envelope or actionable
reply hint reaches the blank seat; still exactly one send per case. The
round-8 lifetime generation binding, append-only prefix check,
generation-change tripwire, and loud refusals are unchanged.
All four b4d8d1797+6fa281f1 pins flipped to hard GREEN; tool-cycle case
added (multi-step turn captured whole); legacy string fixtures in the
session/runner suites updated to the JSONL contract; custody argv pins
updated for raw sends. eval-rig-session 28, eval-rig-runner 4,
eval-rig-provider 6, eval-entry 2 — 40/40 green; daemon tsc clean.
PIN 4: any extra user-role TEXT record entering the generation between
prompt delivery and native-turn completion is an automatic case-INVALID,
named loudly — detection not prevention; a repeat costs one case, never a
run. PIN 5: probe delivery must not hand the blank seat an actionable
reply hint — sends go raw (envelope suppressed). Both locked it.fails
against the string-based captureSince; neither amends the frozen
criteria — they mechanically enforce the existing no-intervening-input
custody rule.
Harness-correction atom (door disposition qitem-20260825080716-ad2422ab; frozen criteria b0a426fc...).
dev-qa's real Test-A run (candidate d99e44672) was INDETERMINATE: the capture is NOT
Claude-JSONL-schema-aware — it treats the conversation record as an opaque string, so it graded the
user prompt/envelope (case-1) and returned 80-byte reply-footer fragments while the generation ran 9+
minutes past declared completion (cases 3/4).
These pin the two real defects against the CURRENT string-based captureSince (locked `it.fails`, flip to
hard failures the moment the fix makes them pass):
- OUTPUT-ONLY: the graded transcript must be assistant/tool TEXT only — no user prompt, no JSON message
envelope, no transport/reply envelope. Current capture returns the raw JSON slice.
- NATIVE-TURN COMPLETION: capture must NOT return on a footer-only / stop_reason:null fragment while the
assistant turn is still open. Current capture returns on mere content-stability.
- SUCCESSIVE-TURN SEPARATION is GREEN already: the per-case pre-send offset excludes an earlier
completed turn; the door's cross-case bleed was a symptom of the completion defect, not of separation.
The FIX (next): make captureSince parse the JSONL (messages {type,message:{role,model,content:
[{type:text,text}],stop_reason}}), extract assistant/tool output only for the current turn, and return
only after a terminal stop_reason — never a mid-turn footer fragment.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds the four missing rig-context verbs (get/list/profile/recap-write) with
the addressing grammar (required /<file> component), the profile-composed
seat:/mission: atoms as an authoring affordance, and the expertise-pack
snapshot caveat; extends rig proof add with the source-selection law
(contractSource in the echo); absorbs STOP 1 (compose by ref, nothing
catches the copy) and STOP 3 (owner boundary = pushes and PRs, genericized
per the durable sanitization ruling); header now names this delta. The three
already-present items (divergence trust, proof-add verb, context
show/sync/rm) are extended or left alone, never duplicated; no eval-harness
capability absorbed (landed-not-drivable dies with the delta).
review-r2 round-7 HIGH-1: the generation tripwire was scoped to one sendPrompt/captureSince pair, but
every sendPrompt OVERWROTE boundGenerationId from the latest sidecar — so a re-prime BETWEEN cases was
accepted as a fresh binding, letting one persistent Test-A run span two native generations while the
provider still reported its spawn-time generation. The fixed door requires ONE seat/session/generation
across baseline -> WALK -> GET -> post.
Fix: bind the record generation ONCE for the RigSeatSession lifetime (boundGenerationId: string | null,
null until the first case binds it). Every later sendPrompt re-reads the current record and, on a
generation change, FAILS LOUD before `rig send` — the binding is NEVER overwritten, so a mid-run
re-prime can no longer be silently accepted. The intra-observation tripwire in captureSince now checks
against that same lifetime binding.
Pinned two ways: a session-helper test (injected reader) and — as r2 required — an entry-level test
through the DEFAULT ContextUsageStore sidecar reader, both proving g1 -> g2 between cases emits ONLY the
first natural send. All prior constraints preserved (single-send custody, intra-prompt tripwire, loud
refusals, append-only slice, discriminator 169889b30, the wired runner entry).
eval-rig-runner 4/4 + eval-rig-session 22/22 + eval-rig-provider 6/6 GREEN; daemon tsc rc=0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review-r2 round-6 HIGH-1: the helper mechanics were sound but no production caller injected a
readGenerationRecord, so `run-evals.mjs --provider rig` refused before the first Test-A prompt — and the
fixed door requires the shipped live provider to run end to end. r2 ruled the wiring in-scope (not a
deferred live-drive concern).
Round 7:
- New test/helpers/eval-rig-runner.ts factors the runner's rig-provider construction into a testable
seam. `defaultRunnerGenerationReader` is the AUTHORITATIVE current-generation reader: it resolves the
seat's Claude conversation record from the status-line sidecar via ContextUsageStore.readAndNormalize
(sessionId = generation identity, transcriptPath = the append-only JSONL — the same record the daemon
reads; the read path is file-based off OPENRIG_HOME, so a throwaway in-memory db satisfies the
constructor without a live daemon db), then reads that JSONL. No record / unsupported runtime refuses
LOUD. `buildRigProviderSession` wires it into createRigCliSession (roll + prefix tripwires already
live in the session helper).
- run-evals.mjs `--provider rig` now builds the session via buildRigProviderSession, so the shipped
entry carries the reader end to end (no more unconditional refusal).
- New entry-level discriminator eval-rig-runner.test.ts exercises the PUBLIC runner seam: it submits
EXACTLY ONE natural prompt and captures the current-generation suffix; the default reader refuses
loud when no record resolves; and it resolves a real sidecar's sessionId + JSONL as the record.
All round-6 constraints preserved (single-send custody, generation-change tripwire, loud refusals,
r2's discriminator 169889b30). eval-rig-runner 3/3 + eval-rig-session 21/21 + eval-rig-provider 6/6
GREEN; daemon tsc rc=0. The non-author live drive now verifies frozen bytes rather than implementing them.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review-r2 round-5 HIGH-1 falsified the append-only premise: `rig transcript` is a
bounded-OVERWRITE pane snapshot, so the round-5 boundary could erase a re-emitted
command. Desk ruled Option B: bind the boundary to the seat's CURRENT-generation
append-only conversation record (the Claude generation JSONL), with binding
constraints.
Round 6 (helper mechanics, injectable seam):
- New `readGenerationRecord(seat) -> { generationId, content }` seam: the
current-generation append-only record, read OUT-OF-BAND (submits nothing to the
seat). The live entry backs it with contextUsageStore (sessionId = generation
identity, transcriptPath = the JSONL); unit tests inject a fake.
- sendPrompt binds the generation identity EXPLICITLY at bind time (constraint 1;
never path-guessing) and records its content, then submits EXACTLY ONE input —
the natural prompt (the frozen one-send custody contract preserved).
- captureSince polls the record to stability and returns the SUFFIX since the
pre-send content (an exact slice — append-only within a generation guarantees a
prefix; a non-prefix refuses loud).
- GENERATION-CHANGE TRIPWIRE (constraint 3): a generation roll mid-observation
(re-prime -> new JSONL) FAILS LOUD, never reads across the swap.
- LOUD REFUSAL (constraint 2): no reader wired, or an unsupported runtime / no
record (reader throws), refuses loud — never a silent degrade, never a fall-back
to the bounded pane. The bounded `rig transcript` path is gone.
- r2's bounded-rotation discriminator (169889b30) PRESERVED verbatim (constraint 4).
eval-rig-session 21/21 + eval-rig-provider 6/6 GREEN; daemon tsc rc=0. Constraint 5
(a public monotonic-cursor surface) banked for 0.5.4. The live reader wiring in
run-evals.mjs is the non-author's frozen-drive step (deferred as the live leg has
been; the helper refuses loud until it is wired — no unsound interim).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review-r2 round-5 NOT-CLEAR HIGH-1: the shipped `rig transcript` is a
BOUNDED-OVERWRITE pane snapshot (transcript-rotation.ts, default 1000 trailing
lines, atomic file replace each tick) — NOT append-only. So round-5's
transcript-suffix boundary is unsound: when an old identical command sits in the
bounded tail, LCS matches the current turn's re-emission to it and deletes
current-turn evidence (r2's discriminator returns "").
Locked as `it.fails` (permanent pin, flips to a hard failure the moment a
monotonic boundary makes the current command survive — forcing the .fails
removal at round-6). The single-send custody correction from c1d25289e stands;
this pins the remaining boundary-source defect for the replacement.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Test-A blocker 3, repair round 5 (custody transfer from dev50-driver; deposit
transition 8923). Round-4 solved capture-boundary identity with a unique
eval-sync nonce, but implemented it as a SECOND `rig send` before every natural
prompt. On the required claude-code seat that is an extra user turn — a forbidden
intervening input under Test-A's frozen no-intervening-input custody contract
(round-4 QA verdict NOT CLEAR, source row 2725c37f).
Round 5 uses the per-seat APPEND-ONLY transcript as the out-of-band boundary
(deposit): record the pre-send transcript (via `rig transcript <seat> --tail N`,
which submits nothing to the seat), send EXACTLY ONE input — the natural prompt —
then poll the transcript to stability and return the suffix since the pre-send
transcript. Because the transcript only appends (never scrolls/redraws like a
pane), an old identical command sits in the matched prefix and is never mistaken
for the current turn, so the marker is unnecessary and is removed entirely
(nonce option + eval-sync send deleted). The leading-echo strip stays the
provider's contract.
RED-first (reproducible at round-4 tip 8fd6cbe91 with only the CUSTODY test
applied): the CUSTODY pin asserts sendPrompt submits exactly ONE `rig send` (the
prompt), no eval-sync marker — RED against round-4 (2 sends), GREEN now. Also
pinned: the append-only boundary keeps a re-emitted identical command, and the
transcript stability/timeout/transient-failure paths.
eval-rig-session 17/17 + eval-rig-provider 6/6 GREEN; daemon tsc rc=0. (The
slice-07 buildProductionPackage/eval-production-refs tests fail here only for
lack of a built daemon dist in this fresh worktree — env, pre-existing at base,
they do not import these files.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The frozen door's live-harness gate required the documented `--provider rig`
entry to drive one real persistent seat end to end without a Test-A-private
source edit. New eval-rig-session drives ONE seat via supported rig CLI
surfaces only (up/whoami/send/capture/down): --seat attaches (never torn
down), --seat-spec spawns a scratch rig (torn down once). captureSince anchors
'since the prompt' across pane wrapping/truncation/scroll-off and waits for
pane stability; a stuck seat times out as ERROR. The provider fails the whole
RUN fast on a spawn failure (measured: per-case re-spawn churned six leaked
rigs) and dispose failures never discard the recorded grades. run-evals wires
the session behind the existing seam and teaches a loud refusal when neither
--seat nor --seat-spec is given. [NOT guard-accepted]
Perf fix 2 (row perf-process-census (c)): the operator measured "the 5m periodic
snapshot path adds x8 retry bursts." Investigating the mechanism, it is ALREADY
bounded on this main (835b700fc): the snapshot path (refresh with fillNullOnly)
passes attempts=1 to captureCodexThreadId (OPR.0.5.3.10 mini-req 2), and the
claude branch returns BEFORE probeClaudeResume (FR-4 rev1) so the 24- and
16-attempt probe loops are never reached on the recurring tick. The 8/24/16
retry loops live only in the non-periodic adoption/legacy paths.
This guard: a codex seat whose discovery FAILS on the snapshot path performs ONE
pane probe and ZERO retry sleeps — not the pre-fix x8 burst (8 probes + 7 sleeps).
A regression to the 8-attempt loop on the recurring tick flips these and fails.
GREEN on current main (the burst is bounded); locks it against regression.
Full resume-metadata-refresher suite 23/23, daemon tsc rc=0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Row perf-process-census (RELEASE-0.5.3 exclusive priority). Investigating the
reported O(unresolved-seats) `ps` amplification, I found it is ALREADY remediated
on this main (835b700fc) by OPR.0.5.3.10: checkOnce builds one poll-scoped census
(processCensus.cycleLister()) outside the seat loop and threads it to every seat's
readEffectiveModel; ProcessCensus memoizes one enumeration per cycle plus a
freshness window. What was missing was the coverage that pins the invariant.
This guard: 5 UNRESOLVED seats (never settle) drive exactly ONE process
enumeration per checkOnce pass (5 reads, 1 scan), and one scan per pass across
passes past the freshness window — never one scan per seat. A regression to
per-seat scanning would flip scans 1 -> 5 and fail this. Second case pins the
load-bearing wiring: checkOnce threads a cycle to every seat when a census is
present.
GREEN on current main (the fix is landed); this locks it against regression.
Full model-divergence-monitor suite 23/23, daemon tsc rc=0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adopt eight onboarding-walk parent files as the committed world/install static
pack (provenance + source hashes; founder->owner genericizations under class
ruling bc60ebab; eight internal-path references genericized per r2). The
handover catch-up is the SEAT-SCOPED RECAP per Q2-Amendment 1: the pack's
'recap' atom is an ADDRESS (seat:RECAP.md) joined from the seat tree at compose
time — HANDOVER = fresh walk + recap, POST-COMPACTION = measured re-prime +
recap, missing recap fails loud; the instance-bound HANDOVER-STEPS.md runbook
does NOT ship. Static packs project through generate-context-packs with
daemon-parser validation, version stamping, the FULL committed leak authority
(internal-tokens.generated.json, never a local subset), and cross-source
ref-collision rejection before any check-success or write. Per-piece sha256 on
the profile route (Test-A hash-exact gate). Tmux inline-send threshold
100KB->8KB: tmux's own inline ceiling (~9.4KB measured; live 'command too
long' at 19.8KB) binds far below the OS arg limit the old bound was sized
against. [NOT guard-accepted]
A scratch daemon launch strips inherited OPENRIG_DB and names an explicit
scratch DB under the scratch OPENRIG_HOME; an inherited fleet DB reaching
a scratch daemon is a harness defect regardless of outcome. Explicit
OPENRIG_DB still wins by design (daemon-db-path.ts, ruling 00c4ab76) -
the rule governs inheritance, not deliberate configuration.
pollAllRunningTmuxSeats now mirrors seat-structural-activity-service's this.sweeping
pattern: `if (this.sweeping) return; this.sweeping = true; try { ...sweep... }
finally { this.sweeping = false; }`. A new whole-fleet window-activity sweep never
starts while one is in flight, so a slowed/stuck tmux can never accumulate
overlapping sweeps — at most one sweep's worth is ever in flight.
Requirement-neutral: this suppresses OVERLAP only; a non-overlapping tick runs
unchanged, so cadence and activity-freshness semantics are untouched and neither
interval is raised. seat-activity 16/16, structural sibling 7/7, consumers 33/33,
daemon tsc rc=0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase-0 containment (row c6038f78): seat-activity-service.pollAllRunningTmuxSeats
has no single-flight guard, unlike its sibling seat-structural-activity-service
(MUST-FIX 2). Under a slowed/stuck tmux the per-tick scheduler can start a second
whole-fleet window-activity sweep while the first is still in flight, accumulating
overlapping captures.
RED shape: gate the per-seat read so the first sweep stays in flight, then fire a
second sweep — it must add ZERO reads (single-flight). Fails now ("expected 4 to
be 2"): the second sweep overlaps and issues a second full round of reads.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The founder-ungated graduation (rows bac070a1/8eead257/bc60ebab) executed end to end:
- context-engineering ships at specs/agents/shared/skills/process/ (edges [spec], category
process; ON-DEMAND only per the adoption ruling - never base-walk, never always-loaded).
BYTE PARITY MEASURED, not assumed: body byte-identical to the r1-frozen artifact
(fadb9bb2...8432); the shipped hash is 8c878bb1... because the RULED Layer-1 provenance
strip drops exactly 13 frontmatter lines (last_verified/distribution_scope/
source_evidence) - the designed pipeline, disclosed with the diff; all FIVE inline
perishable caveats and citations survive in the shipped bytes.
- The consolidated charged-term sanitization per the durable class ruling (bc60ebab): three
approved replacements (orchestration-team chooser law; openrig-user x2 - its own named
ship_after_fix sanitization) + two same-shape changelog attribution parentheticals
((founder, 2026-08-13) -> (mission owner, ...)) that blocked the all-or-nothing scan,
applied under the class ruling with mechanics/meaning unchanged. Canon founder-count 0
across the shipped set (measured with the scanner's own ship-set computation + stage
exclusions; feedback.md sidecars are excluded by design and untouched).
- The full 51-skill apply's deterministic output: regenerated control-plane JSONs, the three
genericized skills' shipped copies, and accumulated canon-drift regeneration for skills
whose canon moved since the last apply (attention-queue, watchdog, et al - small deltas,
one apply run, one authority).
Proof: mirror-skills --check exit 0 (two pre-existing stale-allowlist advisories);
mirror/leak-scanner/pack/leak-guard suites 46/0; generate-context-packs --check 37 packs
clean. Local only - no push, no PR, per the founder's clarification.
Founder ruling (08-24 19:34Z, mission NOTES R-5.3-authority-boundary): local
commits and ordinary implementation/apply work are NOT founder-gated — only
pushes and PRs require founder authorization. OPENRIG_SKILL_CANON_ROOT and the
authority-path env vars are MECHANICAL AUTHORING GUARDS (explicit-paths-required),
not founder authority. A message that overstates its own authority blocks work
the founder never meant to hold.
Comments/strings only — every guard MECHANIC is byte-identical (regen-edge-digests
is a verified no-op; mirror-skills --check rc=0; edge/mirror tests 5/5):
- scripts/mirror-skills.mjs: the apply error string now names the actual
requirement (set OPENRIG_SKILL_CANON_ROOT to the canon root — an explicit-path
authoring guard, not an authorization gate); the EXTERNAL_CANON_PENDING comment
reworded ("the canon mirror-apply, run with the explicit canon-root path").
- scripts/regen-edge-digests.mjs: "no founder authority YAMLs" -> "no
external-canon YAMLs"; "stays founder-gated" -> "requires the explicit
canon-root path (OPENRIG_SKILL_CANON_ROOT)".
- packages/daemon/scripts/gen-control-plane-json.mjs: same two reword classes.
Swept scripts/ + packages/ for the wording class; the remaining "founder"
references are unrelated and accurate (taste-gates, past authorizations, the
founder/PM lock, trust-domain ownership, recorded rulings) — left as-is.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
checkOnce now wraps each seat in try/catch (delegating to an extracted checkSeat):
a throw during one seat's check is reported as a detector error (loud warn naming
the seat) and the pass CONTINUES — every remaining pinned seat is still checked.
The throwing seat is not settled, so a transient throw self-heals next pass and a
persistent one keeps surfacing instead of ending the detector silently.
Pure isolation: checkSeat holds the exact prior per-seat logic (continue -> return);
all 21 existing monitor tests pass unchanged, daemon tsc rc=0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Row 3f66664a (defense-in-depth from the monitor-fix severity note, row bf850d1e):
at the blocked sha a single prototype-key pin threw inside modelsMatch and
silently ended the entire checkOnce pass — no per-seat try/catch. The null-proto
fix closed the live hazard; this pins the CLASS so any future per-seat throw
fails LOUDLY instead.
RED shape: two pinned seats, the first throws during its check; the diverging
seat AFTER it must still be checked and proclaimed, and the throwing seat must
be reported as a detector error (a loud warn). Fails now because the throw
aborts the whole for-loop.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The desk-ruled repair: V8a/V8b re-pin to the folded source-selection law (an absent or
section-less PRD yields to the fixture's authored README contract - qa-verdict lifts join,
doneVia populates, doneItems 13, no throw); V8f ADDS the true-absence case the old V8a
actually cared about (no PRD, no SPEC, no authored README contract -> pure tick-state, no
doneVia, no throw) - absence-never-throws inherited at the place it is still true; FS-1a
re-pins zero-proof-readdirs to true absence (stripping only the PRD no longer strips the
contract). The file no longer contradicts itself across the ruling change (V8d pinned the
new law; these pinned the old on the same fixtures).
RED evidence: the shipped state itself - 16/19 with exactly V8a/V8b/FS-1a red, matching the
desk's independent clean-worktree reproduction. Now 20/20; neighbors
scaffold-cross-reader + contract-source-selection green; daemon tsc clean.
Over the same ## Proof contract body, `rig proof add`'s parseProofContract and
the review composer's parseLogicalCheckboxes disagreed on what counts as an
item; a 1-based byIndex evidence ref then named a DIFFERENT promise on each
side — evidence silently mispaired to the wrong deliverable (verdict 407061c0).
One-grammar fix, twin-plus-parity preserved (no third private parser, no
cross-package dedup):
- New byte-equal twin packages/{cli/src/lib,daemon/src/domain}/scope/logical-checkbox.ts
holds parseLogicalCheckboxes + LogicalCheckboxItem; added to scope-audit-parity
CLASSIFIER_FILES so byte-equivalence is CI-enforced.
- compose.ts imports + re-exports from the twin (slice-detail-projector untouched);
the daemon parse is unchanged.
- proof.ts parseProofContract now uses the twin grammar (checkbox-only, deeper
continuations JOINED) — fixes CLASS 2 (split sub-bullet) and CLASS 3 (phantom
bare bullet). The per-item scaffold-placeholder skip runs on the CANONICAL
contract AFTER the pristine-scaffold second-face check (which needs the
unfiltered items) — fixes CLASS 1 (mixed placeholder+authored) so the byIndex
is not shifted onto a scaffold row. The private `/^\[.*\]$/` placeholder copy
is replaced by the shared isScaffoldPlaceholderText twin.
source:null (no ## Proof contract section) stays distinct from source:prd with
zero items (authored-empty) at the proof-add boundary.
GREEN: cli proof 27/27 (incl the 4 RED classes + control + source pins; the
existing second-face pristine tests preserved); daemon grammar consumers 191/191
(parity twin byte-equal, composer/cross-reader/scope-view/slice-detail/progress);
daemon tsc rc=0; changed cli files type-clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Pins the divergence review-r1 constructed in verdict 407061c0: over the SAME
## Proof contract body, `rig proof add`'s parseProofContract and the review
composer's parseLogicalCheckboxes disagree, and a byIndex evidence ref then
mispairs evidence to the wrong promise.
- CONTROL (agreeing): plain authored checkboxes — one item each (GREEN, non-vacuous).
- CLASS 2: a deeper sub-bullet must JOIN its parent (was a phantom 3rd item).
- CLASS 3: a bare non-checkbox bullet is NOT an item (checkbox-only).
- CLASS 1 (priority): a mixed placeholder+authored body drops the placeholder
per-item; the exact ONE-POSITION mispair is pinned BY VALUE — evidence "1"
covers the first REAL promise, not the scaffold placeholder it shifted onto.
- source:null (no ## Proof contract section) vs source:prd with zero items
(authored-empty) pinned at the proof-add boundary — distinct, not conflated.
4 RED (checkbox-only, CLASS 2, CLASS 3, CLASS 1); the existing second-face
pristine-scaffold tests stay GREEN (fix must preserve them).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Row e69daaef's production fix on the post-Slice-07 main (re-grounded on ec01785c5 per the
sequencing). The ONE selection home lands byte-identically in BOTH scaffold-placeholder twins
(the arch parity contract forbids deduplicating them - proof.ts therefore imports the CLI
twin, a DISCLOSED deviation from the row's subpath-export wording, with the twin header's own
ruling as the citation): authored PRD -> pristine PRD yields to authored SPEC -> authored
README -> null, mirroring NODE_FILE_PRECEDENCE. compose's extractProofContractSelected
delegates to it (3-arg, legacy 2-arg compatible) and exposes source; the projector captures
SPEC/README distinctly and threads them; plan-lock gains the spec param; proof.ts deletes its
private predicate copy (r1 F1, delete-not-align), reads all three documents, and gains the
missing README-node-file corner.
WIRING TRUTH found during the fix, on the record: compose/audit callers already pass the
resolved NODE FILE (SPEC-first precedence), so on the observed shape their BYTES agreed - the
function-level no-SPEC-path divergence, the label honesty, and proof-add's README corner are
what this closes. The audit's pickSectionSource is wiring-compatible by construction
(discriminator: legacy 2-arg call yields identical indices).
Proof: RED 11e0802a7 (function-absent discriminators), now contract-source-selection 7/7 +
scaffold-placeholder 25/25 + eval-behavior-bridge 5/5 + daemon tsc clean; cli proof 18/18 +
scope-audit 52/52 + parity 3/3 + cli tsc clean; twins byte-equal verified.
The exact observed shape pinned: pristine PRD + authored SPEC + no README must select the
SPEC at ONE home (selectProofContractSource, scaffold-placeholder territory) and compose's
DELIVERED pairing must AGREE with it - currently compose has no SPEC path and returns the
PRD placeholder on this shape (r1's confirmed A3, lines 483-485). Plus: authored-PRD
canonical control, the shipped README fallback preserved when no SPEC contract exists,
all-pristine degrading to a NAMED no-contract, and source exposure on the compose side.
Function-absent RED: 5 failed. Production fix + freeze re-grounds on post-Slice-07 main
(fold-freeze; compose.ts intersects that restack) so this does not manufacture a crossing.
The smallest truthful rule at the proof-add contract seam: a ## Proof contract whose items
are ALL still the template's [bracket placeholders] is a PRISTINE SCAFFOLD - it derives the
contract from SPEC.md's own authored ## Proof contract with a NAMED advisory (author the PRD
to make it canonical), else degrades to no-contract with the degrade named; a placeholder
index is never used. A genuinely authored PRD stays canonical even beside a SPEC contract -
the KI-5.3-2 first-face ruling preserved, pinned as the control. The echo carries
contractSource (prd|spec|null) so the composer and the operator see WHICH document indexed
the evidence.
Proof: RED 36ad0790a (both observed faces + the control's contractSource field), now proof
suite 18/18 + cli tsc clean.
Row ki532proofssot's both observed faces pinned: FACE 1 - scaffold PRD beside a locked
six-item SPEC must derive the SPEC's contract with a NAMED advisory (currently: the
bracket-placeholder becomes a plausible contractItemsDeclared=1 and evidences pair against
scaffold text); FACE 2 - scaffold PRD with no SPEC contract degrades to NO contract, never
the placeholder 1-item index; CONTROL - a genuinely authored PRD stays canonical even beside
a SPEC contract (the first-face ruling preserved). Run: 3 failed | 15 passed.
The restack onto slice-05 Q3 added the `behavior` EvalCategory to the shared live-model
harness. Behavior cases assert observable behavior and carry NO `rig context get <ref>`
contract, but the Repair-2 preflight ran resolveCaseRefs on EVERY case and refused the whole
run (run-evals.mjs exit 2) on any unresolved one — so a conforming behavior case could never
travel through the shipped eval entry. Latent at 35f7d8dea (pre-slice-05, no behavior cases);
activated by the restack, exactly the base interaction the re-review checked.
Fix: resolveCaseRefs stamps requiresRef (true only for selection/loading — the categories whose
contract pulls context); unresolvedCases enforces only on requiresRef cases. Behavior cases are
never refused. The hermetic guard applies canonicality only where the contract requires it.
RED evidence: mixed selection+behavior set failed (behavior-observable in unresolvedCases,
requiresRef undefined) → 2 failing; GREEN after fix, 46/46 eval+router+ref+behavior-bridge.
Pin at the actual entry/preflight seam (run-evals.mjs:55 composition), not a direct runEvals
unit: a mixed selection+behavior invocation proves the entry can never refuse a valid behavior
case while genuinely-broken selection cases still refuse.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Addresses review-r2's round-2 NOT-CLEAR (HIGH closed; two MEDIUMs).
MEDIUM-1 (standalone entry not runnable by either documented path): the file was mode 100644 (./ ->
permission denied) and `node run-evals.mjs` fails on Node 22 (TS parameter-properties in the imported
helpers). Ship a documented `eval` package command that supplies the tsx loader
(`npm run eval -w packages/daemon -- [args]`), make the entry executable (100755 — the shebang is
real), and correct the file + README usage. Pinned by eval-entry.test.ts. Verified: the documented
command runs (preflight + fake path, no ERR_UNSUPPORTED).
MEDIUM-2 (every preflight/test leaked its generated package into the OS temp root):
buildProductionPackage now returns a cleanup handle and registers a process-exit fail-safe; callers
reclaim eagerly via afterAll. Verified by effect: after the documented run + full suite, zero
eval-prod-pkg-* dirs remain. Pinned by a cleanup test.
Verified: eval 34/34, router-stub 6/6, daemon tsc rc=0, check-packing 5/5 (worktree root), mirror-check
rc=0, and the hermetic zero-residue discriminator green. No canon/stub change this round.
Addresses review-r2's re-review NOT-CLEAR on candidate d0faa5f80.
HIGH-1 (production resolution was outside the eval + rode gitignored residue): a shared per-case
validator (eval-ref-resolution.ts) now backs BOTH the guard test and a run-evals PREFLIGHT (every
provider, not just rig). Refs resolve against the EXACT production package BUILT into a temp dir via
generate-context-packs.mjs — never the gitignored packages/daemon/context-packs residue. Per-case: a
case with no/absent/non-canonical ref refuses the run by name. Verified with zero context-packs residue
present (eval 31/31, run-evals preflight, router-stub 6/6, all green detached).
MEDIUM-1 (stale/false instructions): README + generate-eval-fixtures.mjs no longer claim the live
provider points OPENRIG_CONTEXT_PACKS_ROOT at the fixtures (the run resolves against production). The
router stub no longer claims `rig context list` prints each entry's "when": list gives ref/name, and
the when comes from the index or `rig context list --json` purpose. Canon + projection byte-identical;
edge digest regenerated; mirror-skills --check clean.
Verified: eval 31/31, router-stub 6/6, daemon tsc rc=0, check-packing 5/5 (worktree root), mirror-check
rc=0, and the hermetic zero-residue discriminator passes. Founder-apply-gated canon edit preserved to
proof/canon-edit-openrig-skills-PENDING-FOUNDER-APPLY-20260824.diff.
REPAIR 1 (router-stub teaching): the stub taught `rig context get <name>` but never bridged ask->ref,
so the live natural-prompt door could not carry a seat from prompt to entry. The pull section now
teaches ask -> ref -> load: `rig context list` to discover every ref and its "when", select the
canonical full-path ref (skills/<ns>/<name>), then `rig context get skills/<ns>/<name>`. No alias
layer. Canon and projection edited byte-identical; edge digest regenerated; mirror-skills --check clean.
REPAIR 2 (fixture alignment): eval cases and the fixture generator used bare refs (core/x) that fail
exact lookup against the production layout. Converted every ref to canonical skills/<ns>/<name>, and
the eval now resolves against the EXACT production package (run-evals + RigSeatProvider point at the
packaged builtin library, not a hand-seeded fixture root) so fixture-vs-production drift fails
structurally. New guard eval-production-refs.test.ts pins every case ref canonical AND resolving in
production.
RED-first both (router-stub +2 pins; eval-production-refs 3 pins). Verified: eval 29/29, router-stub
6/6, daemon tsc rc=0, check-packing 5/5 (worktree root), mirror-skills --check clean. The canonical-
source edit is uncommitted and founder-apply-gated; preserved to
proof/canon-edit-openrig-skills-PENDING-FOUNDER-APPLY-20260824.diff.
Row 782b467a's two deliverables (desk-ruled scope):
D1: RigSeatProvider gains SESSION-PERSISTENT mode behind the same EvalProvider seam - lazy
single spawn reused across baseline/WALK/GET/post (persistence proven by one-spawn +
generation identity), the INPUT-ECHO contamination control (only the LEADING prompt echo is
stripped so a grader cannot pass on the prompt's own text, while a seat's genuine later
quotation is kept), idempotent dispose with loud refusal after, and the R6 not-wired refusal
preserved verbatim on the legacy path (no false green before the non-author's live wiring).
D2: walk --through-profile consumes the AUTHORITATIVE composed profile: exactly one input
form (mixing rejects before any send), pieces labeled by identity, NO-COPY (bytes sent ===
bytes served, pinned), the --json report carries delivered identity EXACT-equal to the
profile list, and a mid-walk failure reports the delivered PREFIX vs expected by identity
with the aborted atom named. The frozen criteria artifact untouched; no live run.
Proof: RED c9716a78e (provider 4 red + walk 4 red with the legacy-refusal control), now
provider 5/5 + walk-profile/fork/grammar 12/12, both tsc clean.
Row 782b467a's two deliverables pinned before implementation: (D1) RigSeatProvider session
mode - one spawn across baseline/WALK/GET/post, the input-echo contamination control (leading
prompt echo stripped so a grader pattern matching only prompt text cannot pass, while a
genuine later quotation is KEPT), idempotent retirement with refusal after, and the R6
not-wired refusal preserved on the legacy path (no false green). (D2) walk --through-profile
consumes the AUTHORITATIVE composed profile: delivered report BY IDENTITY equals the
profile's piece list in order (never hand-authored); NO-COPY (bytes sent === bytes served);
mid-walk failure reports the delivered PREFIX vs expected by identity; mixing input forms
rejects before any send. Run: provider 4 red / 1 pass (the legacy-refusal control), walk 4 red.
r1's bb00e850 observation productized per row 17015088: the seat-ref parse goes through the
CANONICAL parseSessionName (first-@ split, the documented greedy-rig ruling) in a named,
tested function ONE-HOMED beside the store it reads; the seat route thins to a config-read +
call. Safety floor preserved and pinned: non-canonical parse verdicts (external/legacy/
malformed) are labeled absences naming the input and verdict - never a throw, never a guess
(fuzzy-matching another seat's directory would hand a successor a DIFFERENT occupant's
decisions). Host-qualified refs now degrade with the CANONICAL path in the label.
PROFILE-ROUTE AUDIT (the row's second half): rig/seat/mission arrive as SEPARATE pre-split
query params gated by the bounded-segment regex - no ref parsing occurs there, no second
parser exists; audit finding, no code change.
Proof: RED 180bdabf2 (host-qualified discriminator + local control + malformed labels), now
store/packet 21/21 + daemon tsc clean.
r1's bb00e850 observation as pins (orch-lead row 17015088): the host-qualified three-part
discriminator (canonical first-@ split reads member=s1 rig=r1@host-x - greedy-rig BY DESIGN -
and the labeled absence must carry THAT path, where lastIndexOf read seat=s1@r1); the
two-part local control; malformed refs as labeled absences naming the input, never a throw,
never a guess. Module-function-absent RED: 3 failed | 9 passed.
The Q2 boundary requirement productized at the named caller seam: buildRestorePacket renders
the AUTHORED recap as a third leg - a POINTER to seat:RECAP.md with chain depth (no-copy
composition: the packet never inlines the bytes; the successor composes by address via the
handover profile), labeled absence per the B16 doctrine, backward-compatible when omitted.
SeatHandoverService gains an injected authoredRecapResolver threaded into the fresh-path
delivery; the seat route wires it from topology.root config + the ONE store (present with
chain depth, or a named absence CARRYING THE PATH TRIED - a seat-id/directory mapping gap
surfaces loudly at the door drive, never silently). rig context recap-write: the outgoing
occupant's verb - seat dir from config, the write through the store (supersession observable
through the verb), advisory contract findings on stderr while the write lands, the
addressability gate refusing loud with nothing left behind. New daemon subpath export
./seat-recap-store (the surface pattern beside crash-cart et al). Grammar pin +recap-write.
Packet-pin verb corrected pre-green and disclosed: my RED asserted 'rig context get' as the
pull, but get is library-only - the honest pull is the handover profile compose.
Proof: RED b20f28d62 (packet 2 red + verb 2 red), now packet/store 18/18 + daemon tsc clean;
cli recap-write/grammar/fork 10/10 + cli tsc clean.
The Q2 boundary requirement the store alone does not satisfy: buildRestorePacket gains the
AUTHORED recap as a third leg beside the from-record recap - the successor is pointed at the
ADDRESS (seat:RECAP.md, no-copy composition, with 'rig context get' as the pull) with chain
depth named; absence is a labeled B16 line, never silence; omitted-entirely stays
backward-compatible. rig context recap-write: seat dir from topology.root config
(daemon-independent like trace), the write through the ONE store (supersession observable
through the verb), advisory findings on stderr while the write still lands (never gated on
prose), the addressability gate refusing loud with nothing left behind. Grammar pin
deliberately +recap-write. Run: packet 2 red, verb 2 red.
Mini-req 8 per the amended lock: 'behavior' joins the harness's category enum (schema +
grader union; order stays loading-only — the selection-only rule generalized to
non-loading). The atom probe shape reconciles with EvalCase: expectedPatterns (compilable,
validated at manifest ingest) and rubric become LEGAL closed-set probe keys — the
dispositioned Atom-2 key-gate note lands at its recorded spot, and r1's predicted natural
mistake (rubric on a probe) becomes the schema instead of silent loss.
compileAtomProbesToEvalCases emits plain case DATA the harness validates through its own
schema at load (decoupled by data like the YAML cases; no product-imports-test tangle, no
second runner). A probe without expectedPatterns has no deterministic-door leg: SKIPPED WITH
A NAMED REASON, never an invalid case, never silent (the no-silent-caps rule).
The one-invocation door shape is pinned: loadEvalCasesFromDir + runEvals over a mixed dir
grades selection AND behavior in one run with per-category recorded summary.
Proof: RED e3d3bc406 (module+category absent, disclosed), now bridge/eval/atom sweep 55/55 +
daemon tsc clean.
Module-absent RED (disclosed; the bridge module and the behavior category both do not exist):
the harness schema admits category 'behavior' (order stays loading-only); the atom probe
shape reconciles with EvalCase - expectedPatterns (compilable) and rubric become LEGAL
closed-set probe keys, landing the dispositioned Atom-2 key-gate note exactly where its
recorded landing spot said; compileAtomProbesToEvalCases emits case DATA the harness's own
schema validates (decoupled by data like the YAML cases - no product-imports-test tangle, no
second runner); and the door's shape: loadEvalCasesFromDir + runEvals over a mixed dir grades
selection AND behavior in ONE invocation with per-category recorded summary.
F1: supersession names disambiguate with a counter suffix (RECAP-<ms>-N.md) instead of
renaming onto an existing path — nothing is lost AND the boundary write still succeeds
(better than throwing on both counts); listRecapChain admits the suffix and sorts by
(ms, sequence). F2: OPENRIG_RUNTIME maps the PRODUCT vocabulary explicitly (claude-code ->
claude, codex -> codex; bare claude kept for hand-set environments); a genuinely unknown
value falls back to claude WITH a stderr warning naming itself - the fallback no longer
silently carries every claude seat, and a future third runtime speaks instead of composing
the wrong profile.
Proof: RED d60d21d61 (r1's constructed same-ms and claude-code shapes + their bonus
gate-leaves-state-untouched property), now seat-recap-store 9/9 + daemon tsc clean; cli
fork/grammar/context 39/39 + cli tsc clean.
r1's constructed shapes (verdict row 84070180): F1 - clock 5000->7777->7777 must keep BOTH
predecessors byte-preserved (renameSync onto the existing name silently replaced v1 - the
retention contract inverted); F2 - OPENRIG_RUNTIME=claude-code (the product's real adapter
vocabulary, live on r1's own seat) must map EXPLICITLY to claude, and a genuinely unknown
value falls back WITH A VOICE naming itself (their point 3: a future third runtime must not
silently get a claude profile - my original pin only exercised the matching codex value, so
the dead half was invisible). Plus the bonus property r1 verified and asked pinned: a write
failing the addressability gate leaves the current recap AND the chain untouched.
writeSeatRecap: the authored recap (Q2 - extends the shipped from-record boot recap's name)
lives seat-homed as RECAP.md beside LEARNED (position knowledge, Q2-Amendment 1(a));
supersession moves the current recap byte-preserved into recap-superseded/ under the seat
directory (1(b): seat-directory lifecycle owns cleanup, no librarian). The ONE hard gate is
ADDRESSABILITY (an unaddressable recap would fail every handover profile downstream,
silently late); the authoring contract validates ADVISORY on its checkable subset only
(no-decisions-section; nonstandard UNVERIFIED marker) - findings never gate the boundary,
and the uncheckable halves (temporal order, derived-values-as-commands) are not pretended at.
r1's 4d observations: the escape marker rides the profile verb's stdout FRAMING header
([seat !ESCAPED-ROOT] - self-describing payload, zero composed bytes touched; the older
labels pin updated deliberately with it); --runtime defaults from OPENRIG_RUNTIME (flag
beats env; unrecognized env falls back claude) closing the silent-claude-profile hazard.
Proof: RED 1eb542990 (store module-absent + both obs pins red), now seat-recap-store 7/7 +
daemon tsc clean; cli fork/grammar/context 39/39 + cli tsc clean.
Module-absent RED for the store (disclosed): writeSeatRecap supersedes the current RECAP.md
into a byte-preserved chain UNDER the seat directory (seat-directory lifecycle owns cleanup,
Q2-Amendment 1(b)); the one non-advisory gate is ADDRESSABILITY (a recap that cannot resolve
would fail every handover profile downstream, silently late) - the authoring contract itself
validates ADVISORY on its CHECKABLE subset (no-decisions-section; nonstandard UNVERIFIED
marker), findings never throw, the boundary is never blocked on prose shape. Plus r1's two
4d observations RED at the CLI: the escape marker rides the stdout FRAMING header (payload
self-describing, zero composed bytes touched); --runtime defaults from OPENRIG_RUNTIME with
the flag beating the env (the silent-claude-profile hazard, mini-req 3).
rig context profile <ref> --situation [--runtime --budget --rig --seat --mission --json]:
serving only - pieces to stdout with [source] labels (Q2-Amendment 1), budget report +
provenance warnings to stderr so stdout stays the composed walk an agent consumes. Mission
root wired per the desk ruling (row 2675535d): workspace.slices_root/<mission>, same
bounded-segment gate, same grant semantics, NO new config key. The borrowed no-nested-packs
invariant is now NAMED in the route comment with its pin cross-referenced (r1 4c rec 1).
ATOM-7 grammar pin deliberately updated twice-over, disclosed: 'get' (repairs the
pre-existing baseline red on orch-lead's ledger - the pin was never updated at slice-07 R1)
and 'profile'; both delivery-free, the delivery-seam exclusions still hold.
Proof: RED ae598a69f (mission-root + profile-verb pins red; rec pins green-by-design as
standing-invariant pins, r1's explicit ask), now daemon 41/41 + tsc clean, cli fork/grammar/
context suites 37/37.
Two rec pins land GREEN by design (they pin standing invariants against future change, r1's
explicit ask): no-nested-packs (the longest-prefix split's borrowed invariant, now asserted)
and the CLI '#' routing fork (the one logic that lives only in the CLI). Two pins RED:
mission: atoms resolve from workspace.slices_root/<mission> per the desk ruling 2675535d
(absent param stays a loud missing-config); the rig context profile verb (grant params
threaded, [source] labels on stdout, budget + provenance warnings on stderr). Plus the
ATOM-7 grammar pin deliberately updated twice-over, DISCLOSED: 'get' (slice-07 R1, the pin
was never updated - the pre-existing baseline red on orch-lead's ledger, repaired here on
purpose) and 'profile' (this atom), both delivery-free library verbs.
Q4's ruling productized at the pull verb: an argument carrying '#' routes to the daemon's
single resolver home (GET /library/resolve-address); stdout is exactly the resolved span
bytes so an agent consumes the addressed section and nothing else; --json passes the full
resolution (packRef, filePath, headerPath, headerLine, text, ownText). Fail-loud passthrough:
the daemon names every failure, the verb adds nothing. Pack-level get (no '#') is
byte-unchanged.
Evidence: cli suite = EXACT pristine-main baseline (3 red files / 5 red tests, identical
with and without this delta - discriminated against the fully-built cut worktree at
01f2709d9; the baseline reds are pre-existing and on orch-lead's ledger). Daemon side at
76b365216: address-route 5/5 + markdown-address 15/15 + neighbors, tsc clean.
GET /library/resolve-address?address=<pack-ref>/<file>[#H2[/H3]]: longest-prefix pack match
against the library index (the index decides the pack/file split, never a guess), file
resolution through the service's containment-checked resolveFileWithinPack, span resolution
through the Atom-1 machinery (Q1 full span, fence-protected, no-match names candidates).
Bare file address serves the whole file; every failure is a NAMED 4xx (invalid address 400,
pack/file not found 404 naming what was tried, unresolved/unreadable 422). The addressable
unit is the FILE per the locked grammar - the assembled bundle is never a target (its
'## File:' frames are themselves H2s; addressing it would collide by construction).
Plus the Atom-1 ambiguity guard: resolveAddress now fails LOUD when multiple sections share
the requested path, naming every header line - serving must not depend on the validator
having run.
Proof: RED 4325a68de (6 pins: 5 route shapes + the ambiguity guard), now address-route 5/5 +
markdown-address 15/15 + profile-route 10/10 + neighbor routes 24/24 (54 total), tsc clean.
Pins the mini-req 6 door shapes at the daemon resolver home: GET /library/resolve-address
does longest-prefix pack matching + file-within-pack + span (Q1 full span, fenced headers
never resolve, no-match names candidates, bare file address = whole file, unknown pack/file
404 naming what was tried). The addressable unit is the FILE per the locked grammar - the
assembled bundle is NOT a target (its '## File:' frames are themselves H2s; addressing it
would collide by construction). Plus the Atom-1 ambiguity guard: duplicate header-paths must
fail loud naming both lines at RESOLVE time, never serve the first match (the serving path
must not depend on the validator having run). Route-absent RED + guard-absent RED.
r1's one-line repair as specified: relFromBase === '..' || startsWith('..'+sep) ||
isAbsolute. A file named '..hidden-notes.md' inside its root no longer self-refutes the
warning channel; the trust surface stays believable. Proof: RED af1bd05f7 (r1's measured
shape), now profile-route 10/10 + resolver 6/6, tsc clean.
r1's measured shape (round-2 verdict 331b6ebd): relative() returned '..hidden-notes.md' and
the bare startsWith('..') read an in-root file as escaping - a self-refuting warning on the
exact bug class r1 pre-flagged, reproduced one level in. Run: 1 failed.
r1's 4a riders + their three pre-review corrections, built in RED-first (RED f1260d6ce):
RIDER 1: makeProfileReadFile reports a SourceReadRecord per successful read (realpath AFTER
the read so its throw can never garble the honest read error); the route attaches
{nominalPath, realPath, escapesRoot} to every piece and emits provenanceWarnings — ALWAYS an
array, empty = clean — naming any piece whose real bytes came from outside its root. Report,
never block (realpath containment would break the product's own symlinked layouts).
Containment computed via path.relative against the realpath'd base — never a bare string
prefix (r1: startsWith says /seat-evil is inside /seat).
RIDER 2: the rig/seat params documented and pinned as the caller's EXPLICIT GRANT of read
access to that seat DIRECTORY SUBTREE (r1's precise wording — the pack chooses paths within
a granted root); same pack + same atoms: no grant = 422 naming the missing root, grant =
the read happens and is visible in provenance.
Plus: a DANGLING symlink is its own named failure (lstat-discriminated from genuinely
absent); test fixture compares realpaths (macOS /var -> /private/var).
Proof: RED f1260d6ce (rider pins failing on the frozen 6f7debf7d), now profile-route 9/9 +
resolver 6/6 + neighbor routes 24/24, daemon tsc clean.
r1 CLEARed 4a with two riders routed to THIS atom (verdict row db91c0cd; they probed the
symlink disclosure to a live out-of-root secret): RIDER 1 - with a symlink in play the source
label is accurate about the REF and wrong about the BYTES, a provenance defect in a contract
about provenance that bites on benign symlinks; pins: per-piece provenance
{nominalPath, realPath, escapesRoot} + profile-level provenanceWarnings naming escaping
pieces, report-not-block (realpath containment would break the product's own symlinked
layouts, r1's own reasoning). RIDER 2 - URL-installed packs may carry seat: atoms by design;
pins the trust boundary END-TO-END: no rig/seat params = 422 naming the missing root, the
explicit grant reads exactly the named seat and the read is visible in provenance.
Run: 2 failed | 5 passed.
GET /library/by-ref/profile?ref=&situation=&runtime=[&budget=][&rig=&seat=]: the pack's
manifest flows through the ONE parser chokepoint; the seat root resolves from topology.root
CONFIG (slice-06 D1 layout rigs/<rig>/seats/<seat>, never a literal); rig/seat are validated
as bounded single segments so a query string cannot walk the topology tree; composeProfile
runs with the multi-source readFile and prefix-derived source labels; every compose failure
is a NAMED 4xx (invalid situation/runtime/budget/seat-params 400, unreadable manifest /
no-atoms / compose failures 422) - a profile is never quietly thinner than its graph says.
Proof: RED e543c65ed (5 route-absent pins incl. the seat-tree recap-by-address door shape),
now profile-route 5/5 + neighbor routes 24/24 (29 total) + daemon tsc clean.
Route-absent RED (disclosed): GET /library/by-ref/profile composes handover with a
SEAT-TREE-homed recap assembled by address and source-labeled (the ATOM METADATA LIVE door's
amended shape); fresh needs no tree params when no tree atom selects; a tree atom without
rig/seat fails LOUD naming the missing config (never a thinned walk); budget overage reports
with nothing truncated; unknown situation / atom-less pack / unknown ref are NAMED 4xx.
The Q2-Amendment grammar ruling productized: parseSourceRef splits the pre-# ref into its
resolver kind (bare = library, seat:/mission: = tree) with fail-loud unknown-prefix and
traversal rejection (r2-B3 clean-refusal precedent); makeProfileReadFile dispatches library
refs to the pack dir and tree refs to CONFIGURED roots (CE-v2 03-tree-addressability: from
config, never literals - a missing root is a loud missing-config error naming the kind, never
a silent empty; this module holds no path literal); sourceKindForAddress feeds the composer's
per-piece labels from the address prefix. Ingest rule scoped (pin first, RED 195c145ab): the
declared-file check applies to LIBRARY refs only - a seat-homed recap atom (nothing must be
library-homed to be composable, Q2-Amendment 1(c)) is legal at ingest, while traversal and
the markdown-header-suffix rules still bind on tree refs. Deep-chain test timeout sized from
the measured loaded case (12s at host load 70).
Proof: RED 2106a2bfa (resolver pins, module-absent form disclosed) + RED 195c145ab (ingest
pin failing against the declared-file check), now 43/43 across the four slice suites +
daemon tsc clean.
A seat-homed recap atom (Q2-Amendment 1(c): nothing must be library-homed to be composable)
declares a seat: address whose file lives on the seat tree, not in the pack — the
declared-file check must scope to LIBRARY refs. Currently rejects: 1 failed.
Pins the Q2-Amendment grammar ruling before the resolver exists (module-absent RED,
disclosed): bare refs are library, seat:/mission: prefixes select the tree resolvers with the
SAME #-grammar after the ref; unknown prefixes and traversal-shaped refs fail loud; the
readFile dispatcher resolves tree refs from CONFIGURED roots (CE-v2 03-tree-addressability -
from config, never literals), names source kind + path on a missing file, and an UNCONFIGURED
root is a loud error naming the config, never a silent empty; sourceKindForAddress feeds the
composer's per-piece labels from the address prefix.
F1: the requires walk uses an explicit stack with per-frame progress — an 8000-deep legal
chain parses clean and a same-depth cycle rejects through ContextPackError with the cycle
spelled out; no recursion threshold exists because none is safe across Node
versions/platforms and depth is attacker-choosable via URL-installed packs.
F2: atom entries validate against the closed legal key set; unknown fields reject loud with
atoms[i] + the field name, and the sharpest singular/plural slips (require/region/situation/
probes) get a did-you-mean.
Proof: RED 5c3a577f7 (F1 fails with r1's exact RangeError shape; F2 with the silent-drop
shape), now atoms 12/12 + manifest-parser 15/15 + profile-composer 9/9, daemon tsc clean.
r1's measured discriminators (verdict row d5ca476d, F1+F2 MEDIUM): the recursive cycle walk
threw a bare RangeError at n=5000 (no atoms[i], no id, no path - and depth is
attacker-choosable via URL-installed packs, so no threshold is safe); a 'require:' typo for
'requires' parsed clean with the dependency edge silently dropped. Pins: an 8000-deep legal
chain parses clean, a same-depth cycle still rejects through ContextPackError naming the
cycle; unknown atom fields reject loud naming the field. Run: 2 failed | 10 passed - F1
fails with the exact RangeError shape r1 measured.
Pure composer (readFile injected, so the same algebra serves library packs and the coming
seat/mission tree sources): SELECT by the locked situation tags (fresh; handover=fresh+
handover; post-compaction=tagged-subset+handover), FILTER by runtime (mini-req 3), CLOSE over
requires (a dep excluded by the runtime filter fails LOUD - a closure must never quietly thin
the walk), ORDER stably (order, then id), RESOLVE every piece through the one Atom-1 address
machinery (whole file or Q1 full span; every failure names the atom), LABEL every piece's
source kind (Q2-Amendment 1 binding, caller-decided, library default), and evaluate the
BUDGET at compose: overage reported with drop candidates ordered optional -> recommended ->
core, larger first within a tier - nothing ever truncated (mini-req 9, D2).
Proof: RED 0d06d0c9a (9 pins, module-absent form disclosed), now 9/9 + daemon tsc clean.
Pins the locked composition algebra before the composer exists (module-absent RED, disclosed):
FRESH = ordered base walk; HANDOVER = FRESH + handover material; POST-COMPACTION = tagged
subset + handover material CLOSED over requires (identity arrives only via closure in the pin);
pieces carry the resolved Atom-1 span AND a per-piece source label (Q2-Amendment 1 binding);
claude/codex compose measurably different profiles and a closure crossing the runtime filter
fails LOUD; missing file / no-match address stop the compose naming the atom; budget at
compose reports overage + priority-ordered drop candidates (optional -> recommended -> core)
with NOTHING truncated (mini-req 9, D2).
Mini-req 1 per the locked SPEC + intake schema: an optional atoms[] section on the pack
manifest (the one metadata home per profile-library; content files stay readable). Each atom
is an ADDRESS plus metadata, never a new file — fresh/handover/post-compaction compose
addresses into the same bytes, so mini-req 5 (no forked content) holds by construction.
Schema: id (slug, unique), address (the Atom-1 grammar via parseAddress — must reference a
declared pack file; header paths require markdown), taxonomy (founder vocabulary), regions
(world anatomy — compose-by-region), situations (the algebra's selector, never empty),
purpose depth|width, runtime claude|codex|any default any (mini-req 3), order (integer —
absorption depends on sequence), requires (declared ids only, no self-ref, CYCLES rejected
at ingest with the cycle spelled out — no subset profile could close over one), priority
(what drops first when a budget binds, mini-req 9), probe ({prompt, expect} — changed
behavior, the one-harness shape). Token counts deliberately absent: derived at compose
(volatility x consequence). Every rule fails LOUD with atoms[i] + id.
Proof: RED c580e82e4 (9 pins fail, 1 back-compat control), now atoms 10/10 + existing
manifest-parser 15/15 + assembler/library/compose neighbors 32/32 + daemon tsc clean.
Pins mini-req 1 per the intake schema (an atom is an ADDRESS plus metadata, one metadata home
per profile-library): full-field round-trip (id, address via the Atom-1 grammar, taxonomy,
regions, situations, purpose, runtime, order, requires, priority, probe), defaults (runtime
any; regions/requires/probe optional), whole-file vs header addressing (headers need markdown),
and fail-loud validation: duplicate id, undeclared/self/cyclic requires, address ref not a
declared pack file, malformed address, bad enums/shapes. Token counts deliberately absent
(derived at compose, the intake's volatility rule). Run: 9 failed | 1 passed (the no-atoms
back-compat control passes pre-implementation by construction).
F1 (MEDIUM): scanHeaders now surfaces the open fence at EOF and the validator emits an
unterminated-fence finding with the opener's line — a file that loses its sections to one
stray fence can no longer validate clean; the gate's silence is meaningful again.
F2 (LOW): the H2 scope is tested against null, never truthiness, so an empty-slug H2 keeps
its H3 children (["", "kid"] — unreachable by any legal address since parseAddress rejects
empty segments) instead of silently promoting them to top level; the family rule flags ANY
section whose path carries an empty segment, so parent AND children are both named.
Also banked r1's A2 structural note in the parseMarkdownSections comment: spans include the
header line, so a matched section is never empty by construction.
Proof: RED 414965833 (both pins fail with r1's measured shapes), now 14/14 + daemon tsc clean.
r1's own measured discriminators (verdict row 85d56172, F1 MEDIUM + F2 LOW): a stray unclosed
fence swallows every later header yet the compose gate reported CLEAN (findings []); an
emoji-only H2 slugifies empty, currentH2's truthiness test reads it as no-H2-in-scope, and its
H3 children silently address at TOP level. Pins: unterminated-fence finding; children stay
under the unaddressable parent (["", "kid"], unreachable by any legal address) with BOTH
family members flagged. Run: 2 failed | 12 passed.
Pure, daemon-homed module (the CLI depends on @openrig/daemon, so both surfaces share ONE
resolver — one grammar, one home): parseAddress (the single form name#H2-slug/H3-slug, H2+H3
depth per the Q1 ruling), parseMarkdownSections (code-fence-protected: fenced headers never
address and never terminate spans; full span = until next same-or-higher header, ownText =
until any header, exposed as FIELDS of one resolution — no second syntax), resolveAddress
(FAIL-LOUD: a no-match names the miss and the real candidates; missing atoms stop a compose,
never thin it), validateMarkdownAddressability (duplicate header-paths + unaddressable
headers), slugifyHeader (one rule: lowercase, strip emphasis markers, hyphenate
non-alphanumeric runs, trim).
Scope fence held (Q4): resolution support only — no CRUD/edit/lint, no verb wiring yet (the
pre-# resolvers and rig-context wiring are the next atoms). Proof: RED f42800e38 (12 pins,
module-absent form, disclosed), now 12/12 + daemon tsc clean.
Pins the locked Q1 ruling + mini-req 6 before any implementation exists (module-absent RED —
the weak form, noted per house discipline; the resolver is NEW code with no wrong-impl state
to pin against): one grammar form name#H2-slug/H3-slug; spans terminate at the next
same-or-higher header; own-text is a FIELD of the same resolution, never second syntax;
fenced headers never address and never terminate spans; no-match FAILS LOUD naming the
candidates; validator flags duplicate header-paths and unaddressable headers; ONE slug rule
(lowercase, hyphenate non-alphanumerics, collapse/trim).
The openrig-core router skill (openrig-skills, the "start here" index) now carries the rig tool
grant (allowed-tools: Bash(rig:*)) and a "load an entry on demand" section teaching
`rig context get <name>` (R1's serving verb), so the harness has ONE skill that teaches agents to
PULL library content matching the installed CLI — the precondition for CE-08 harness thinning. The
mass removal of the other skills and hidden-from-listing are CE-08 (paced by R6's evals), fenced out
of R3. Mode-free (CE slice-05 ruling); the tool grant is the Claude allowed-tools line, Codex covered
by its permission floor (shared ./skills; both manifests derive-shaped per R8 — no manifest change).
RED->GREEN: router-stub.test.ts pins the tool grant + the pull instruction + a mode-free guard
(2 discriminating cases fail before, 3 green after). eval suite 26 green (unaffected); daemon tsc
clean; mirror-skills --check clean.
The eval runner recorded {id, category, pass, error} and discarded the patternResults + loading
order diagnostic grade() already computes, so a FAIL could not say why — a gate whose verdict
cannot be interrogated. Repair: a testable reporter (failReason + recordedGrade) carries
patternResults, the order block, and a FAIL reason into each recorded entry, and the console prints
the reason on FAIL the way ERROR already did. Downstream thinning consumes these grades, so the
distinction between pulled-nothing / pulled-late / pulled-wrong must survive into the record.
RED->GREEN on the reporter (5 pins). Proven by effect: a FAIL now records order{ok:false,reason}
and prints "domain action precedes the context get". 26 eval tests green.
run-evals.mjs (sibling to run-scenarios.mjs) loads the authored cases, runs them through a
provider, grades each captured transcript at the deterministic door, writes recorded grades (JSON),
and exits nonzero on any fail/error. --provider fake is the CI-runnable path (transcripts from
--transcripts; absent prompts ERROR, never a silent green). --provider rig is the live-seat
provider — the proof-contract PULL-WORKS/AGENT-DRIVEN door — which throws with its wiring recipe
until the non-author drives seat spawn + capture, so a live run can never read as a false green.
Verified: run-evals --provider fake loads all 14 cases + writes recorded grades (a covered prompt
grades PASS incl. the loading get-before-action order; uncovered prompts ERROR); --provider rig
fails loud. 21 eval tests green.
A generator writes 13 minimal context packs at the canonical refs the eval cases select among
(the 10 case refs + 3 distractors so selection is a real choice, not a singleton), so a live run
has a real library to pull from. Output is derived (regenerate; never hand-edit under fixtures/);
production content ships via R2 packaging — this is the dev/eval mirror, and the live provider
points a spawned seat's config-resolved packs root at it (Track A).
A conformance guard verifies every fixture manifest through the daemon's OWN parseManifest (not a
second parser) and asserts the fixtures cover every ref the cases expect a seat to pull. 21 eval
tests green.
Wires the harness: runEvals drives each case through an EvalProvider, grades the captured
transcript at the deterministic door, and returns recorded grades + a pass/fail-by-category
summary. The provider seam keeps the live-model executor (spawn a real seat, capture what it ran)
behind one boundary with execution optional/deferred per the ruling; a deterministic FakeProvider
makes the harness runnable end-to-end in CI without model access.
RED->GREEN on the runner accounting (2 cases fail the "nothing ran" stub, green with the real
loop). 19 eval tests green (grader, schema, cases, runner).
Adds the declarative eval-case format: cases are YAML data (test-system/evals/cases/) validated by
a pure, loud, named-error schema (mirrors scenario-schema) — category, prompt, compilable expected/
forbidden patterns, and the loading-only get-before-action order block (selection cases may not
carry order; loading cases must). A loader parses each case list and validates through the schema
on one shared path. The authored set: 10 selection + 4 loading natural-prompt cases over canonical
library refs, each with a 1-5 rubric.
RED->GREEN on the schema (5 discriminating rejections fail an accept-all stub, 7 green with real
validation); a conformance guard proves the authored set validates and covers both categories.
17 eval tests green.
First increment of the context-selection eval harness — a live-model gate, sibling to the
deterministic scenario runner, one harness serving the selection and loading case categories
through a portable EvalCase shape. Lands the deterministic DOOR grader: every expected
command-pattern must match and no forbidden pattern may; loading cases add a get-before-action
order check (a domain action with no preceding `rig context get` fails). The 1-5 rubric rides
each case as authored text and is scored by an optional/deferred judge, never by grade().
Layout keeps the determinism/eval split legible: cases and fixtures under
packages/test-system/evals/ beside scenarios/, runner and grader code in
packages/daemon/test/helpers/eval-*.ts. RED->GREEN proven — 4 discriminating cases fail against
an unconditional-pass stub, 6 pass with the real logic.
r2 round-2 NOT-CLEAR at 4e32b9b88: new URL(f.path, finalManifestUrl) also honors an ABSOLUTE f.path,
and the manifest validator accepts a URL-shaped value as a filesystem-relative .md path — so a
stranger-supplied manifest could make `rig context add` fetch a file from a different origin
(crossedOrigin:true, exit 0, installed).
Fix: before fetching, require every resolved file URL to stay under the manifest's own directory
URL (new URL("./", finalManifestUrl)) via an href prefix check. The trailing slash on the base
defeats prefix-sibling tricks ('/pack' vs '/pack-evil'); origin mismatch and normalized '../'
escapes both fail the prefix. A refusal throws inside the staging try, so the existing rmSync leaves
NO partial pack.
Verified: context suite 31/31 incl. a new absolute-URL discriminator (a cross-origin files[].path is
refused with "outside the pack directory", exit 1, readdirSync(root) empty). RED: remove the guard ->
the discriminator FAILS. CLI tsc clean.
Delta over 4e32b9b88, 2 files.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
r2 NOT-CLEAR at f9a25e9f3: installPackFromUrl computed the file base from the caller's pre-fetch
spelling, but fetch follows redirects. A manifest served behind a 302 then resolved its declared
files against the STALE request root, so a reachable valid pack (redirected) failed with a 404 on
its first file.
Fix: fetchTextOrThrow now returns the response's FINAL url (res.url, post-redirect); each declared
file is resolved via new URL(f.path, finalManifestUrl) — the platform URL resolver, relative to the
manifest's final location — replacing the string-concatenated base. Atomic staging/cleanup unchanged.
Verified: context suite 30/30 incl. a new redirect discriminator (302 -> /pack/manifest.yaml; SKILL.md
resolves against /pack/ and installs). RED: resolve from the original url instead -> the redirect test
FAILS. CLI tsc clean.
Delta over f9a25e9f3, 2 files.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds URL install to `rig context add` and mints the config key the desk's tree-addressability steer
requires, so the landing zone resolves from config — never a hardcoded ~/.openrig literal (a shipped
verb runs unmodified on a stranger machine).
Config key (mints context.packs_root, cloning topology.root across both lockstep twins):
- CLI config-store.ts + daemon settings-store.ts: interface/DEFAULTS/VALID_KEYS/ENV_MAP/KEY_TO_PATH
+ resolve projection (CLI) / getDefaultValue const (daemon). Resolution ladder env
(OPENRIG_CONTEXT_PACKS_ROOT, OPENRIG_* only — no RIGGED_* legacy) > config file > derived default
$OPENRIG_HOME/context-packs; an unknown key still throws "Unknown config key … Valid keys: …".
- config.ts help lists context.packs_root; both parity tests pin it.
- The landing zone now routes through the key in BOTH consumers: CLI `add` (context.ts) and the
daemon's user_file discovery root (startup.ts resolveOne("context.packs_root")) — they agree by
construction.
URL install (context.ts):
- `rig context add <url>` — <url> points at the pack's manifest.yaml (trailing '/' -> +manifest.yaml);
every files[].path is fetched relative to it (Node fetch; deliberately dumb — no registry, no cache).
- ATOMIC: stages into a temp SIBLING of the target, re-validates the on-disk pack, then publishes with
one renameSync; on ANY failure (unreachable URL, malformed manifest, a declared file that 404s) the
staging is rmSync'd — NO partial pack left behind. Refuses if the target name already exists.
- CLI install allowlist aligned with the daemon parser's .sh/.ts (R2 ship-set) so install accepts
what the daemon serves.
Verified (unniced): config twins 82+53 (parity + env>file>default ladder); context suite 29/29 incl.
4 URL-add cases (installs into the OPENRIG_CONTEXT_PACKS_ROOT zone; unreachable / malformed / 404-file
each fail loud + leave NO partial); RED — dropping the staging cleanup fails all three no-partial
tests. Both packages typecheck clean. Clean-vendored `npm run test:repo` 172/172, 0 fail.
Base 50ffac983 (= main, ff-able). No parent-host mutation, no push/fold.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
r2 NOT-CLEAR at 2027bf4c4: the generator introduced a narrower membership (.md/.markdown/.yaml/
.yml/.txt only) than the canonical mirror contract and dropped the .sh/.ts helper assets the served
skill prose references (skills/process/systematic-debugging -> find-polluter.sh,
condition-based-waiting-example.ts). The package therefore served a complete-looking bundle
(missingFiles empty) with two dangling instructions — not the canonical mirror projection R2 promised.
Fix:
- scripts/generate-context-packs.mjs: membership is now the mirror's EXCLUDE-ONLY contract, IMPORTED
from mirror-skills.mjs (EXCLUDES) so the two cannot drift — include every file except
feedback.md / evals/ / .DS_Store / *.local.md. No suffix allowlist in the projector; a suffix the
parser cannot serve is NOT silently dropped — it reaches the manifest and FAILS THE BUILD loud.
- packages/daemon/src/domain/context-packs/manifest-parser.ts: ALLOWED_FILE_SUFFIXES gains .sh/.ts
(ship-set helper assets, served as UTF-8 bundle text, never executed). Same parser — no parallel
path. Its reject-test now pins a still-unsupported suffix (image.png) so unknown types still reject.
Discriminators added (r2's two asks):
- REAL-TREE MEMBERSHIP: projected files[] per skill == an independent mirror-ship-set walk.
- PACKED-PATH DELIVERY: systematic-debugging files[] carries both helpers; served via
ContextPackLibraryService + assembleBundle -> missingFiles=0 and the .sh path + content
("Usage: ./find-polluter.sh ...") + the .ts path are in the bundle. The loss-blessing test is
removed and inverted.
Verified (unniced): generate-context-packs.test.mjs 7/7 incl. both real-canon discriminators;
daemon context-pack suite 110/110 (parser/library/assembler/routes) — the allowlist widening
regressed nothing; daemon tsc clean. RED: re-introduce the narrowing -> both real-canon
discriminators fail. Real projection: 35 packs valid; systematic-debugging ships both helpers.
Delta over 2027bf4c4. This is the first R2 change to touch daemon src (the parser allowlist), the
minimum to serve the ship set through the existing parser per the reviewer's "no parallel parser".
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Projects canonical skill content into context-pack entries shipped inside the published CLI
package and served RELATIVE TO THE BINARY, so `rig context get` serves bytes that match
`rig --version` by construction — closing the fork-drift class CE-v2 priced (97/387 sections
diverged). Implements the generated-manifest-at-package-time entry-shape ruling (dev-planner
2026-08-23): the projection is derived at package time, never committed, regenerated every build;
drift is a build failure, not a runtime condition.
Mechanism (reuses what exists — no second parser, no read-path change):
- scripts/generate-context-packs.mjs: projects packages/daemon/specs/agents/shared/skills/ into
packages/daemon/context-packs/skills/<ref>/ with a generated manifest.yaml per skill. Every
manifest is VALIDATED through the daemon's own parseManifest (the single authority); a malformed
projection FAILS THE BUILD. Non-content files (.sh/.ts) are excluded. `--check` = validate-only
drift gate. Output is gitignored (derived, never edited in place).
- scripts/build-package.sh: generates at package time (pinned to the build semver) and ships the
output with a cp line mirroring the existing assets/specs pattern, into <cli>/daemon/context-packs.
- The daemon already registers that dir as a `builtin` discovery root resolved via
import.meta.dirname/../context-packs (startup.ts:744) and `rig context get` (R1) already serves
any builtin-root pack — so R2 needs ZERO daemon/CLI src change; it populates + ships the root.
- package.json: adds generate-context-packs[:check]; test:repo builds the daemon (the gate reuses
its parser) then runs the --check drift gate.
Verified (unniced):
- GREEN: scripts/generate-context-packs.test.mjs 5/5; generator projects 35 valid packs.
- RED (mutation): neutering the parseManifest validation makes the "malformed fails the build"
test fail — the net is genuinely guarded.
- Serve path end-to-end: the real ContextPackLibraryService pointed at the generated output as a
builtin root serves skills/core/attention-queue (sourceType builtin) + all 35 packs, with no
read-path change.
- Staleness-by-construction + determinism: unit-proven (editing canon after generation does not
change packed bytes; two runs are byte-identical).
- Full lane: npm run test:repo 170/172 pass (2 pre-existing skips), and the package-build test
ran build-package.sh end-to-end — generation + ship placed 35 manifests in <cli>/daemon/context-packs.
Fences honored: no scope change, no parent-host mutation, no push/fold. Track A from-config
(daemon owns discovery roots) unchanged. R8 two-manifest question untouched (this is R2 content,
not R3 stub). The full "pack the CLI, edit canon after packing, served bytes unchanged" door test
is the non-author reviewer's per the proof contract.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
D1 (unify what exists): promote `rig context` to the on-demand library. R1 adds the agent-facing
serving verb `rig context get <ref>` over the EXISTING assembler path (the same by-ref/preview
route `preview` uses — never a parallel assembler). Unlike `preview` (the operator's pre-send
check, unchanged), `get` prints the assembled bundle bytes on stdout (warnings to stderr) so a
seat loads exactly the served content; `--json` for programmatic use. Naming ruled: one library,
one verb — `rig context get`, not `rig skills get`.
First step of the pull-first build sequence (R1 cheapest, useful before any packaging change).
Tests: get serves the bundle without operator framing; --json returns the wire. context suite
24/24, niced. Proof-contract item 1 (agent-driven pull from a natural prompt) is the non-author
door test. R2..R7 (package-with-CLI, URL add, evals, router stub, three-mode, inventory) follow.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Founder requirement: the renderer/transcript interaction must be learnable exactly
where the reader hits it, not buried in docs.
- `rig transcript`: a thin transcript (<=5 captured lines) now emits a stderr note
naming the fullscreen renderer (alternate screen -> no scrollback) and the lever
(CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1, set by default; overridable via
OPENRIG_CLAUDE_DISABLE_ALTERNATE_SCREEN=0). Also added to the command's help text.
Note is stderr-only, so it never pollutes stdout content or --json.
- `rig config` help: the transcripts.* key description now explains that thin CLAUDE
transcripts usually mean the fullscreen renderer, NOT lines/poll_interval, and names
the lever.
Tests: thin transcript emits the note (names renderer + lever); a healthy (>5-line)
transcript does not. CLI transcript + config suites 26/26 green, niced.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude Code's fullscreen renderer draws to the terminal alternate screen, which
emits no scrollback — tmux capture-pane stores nothing and `rig transcript` goes
thin. Every managed Claude launch path now prepends
CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 by default (classic renderer → native
scrollback, and same-pane handover scroll-preservation).
- New claudeClassicRendererEnvPrefix(env) in yolo-mode.js (mirrors the
claudePostureFlag pattern: computed at cmd-build time, uniform across paths).
- Applied at all four launch sites: claude-code-adapter launchHarness (fresh /
resume / fork) and claude-resume (restore path).
- Config-overridable: OPENRIG_CLAUDE_DISABLE_ALTERNATE_SCREEN=0 (or "false")
opts back into fullscreen — then the command is byte-identical to pre-change.
RED-first: the default-prefix pins fail on main (adapter emits no prefix);
existing exact-cmd regression pins updated to the new baseline (renderer prefix
present by default). Touched suites 103/103 green, daemon tsc clean, niced.
Remaining slice-01 items (this commit is item 1 only): item 2/3 runtime +
non-author receipts (QA); item 4 point-of-use hints; item 5 prevention triple
(pin "tui" default, decline-the-upsell guidance, VM provisioning); config-store
allowlist registration for `rig config set` of the renderer key.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
One fix at the ONE mapping home covers both consumers: Object.create(null)-backed map, so
map['constructor']/map['__proto__'] read undefined and the existing /null fallbacks
behave — prototype-key pins diverge as ordinary unknown aliases (raw strings preserved, the
pass continues past them) and the advisory returns null instead of a fabricated canonical id.
Structural pin added: the map's prototype must stay null (single-mapping-home test).
Proof: RED 2baa3c5b8 (3 pins fail with r2's exact throw/fabrication), now
model-divergence-monitor 21/21 + spec-validation-advisory 6/6 (27 total), daemon tsc clean.
r2's discriminators implemented as pins, failing against 82449e1f0 for r2's exact reason
(.toLowerCase is not a function via inherited Object.prototype members):
1. modelsMatch constructor/__proto__/hasOwnProperty vs claude-opus-5 -> false (currently throws).
2. Monitor level: constructor-pinned wrong-model seat proclaims RAW and a LATER seat still
proclaims — the pass survives the adversarial pin (currently checkOnce rejects, 0 deliveries).
3. Advisory level: prototype-key pins return null, never a fabricated canonical id (currently
emits 'function Object() { [native code] }').
Run: 3 failed | 24 passed across monitor + advisory suites.
Founder-steered, desk-ruled. The ruled class (alias pin 'fable' vs effective 'claude-fable-5'
re-proclaiming per generation and per daemon restart) ends AT THE DETECTOR: modelsMatch now
canonicalizes the PINNED string through CANONICAL_MODEL_PINS (spec-validation-advisory.ts — the
single mapping home, reused directly, no second map) and exact-compares after trim/lower.
Unknown aliases and fallback models canonicalize to themselves and still diverge; raw
pinned/effective strings travel untouched in proclamations; r2's ambiguity discriminators stay
false (no token containment).
Completed deletion contract: the empty CLAUDE_ALIAS_MIGRATION_BRIDGE and its
SPEC_VALIDATION_CAPABILITIES sentinel gate are deleted per the bridge's own contract; the
expiry tripwire test is replaced by a SINGLE-MAPPING-HOME pin (monitor module exports no alias
map; the same advisory data drives both surfaces). Stale comments updated in both touched
modules (they taught that the runtime detector rejects every alias). Spec-validation BEHAVIOR
unchanged: alias pins still get the fail-open advisory; only its stale parenthetical reworded.
Flipped pre-existing pin, disclosed in RED f00016ca4: 'alias bridge is EXPIRED' expected
fable/claude-fable-5 false — that recorded the false-positive class this row ends.
Proof: RED f00016ca4 (RED-1 modelsMatch, RED-2 monitor-level no-proclamation, controls green);
now model-divergence-monitor 18/18, spec-validation-advisory 6/6, daemon tsc clean.
Against main 55d83c06d: RED-1 (modelsMatch fable == claude-fable-5, trim/case retained) and
RED-2 (monitor level: alias-pinned claude seat with canonical effective fires nothing) fail for
the intended reason — modelsMatch's sentinel gate rejects every alias since the bridge expiry.
Controls already green and retained: alias vs different/unknown canonical still diverges
(fable/claude-opus-5, opus unmapped), wrong-model alias seat proclaims with RAW strings.
Disclosed: the fix commit will flip the pre-existing 'alias bridge is EXPIRED' expectation
(fable vs claude-fable-5 false -> true) — that pin recorded the false-positive class this
founder-steered row ends.
Live specimen (quiet receipt 20260823T222324Z): resolve_home +20 in a 1.5s burst — ~20 seats
crossing the 15-min identityHomeTtlMs together, a synchronized herd of unnecessary ps eww probes
(a second ~6-probe family rode the 60s divergence surface at 22:21:29.9).
DESIGN (per orch-lead's guard + 22:39Z timing correction): a probe that STARTED strictly after
the identity's lstart second closed observed the FINAL occupant of the (pid, lstart) key —
same-pid same-second reuse is impossible once the second passes — so its entry is SETTLED: no
time expiry, removed only by identity mismatch (new key) or FIFO eviction. Probe START time,
NOT completion: a probe started inside the second can observe the retired occupant and complete
after it (the deferred-probe pin). UNSETTLED entries (probe started inside the ambiguous second,
unparseable identity, identity-less callers) keep the 60s homeTtlMs as the finite recovery path.
WHY THE HERD CANNOT RETURN: steady-state entries are settled — identity-keyed probe count is
zero for resolved AND pending seats; there is no periodic expiry left to synchronize. RECOVERY
BOUND for the W-5.3-1 same-second collision: <= homeTtlMs (60s) from cache write + one poll
(was 15 min); still fails CLOSED throughout (strict ts > startTs gate — never a wrong thread).
The dead identityHomeTtlMs option is REMOVED (row instruction; no call site passed it). The
soak-finding pin's clocks were made identity-consistent (base after the start second) with
assertions unchanged. Preserved: (pid,identity) coalescing (waiters inherit the original
probe's settled verdict), failed-probe non-caching, default-home-first, strict time gate.
Proof: RED 504bcb7d0 (3 pins fail on their own assertions against 5c6183e3b), now green —
control-plane-census + process-census + codex-resume 47/47, daemon tsc clean.
Three failing discriminators against main 5c6183e3b, committed RED per the dispatch and
orch-lead's design guard + timing correction:
1. SETTLED herd pin — a probe STARTED after the identity's lstart second closed is permanent:
same pid+identity past 15 min makes ONE probe total (current: 3, expiry herd).
2. W-5.3-1 collision pin — an entry probed INSIDE the ambiguous second recovers via the 60s
bound (current: thread-unresolvable until the 15-min identityHomeTtlMs).
3. DEFERRED-PROBE adversarial pin (orch-lead 22:39Z) — a probe STARTED inside the second stays
short-lived even when it COMPLETES after it; completion time is not a safe permanence
predicate (current: fails via the same 15-min recovery).
Run: control-plane-census.test.ts 3 failed | 16 passed; failures are the pins' own assertions.
Restack of the r1-CLEARed S10 follow-on (was f035c77d0 on 413e2d541) onto 3483f631f. The base
crossed the desk's fix-2 fold in flight (honest crossing) — fix-2 (OPR.0.5.3.10 soak: per-entry
TTL split) touched EXACTLY these two files, so f035c77d0 was not ff-able and patch-id would not
hold (in-file semantic overlap). Re-applied deliberately over fix-2's split TTL.
WHAT IT DOES (unchanged intent): resolve(pid, identity) — identity REQUIRED; the ungated log read
(pre-round-4 S10 class, safe today only by call-graph discipline) is reachable ONLY via the named
resolveUngatedLegacy(pid). Both delegate to private resolveInternal(pid, identity | undefined). A
NEW identity-less call site is a COMPILE error, enforced by a review-visible pin in src/.
INTERACTION WITH fix-2 — the desk's flagged question, answered BY CONSTRUCTION and PROVEN:
Does resolveUngatedLegacy get the identity-less 60s TTL? YES. It delegates with identity=undefined,
and fix-2's TTL selector in resolveInternal is `identity !== undefined ? identityHomeTtlMs(15m) :
homeTtlMs(60s)`. undefined -> the 60s branch, structurally. resolve(pid, identity) passes a defined
string -> the 15m branch. The extraction moved the selector verbatim into resolveInternal; neither
TTL default nor the selector condition changed.
POSITIVELY PROVEN: fix-2's own soak test now reads resolve(500,"identity") (15m, 1 probe over 10min)
and resolveUngatedLegacy(501) (60s, re-probe after 61s) — its assertions verify the identity-less
path takes the short TTL after the rename. No test was weakened to accommodate the rename.
VERIFIED (isolated worktree, niced):
- GREEN: daemon tsc clean (exit 0, zero output); vitest control-plane-census + resume-metadata-
refresher 38 passed (38) — incl. fix-2's soak test over resolveUngatedLegacy and the ternary
routing in both production wrappers.
- RED (r1's exact acceptance, both doors r1 named):
- M1 resolve(pid, identity?: string): tsc exit 2, codex-thread-id.ts(204,7) TS2322 'true' not assignable to 'never'.
- M2 resolve(pid, identity: string | undefined): tsc exit 2, same TS2322 at the same line 204.
The pin fires in src/ at its own line — not a call site. Mutations restored under the check.
DELTA: 4 files — codex-thread-id.ts (+the three-method shape + the ResolveRequiresIdentity pin),
control-plane-census.test.ts (17 identity-less resolve()->resolveUngatedLegacy incl. fix-2's 501s;
+ the pin doc-check test; identity-passing resolve(x,"...") untouched), and the two production
wrappers (resume-metadata-refresher.ts, startup.ts) routing undefined -> resolveUngatedLegacy
explicitly (never a silent fallback).
RESIDUAL r1 named (NOT blocking, out of ruled scope): the pin guards resolve()'s SIGNATURE; a new
public method delegating to private resolveInternal(pid, undefined) would not trip it — reviewer-
caught vector, optional future hardening.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The live soak at installed 413e2d541 proved the census fix (list_processes
+6, bound 8) and exposed the last amplifier: resolve_home +53/5min because
the 60s TTL EQUALS the 60s divergence cadence — identity-keyed entries
expired exactly when next needed and every pending codex seat re-probed
every poll.
Fix per the desk-approved shape: identityHomeTtlMs (default 15 min) for
identity-keyed entries; homeTtlMs (60s) remains for identity-less callers.
Time is not load-bearing for identity-keyed entries: pid reuse is
invalidated STRUCTURALLY by the identity key (rounds 3-6), and — the desk's
flagged reviewer question, answered by construction — the cache holds only
the pid's HOME, never a thread id: a thread roll inside the window is picked
up immediately by the per-resolve log read, and a live process's HOME cannot
change; restore/rewind edges change the pid or the start time, both of which
change the key.
RED-first with the soak-economics discriminator (two polls 60s apart, then
ten minutes on: ONE probe total for the identity-keyed seat; the
identity-less slot serves within 60s and re-probes after). 99/99 across the
seven slice+neighbor suites; daemon tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
r2's same-second control at c2f9a2e9c: retired A can log at ts == B.startTs
and exit within the second B reuses the pid — lstart has no subsecond
component, so ownership INSIDE the start second is undecidable, and the
round-5 ts >= startTs gate returned the retired token. The ruled shape is
honest INDETERMINATE over masked stale: the reader gates STRICTLY
(ts > startTs). Rows at startTs — even the current occupant's own — stay
unresolved for that poll; rows after it resolve. Round-5's same-second
acceptance expectation is superseded in place with the supersession named.
RED-first with r2's real-sqlite same-second fixture (retired at startTs →
undefined; current-occupant same-second row → still undefined; startTs+1 →
resolves). 98/98 across the seven slice+neighbor suites; daemon tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
r2's boundary control at 3d3b6b1c9: the 2s slack readmitted the retired
token at startTs-1 — the same failure narrowed to two seconds, not
eliminated. Their argument closes it exactly: ts and lstart are both
second-aligned, and a process cannot emit its own log BEFORE its start
second, so ts >= startTs admits every current-occupant row (same-second
included) and no retired one. Slack removed; the source comment r2 flagged
as false under slack is now true.
RED-first with r2's real-sqlite boundary fixture (retired row at startTs-1
-> undefined; B's same-second row at startTs -> new-thread). 97/97 across
the seven slice+neighbor suites; daemon tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
r2's effect-level reproduction at bfa77b6e8 (real sqlite, not a stub): logs
rows key on pid:<pid>:<opaque-uuid>, so the pid-only LIKE read on the
DEFAULT-HOME fast path returned a RETIRED process's thread to the new
occupant before identity participated — with zero home probes, i.e. on the
exact optimized path. The opaque uuid is not derivable from census data, but
the measured schema's ts is epoch seconds, so TIME is the honest gate:
lstartToMinTs parses the identity's `ps lstart` (2s slack) and every log
read for an identity-carrying call adds `AND ts >= minTs` — a retired
occupant's rows predate the current occupant's start and structurally never
match. Identity-less callers keep the legacy unbounded read (TTL-bounded at
the cache layer, as ruled). Zero-subprocess default-home HITS preserved; a
genuine miss may honestly probe once (pinned).
RED-first with r2's own fixture shape (retired-only row -> undefined; the
new occupant's own row -> new-thread; identity-less -> newest row; hit-path
probe count pinned). 96/96 across the seven slice+neighbor suites; daemon
tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
r2's deferred-pressure reproduction at 626f60981: with probes measured at
8-36s a pid can be reused MID-PROBE, and pid-only in-flight coalescing
handed the new occupant the retired occupant's answer before the completed-
cache identity check could run. The fix makes identity STRUCTURAL: both the
in-flight map and the completed cache key on (pid, identity), so a differing
identity never joins a foreign probe, a reused pid simply misses the retired
entry (no read-time mismatch check to get wrong), and a late stale-probe
completion writes only its own key — the newer occupant's entry cannot be
clobbered in either completion order. Identity-less callers keep their own
TTL-bounded slot; same-key coalescing and honest failure are unchanged.
RED-first: the both-completion-orders discriminator (B starts its OWN probe,
gets new-thread, and stays cached at 2 probes) was RED at 626f60981 exactly
as r2's JSON showed. 95/95 across the seven slice+neighbor suites; daemon
tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
The dual-gate blocker's ruled remedy (r1 BLOCKING-1 / r2 B1; orch-lead:
identity, not TTL alone): the strict census now carries `lstart` as
ProcessRow.startedAt — the identity half of pid+start-time, at zero extra
spawns (same single `ps`). Both consumers thread the census row's identity
into CodexThreadIdResolver.resolve(pid, identity); a cached entry whose
identity mismatches is invalidated IMMEDIATELY, inside any TTL — a reused
pid can no longer return the retired occupant's thread id. The TTL (60s)
stays as the fallback bound for identity-less callers; failures still reject
all coalesced waiters and cache nothing.
94/94 across the seven slice+neighbor suites (incl. the frozen-clock
identity discriminator and the startedAt-on-every-row census pin);
daemon+CLI tsc clean. Branch already restacked onto main 6ce8da5a2 (the
slice-06 fold) BEFORE this fix per r1's one-combined-candidate guidance —
the startup.ts overlap merged cleanly, both wirings verified present.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
r2-B1 first half: cache entries carry a timestamp and expire (default 60s,
injectable clock) — retained as the FALLBACK staleness bound under the
process-identity remedy landing next. r2-B2: new defaultListProcessesStrict
REJECTS on a failed ps spawn and is the census's production default; the
lenient variant keeps its best-effort contract for direct consumers. 92/92
focused+neighbor suites at this tree.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
orch-lead freeze-boundary ruling (r1's remedy): TTL alone still serves
another occupant's thread inside the window; identity (pid + start time,
already present in the process table) must invalidate reuse at the next
resolve with zero extra subprocess. Two RED pins: identity mismatch
re-probes immediately under a frozen clock; the strict census rows carry a
parseable startedAt on every row.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
r2 B1/B2 + r1 BLOCKING-1 (independent convergence on pid-reuse): (1) a
reused PID with a new HOME must re-probe after a TTL and return the NEW
thread — the size-bounded cache alone served a retired occupant's thread id
indefinitely (both reviewers reproduced it); (2) the census's PRODUCTION
lister must REJECT on enumeration failure — defaultListProcesses swallows a
failed ps into [], which the census cached as an empty SUCCESS for the whole
freshness window (r2's PATH discriminator: 0 rows cached while 520 were
live). Both RED at 9f0ecf351.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
Closes orch-lead's two pre-review gaps, discriminators-first (both RED at
the prior candidate):
1. Per-pid in-flight coalescing: two concurrent resolves for the same
non-default pid now share ONE `ps eww` (the RED run showed the second
caller spawning its own — it hung 20s on the unreleased second probe).
2. Explicit decision, now pinned: a SUCCESSFUL HOME=default resolution is
cached like any other — a stable default-home pid with no thread log must
not re-run `ps eww` every poll. Pid-reuse staleness is bounded by the
cache size and accepted for both cases alike; a FAILED resolution still
rejects all coalesced waiters and caches nothing.
72/72 across census, control-plane counting, refresher, scheduler, and
divergence-monitor suites; daemon tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
The measured control-plane collapse (parent incident cec9cafb; live slow-span
sample: 171 resume_metadata.list_processes calls mean 9.39s max 40.31s, 298
codex_thread_id.resolve_home calls mean 8.24s max 36.72s): every consumer of
the process table spawned its own `ps` — the divergence poll once PER SEAT,
the snapshot refresher up to eight times PER CODEX SEAT, plus a `ps eww` per
pid for home resolution — and under load the spawns stretched, overlapped,
and took queue/capture/fleet reads down with them.
Corrections, mini-req by mini-req:
1. ModelDivergenceMonitor.checkOnce() threads ONE cycle-scoped census into
every readEffectiveModel call (lazy: an all-settled pass spawns nothing).
2. PeriodicSnapshotScheduler.tick() shares ONE cycle census across all rigs'
refreshes; snapshot-time codex discovery is SINGLE-ATTEMPT (the 8-attempt
250ms loop stays for the adoption boundary racing a booting codex, and no
longer sleeps after its final attempt).
3. New domain/process-census.ts: in-flight coalescing + 2s freshness reuse of
SUCCESS; a failed enumeration rejects all coalesced callers, caches
nothing, and the next call retries. cycleLister() gives the per-cycle
at-most-once guarantee the freshness window alone cannot.
4. Contracts preserved: divergence detection unchanged (62 neighboring tests
green), fillNullOnly still never clears a present token, refresh failure
still never blocks the snapshot, adoption-boundary retry loop pinned.
Addendum: new CodexThreadIdResolver — DEFAULT home first (pure file read,
zero subprocess), bounded (256) PID-keyed cache of resolved non-default
homes, failure never cached. Wired as the default in both the refresher and
the divergence surface.
Mini-req 5 receipts: 8 deterministic counting tests green at candidate;
base A/B on a detached main worktree: 4 RED with the amplification visible
in the counts — divergence pass counted THREE enumerations (per-seat),
snapshot tick counted ZERO through the census (bypassed), resolver absent,
capture slept after its final attempt. Both tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
Closes the three r2 BLOCKINGs (r1 independently confirmed all three and
withdrew its CLEAR):
B1: instantiate() — the path /api/up actually takes — now installs the spec's
topology defaults, mirroring the existing both-sites-must-write precedent the
file itself documents for rig-persist. Runs BEFORE node launch (seats read
defaults from first boot), doubly guarded (installer never throws; the call
site converts even a broken injected fsOps into a named warning).
B2: the installer's best-effort contract is now TOTAL — each section's
enumeration runs inside its own catch, an EACCES/EIO becomes a named failure
on the section directory, and one section's denial never starves the others.
B3: traceTopologyChain validates rig/seat/name as single safe path segments
(no separators, dot-segments, empties, NULs) BEFORE any filesystem contact;
the CLI surfaces a clean refusal, never a stack trace. Dotted filenames and
dashed/underscored ids stay valid.
Plus the check both reviewers converged on: topology-defaults-door.e2e.test.ts
drives a REAL spawned daemon with the REAL `rig up` on a clean scratch
OPENRIG_HOME and reads all three installed altitudes back off disk — the door
that would have caught B1 the first time, now permanent. All RED pins green
(72 daemon, 12 CLI targeted); both tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
Pins the three r2 BLOCKINGs at 0d07cbec1 (r1 independently confirmed all
three and withdrew its CLEAR):
B1 (x2): instantiate() — the path /api/up actually takes via the bootstrap
orchestrator — must install the spec's topology defaults on the REAL fs; a
control pins that no resolver still means clean success. RED because only
materializeValidatedSpec was wired.
B2 (x2): an enumeration failure (listFiles/listDirs EACCES) must be a NAMED
failure entry, never a throw — and one section's denial must not starve the
others. RED because the for-headers sat outside the catch.
B3 (x1 + a keeps-valid control): rig/seat/name must each be a single safe
path segment, rejected BEFORE any filesystem read; ordinary dotted names and
dashed/underscored ids stay valid. RED because the walker joined unvalidated
input.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
The ATOM-7 grammar pin asserts the exact ordered rig-context subcommand set
(delivery seams excluded). trace is a pure read (config + filesystem,
daemon-independent, no delivery seam) — precisely the class the grammar
admits. Caught by the honest full-CLI-suite capture; the earlier piped run
had swallowed the verdict line.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
OPR.0.5.3.6 proof items 6-7.
Item 6: the demo rig ships project-example/ — a worked project-tree
(mission SPEC with composing intent, slice SPEC with the exact projected
headings + proof contract, PROGRESS/PROOF stubs that teach their own rules) —
and its README states plainly why project context cannot ship: it describes
what YOU are building; installing someone else's would put a stranger's
project in your agents' heads. Topology defaults ship; project context is a
worked example only.
Item 7: docs/reference/legacy-topology-migration.md — the exact-command
update path for a rig predating the convention (config-derived roots,
BEFORE-advisory proof, rsync --ignore-existing --exclude state/ mirroring
copy-if-absent, AFTER-trace verification, no same-session deletion; the
parent-host campaign is the named consumer). Validated by dry run against
v-openrig-build, a real predating rig on this box: 836 chain/seat entries
would migrate with state/ correctly excluded (3792 without — the exclusion
is load-bearing and now part of the documented command). Receipt in the
slice's proof/.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
OPR.0.5.3.6 proof item 5 (promote-and-document; the mechanism ran live in the
lab on 6+ seats). openrig-core now ships hooks/scripts/refocus.cjs registered
on SessionStart/UserPromptSubmit/Stop (both runtimes) + PostCompact (Claude;
Codex exposes no compact hook). Transcript-growth signal (~2.6MB default,
OPENRIG_REFOCUS_BYTES to tune); silent no-op unless due; degrades to silence
on any error.
CONTENT IS CONFIGURABLE, never project-specific in source: resolution is
OPENRIG_REFOCUS_CONTENT_FILE env > $OPENRIG_HOME/refocus/REFOCUS.md > a
generic three-question default with the chain-trace pointer and the
LEARNED-ownership warning. The lab hook's per-box distillation-tool path and
retired-skill pointer did NOT ride the promotion.
docs/reference/refocus-channel.md specs delivery to RUNNING seats (hook
boundaries = no relaunch, no operator action; latency bound is the seat's
next turn) and the relation to chain files. Behavioral probes: 4/4 (fires on
fresh/growth, silent under threshold, configured file wins).
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
OPR.0.5.3.6 proof items 3-4: a rig spec may carry topology/ defaults that
install under the typed topology.root at materialization — instance/ at the
TOP of the root (D2), rig/ under rigs/<rig>, seats/<seat> beneath; the pod
level deliberately skipped (founder P1). COPY-IF-ABSENT: a shipped default is
a starting point the occupying team appends to; a later rig-up never clobbers
earned context. Best-effort after the persistence tx (a rig never fails to
materialize over a defaults copy) with per-file failures surfaced as named
warnings.
Wiring: optional topologyRootResolver dep on PodRigInstantiator; startup.ts
threads the settings-store value (production); absent in tests = no install.
product-team ships the first curated set: instance/CRAFT.md, rig/CRAFT.md,
rig/ORCHESTRATION-CRAFT.md (the R-a craft file — ghost-text discriminators,
staged-vs-consumed, polling discipline, the compaction warning, plus the
discovery -> curation -> ship path), and seat CRAFT.md for orch1-lead,
rev1-r1, rev1-r2, dev1-qa.
4 installer tests (altitude layout, preserve-not-overwrite, no-op spec,
named-failure best-effort); instantiator suite 33/33; daemon tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
Live-box correction caught by running the walk on this VM: the legacy tree
sits at ~/.openrig/shared-docs even when $OPENRIG_HOME is elsewhere (the
known root split). Both twin helpers now mirror the codex adapter's existing
precedent — OPENRIG_SHARED_DOCS_ROOT env, else the literal
~/.openrig/shared-docs — instead of deriving from the home. The CLI literal
moves into config-store.ts (the settings layer), so the walker file carries
ZERO shared-docs literals (the proof-contract grep receipt).
Live receipt on this box: `rig config get topology.root` ->
$OPENRIG_HOME/topology (source: default); the trace read the REAL rig+seat
LEARNED.md files through the legacy fallback and emitted the named advisory
for each (legacy source, canonical destination, the config key). 107 CLI
tests green; tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
OPR.0.5.3.6 "productize the trace": the chain walk moves into product source.
New packages/cli/src/lib/topology-trace.ts walks instance -> rig -> seat
(instance file at the TOP of topology.root per D2; pod skipped per founder
P1), one chain filename per altitude. Per-level legacy fallback reads the
pre-convention shared-docs/rigs location and NAMES its advisory (source path,
canonical destination, the config key) — never a silent legacy read. The
legacy literal lives in one twin helper, not in the walk.
New `rig context trace --rig <r> [--seat <s>] --name <CHAIN.md>` surface:
daemon-independent by design (orientation is exactly when the daemon may be
down); advisories on stderr, content on stdout, --json for agents.
6 unit tests: three-altitude walk, legacy-fallback honesty (advisory names
both locations), canonical-beats-legacy, absent-everywhere, rig-level trace,
no instance-level legacy. CLI tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
OPR.0.5.3.6 D1 (desk-stamped from the dev50-planner proposal): the topology
tree gets a typed key — resolution env (OPENRIG_TOPOLOGY_ROOT) > config file
> derived default $OPENRIG_HOME/topology. Keying to the home makes the
~/.openrig-vs-$OPENRIG_HOME split a non-question: a box with two homes is two
instances, each with its own topology tree. D2: the instance altitude is the
TOP of this root (<root>/<CHAIN>.md, rigs/<rig>/<CHAIN>.md,
rigs/<rig>/seats/<seat>/<CHAIN>.md).
resolveLegacyTopologyRigsRoot() (daemon settings layer) is the ONE home for
the legacy <home>/shared-docs/rigs literal — walkers call it for their
advisory-emitting fallback and carry no literal of their own.
Both twins updated in lockstep (VALID_KEYS/ENV_MAP/KEY_TO_PATH/defaults +
each side's pin test) plus the rig config --help key listing (HG-6 guard).
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
r2 round-2 NOT-CLEAR on e2d3ddb1a: the legacy advisory collection sat AFTER the early `continue`
for a missing node id, so a fable-pinned legacy node that ALSO lacks an id got no advisory —
violating the candidate's own "advisories regardless of validity" contract. (The prior invalid-
path test always supplied an id, so it could not catch this.)
Fix: collect the alias advisory at the TOP of the legacy node loop, BEFORE the id guard, using a
`nodes.?` fallback location — mirroring the pod-aware member loop exactly.
RED-first: the new missing-id discriminator test fails against e2d3ddb1a (no advisory for a
fable-pinned, id-less node) and passes with the fix (advisory suite 6/6). Daemon tsc clean; niced.
Refs: review-r2 round-2 NOT-CLEAR qitem-20260823033730-cb40ee4a
Co-Authored-By: Claude Opus 4.8 <[email protected]>
r2 NOT-CLEAR on de5ce8411: the public /api/rigs/import/validate route auto-detects format and
sends flat/legacy specs to LegacyRigSpecSchema.validate, which never called the advisory helper —
so a legacy rig spec pinning an alias (nodes[].model: fable) validated clean with no migration
nudge, missing proof item 2 for the legacy format.
Fix (bounded, per r2's remedy): the legacy node loop now calls the same aliasModelPinAdvisory
helper and returns the optional advisories list, fail-open (valid/errors unchanged). The pod-aware
path already did this.
RED-first: the new legacy test fails against de5ce8411 (advisories empty for a legacy fable pin)
and passes with the fix. Suites: advisory + model-divergence + rigspec 74/74; daemon tsc clean;
niced. The CLI door is format-agnostic (prints whatever advisories the route returns), so the
existing print door plus this daemon-side legacy test cover both pod-aware and legacy formats.
Refs: review-r2 NOT-CLEAR qitem-20260823032813-b6693e33 (HIGH-1)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Item 1 (hard obligation): register "model-pin-canonicalization" in SPEC_VALIDATION_CAPABILITIES
(rigspec-schema.ts). That fires the behavioral sentinel gate in modelsMatch (model-divergence-
monitor.ts) so the fable->claude-fable-5 alias tolerance EXPIRES — pins must be exact/canonical,
ending the silent pin-drift the detector's alias bridge (5b5b78289) allowed. The now-dead
CLAUDE_ALIAS_MIGRATION_BRIDGE is emptied per the migration-bridge deletion contract.
Item 2: a spec-validation advisory (new spec-validation-advisory.ts) warns at `rig spec validate`
time when a spec pins an ALIAS form, naming the canonical id. Fail-open: added as
ValidationResult.advisories (never an error, never affects validity); collected in
RigSpec.validate's member loop; passed through /api/rigs/import/validate; printed by the CLI as
"spec advisory: ...".
The two are coupled by the migration-bridge deletion test: once the spec-validation-advisory file
exists, model-divergence-monitor.test requires the bridge empty — so items 1+2 land together.
RED-first: reverting the registration + bridge to main fails the sentinel/expired/tripwire pins
exactly (3 failed/12 passed); GREEN with the change. Suites: model-divergence+advisory 19/19,
rigspec 54/54, cli rig 9/9; daemon tsc clean. All niced per the desk hold. Item 3 = QA runtime
read-back (next-gen seat carries a canonical pin end to end).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Closes r1's Fix-2 Finding 1 (row qitem-20260823001909-a2e67f3e): the prior
hermeticity pin passes with EITHER fix mechanism alone, so a restored eager
constructor warm — the change that killed ~92 of the 162 launches/suite —
would return undetected behind the createTestApp null-observer belt. This
test constructs new PermissionDriftObserver({db}) directly under the
child_process mock and asserts zero execFile.
Discriminator, predicted before running: with the eager warm temporarily
restored, exactly this pin fails (1 failed / 2 passed) while the
createTestApp pin stays green — the precise gap, now closed. Reverted;
3/3 green at candidate.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
Addresses review-r2 NOT-CLEAR on the ingest-health addendum (two causal seams
r1's clear missed — dual-independent review earning its keep).
HIGH-1 (record-before-persist): liveness was recorded right after a successful
capture, before the temp-write+rename that can throw into the best-effort catch.
A failed required write then advertised capture_fresh over stale on-disk bytes.
Fix: record liveness ONLY for a completed healthy tick — on the unchanged-content
early return (file already holds the current bytes) or after a successful atomic
rename. A write throw leaves liveness un-advanced, so getIngestHealth reads the
(correctly stale) mtime. This also resolves the LOW r1 flagged.
HIGH-2 (in-flight tick resurrects liveness after stop): stop() dropped the map
entry but an already-awaiting tick() resumed and re-added it (and could still
write). Fix: a per-start generation token, set before the first tick and
invalidated by stop()/replacement; the tick re-checks it after the async capture
and bails before any write or record. Also closes the pre-existing write-after-
stop race and the older-generation-clobbers-newer race on restart.
RED-first: r2's two discriminators plus a restart-replacement pin fail against the
prior addendum (3 failed / 49 passed) and pass with these fixes; store+rotation
suites 52/52; daemon tsc clean.
Refs: review-r2 sealed verdict c31de324 (qitem-20260822235847-d194abbc);
r1 CLEAR qitem-20260823000920-4febf462
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Addendum to the unchanged-content write-suppression guard (r1 review finding
on 753591407). The guard freezes the transcript file's mtime on an idle seat
whose pane content is static, but getIngestHealth derived capture_stale from
mtime with a 10s threshold (consumers: node-inventory, routes/transcripts).
Result: a quiet seat — the normal waiting state — would falsely report
degraded/capture_stale within 10s while capture ran fine every tick.
Fix (r1's option 2 — storm-immune, no measurement needed): the rotation tick
records a last-capture timestamp in memory on every SUCCESSFUL capture, before
the unchanged-content early return. getIngestHealth reads that timestamp,
falling back to file mtime only when no rotation records the session (adopted
sessions, or before the first tick this process). The liveness record shares
the timers' lifecycle — dropped on stop, so a genuinely stopped rotation
correctly falls back to a stale mtime.
Not option 1 (utimesSync on the suppression path): a metadata touch per tick
per file would likely still amplify through fseventsd — the very effect the
guard removes — and r1 flagged it as needing measurement we cannot run under
the live storm. Option 2 avoids the question entirely.
RED-first: with the pre-addendum mtime check, the new capture_fresh test fails
(idle+suppressed seat reads capture_stale); it passes with the in-memory read.
Fallback-to-mtime and stopped-rotation cases pinned too.
Refs: qitem-20260822231346-974c0825 (review-r1 CLEAR-with-required-addendum)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Founder-priority hotfix FIX 2 (row qitem-20260822230440-da0d2ad6; shape from
row 9e0a051e, extended by measurement). Three changes, one mechanism:
1. PermissionDriftObserver's constructor no longer eagerly warms the claude
permission-mode cache. The cache self-warms on read(), so the eager call
added nothing but execFile("claude","--help") from every observer that was
constructed and never consulted — measured 162 launches/suite pre-fix, and
92 residual after the createTestApp-only fix because ~72 test call sites
build createApp() DIRECTLY (transport/transcript/env/sessions routes et
al). Removing the eager warm closes the class, not the instance.
2. startup.ts (the real daemon's boot boundary) constructs the observer with
an explicitly warmed cache and passes it into createAppWithWebSocket — the
boot-time pre-warm the constructor used to provide is preserved exactly
for production.
3. createTestApp defaults permissionDriftObserver to a null observer (belt;
also documents the contract for future harness paths).
Receipts: RED pin 2f72b4a31 (mock-level: createTestApp execs nothing).
Measured with a PATH shim: pre-fix full suite = 162 claude launches;
post-createTestApp-fix = 92; post-this-commit, the five heaviest offender
files (incl. transport-routes' 30 direct createApp sites + the observer's own
suite) = 73/73 tests, ZERO launches. Full-suite recount deferred to off-peak
(box at load 37 during the daemon-wedge incident); daemon tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
Founder-priority hotfix FIX 2 (row qitem-20260822230440-da0d2ad6, shape from
row 9e0a051e): createTestApp leaves permissionDriftObserver undefined, so
createApp constructs the PRODUCTION PermissionDriftObserver whose constructor
warms the ClaudePermissionModeCache — execFile("claude","--help") — 162 real
claude launches across 46 concurrent daemon test files. Pin: building the
test app execs nothing; the explicit-injection opt-in path stays open.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
New domain/shell-classifier.ts: one shared SHELL_FOREGROUND_BASENAMES
(includes tcsh/csh, matching session-fingerprinter's vocabulary) plus
isShellForeground(paneCommand, expectedShellPath) handling the login-shell
"-" prefix and accepting the basename of the shell the caller itself chose —
so ANY configured default shell verifies as blank, not just the enumerated
set. The blank verifier now classifies through it with the selected
blankShell as expected; the launcher-local hard-coded set is gone.
The three older per-site shell sets (session-fingerprinter,
seat-identity-reconciler, codex-resume) are untouched — consolidation onto
this module is a tracked follow-on, not a silent rewrite inside a defect fix.
Closes r2 BLOCKING B1 (qitem-20260822225230-4b99ef69) at ba7baa341.
Touched suites 69/69 green (includes the reviewer-deposited tcsh pin);
tsc clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
Pins r2's BLOCKING at ba7baa341 (row qitem-20260822225230-4b99ef69): the
blank verifier's hard-coded set lacked tcsh/csh, so a box whose tmux
default-shell is /bin/tcsh got a false successor_pane_not_blank AFTER the
destructive cutover. Includes the reviewer-deposited tcsh pin found
uncommitted in the worktree (kept verbatim), plus two generalizations: an
arbitrary configured shell whose basename echoes back is a verified blank,
and a login-shell "-" prefix on pane_current_command still reads as blank.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
Root cause (proven live on the 2026-08-22 reinstall wave, four Codex seats,
and reproduced on a scratch tmux server): `respawn-pane` WITHOUT a command
re-runs the pane's CREATION (or last-respawn) command. Adopted/hand-recovered
panes carry `/usr/bin/env PATH=… codex … resume <old-token>` there, so the
fresh-handover cutover booted the OLD context; launchHarness then typed its
fresh command into the resumed TUI, readiness passed, and commit stamped
complete while node-inventory derived the continuity label from a STALE
restore_outcome.
Three changes:
1. TmuxAdapter.getDefaultShell() (show-options -gv default-shell) and the
successor respawn now passes that EXPLICIT shell (fallback /bin/sh) —
never undefined. Side effect: the explicit respawn rewrites the pane's
respawn default, self-healing poisoned panes (verified on tmux 3.6a).
2. Post-respawn blank verify via getPaneCommand: a non-shell foreground is a
structured successor_pane_not_blank refusal — launchHarness never drives a
resumed context, the preserved seat is never killed, no candidate is
registered.
3. The handover commit stamps continuity_outcome='fresh' for fresh mode
(earned: now verified-blank) instead of NULL, so the seat's reported
continuity can no longer be impersonated by a restore stamp from days
earlier. Discovered mode stays NULL (genuinely unknown).
Flips the two legacy pins that asserted command===undefined (they pinned the
defect). All 66 tests green in the two touched suites; RED commit 013324c63.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
Pins the 2026-08-22 reinstall-wave defect (release-notes known issue 14):
tmux respawn-pane WITHOUT a command re-runs the pane's creation/last-respawn
command, and adopted panes carry `codex … resume <old-token>` there — so a
`rig handover --source fresh` successor boots the OLD context while the
commit stamps complete and node-inventory derives continuity from a STALE
restore_outcome.
Four RED tests: (1) respawn must pass an EXPLICIT default shell, never
undefined; (2) a pane that still boots a non-shell after respawn must fail
structured successor_pane_not_blank with launchHarness never driven and the
seat preserved; (3) getDefaultShell unavailable falls back to /bin/sh;
(4) a fresh handover commit stamps nodes.continuity_outcome='fresh', never
NULL. 62 existing tests stay green; the two legacy pins asserting
command===undefined flip in the fix commit.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
The per-seat capture-pane rotation tick rewrote the transcript file
unconditionally every cadence (2s default). Across hundreds of live
transcript files macOS amplifies each temp-write + atomic rename through
fseventsd into a host CPU/RSS storm (measured ~100% CPU / 3.8GB RSS /
344 transcript-path ops in 10s on the parent host).
Fix: compose header + capture bytes once, byte-compare to the file's
current contents (the SAME read already used for SESSION BOUNDARY header
extraction — no extra I/O), and return before mkdir/temp-write/rename
when identical. Atomic-replace-on-genuine-change, null-capture skip,
boundary-line preservation, and timer lifecycle are all unchanged.
The 2s-default cadence and the TUI REFRESH_MS cadence are deliberately
NOT touched here — unchanged-write suppression is the root fix; cadence
tuning is separate follow-on work.
RED-first: two byte-identical ticks previously replaced the file's inode
every tick; the added tests pin inode stability on unchanged content
(and a rewrite on genuine change, and boundary-header preservation).
Refs: qitem-20260822222746-3a64ae43 (evidence qitem-20260822222645-c03191da)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Post-5.2-cut cold-row root-cause (env-vs-product): ENVIRONMENT/timing. Under
full-suite conditions on this 4-core box the real codex successor becomes ready
at ~30s (measured in-suite pass at 29,997ms — 3ms under the old ceiling) vs ~8s
quiet, so readinessTimeoutMs=30000 sat exactly ON the loaded boot time and the
test failed by timing while the product behaved: every failure was the service's
honest not-ready, every green run renders the pinned model verbatim, and the
failure reproduced only in/after full-suite runs (5/5 standalone green incl.
one under synthetic loadavg 29 — CPU alone is not the mechanism; the suite's
process/API contention is). Fix: 90s readiness in the TEST (service default
untouched) + the trust approver extended to cover the same window (an approver
that quits at 25s makes the headroom unreachable in exactly the loaded case).
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
Formal CHANGELOG [0.5.2] entry + docs/releases/v0.5.2.md file, both
authored from the VM-side closing paper (RELEASE-0.5.2-CLOSING-PAPER.md
+ ADDENDUM-1) — not derived from 74 commit subjects. Content structure
matches prior release-notes shape.
Known-limits section carries all 5.3 inheritances the closing paper
lists as user-visible; internal debt items summarized. Migration
wording precise: no migrations added in this release (v0.5.1 shipped
068-071 with 068+071 as a create/drop-if-exists pair; v0.5.2 stays
at 071).
Companion to the Release 0.5.2 version-bump commit below; GH Release
body will use docs/releases/v0.5.2.md via gh release create
--notes-file at ceremony time.
A test system for the product — self-driving scenarios that prove the
product's behaviour through the same commands a person uses. This
release exercises that system for real and ships the reliability fixes
that fell out of doing so.
The headline is B1: the crash-cart fleet-restore conductor. Type bare
`rig` at a dead daemon and reach a truthful cockpit; one Enter restores
the fleet kernel-first with surviving tmux panes adopted (never
clobbered); the triage carries exact per-seat remediation walkable in
one keystroke; cancel is honest; destructive restore is only offered
on positive evidence that adoption cannot succeed. Ten build rounds,
four independent gates.
Alongside: the daemon event-loop no-longer-hangs-under-load fix (with
its measurement); unattended Claude seat handover (outgoing seat writes
a recap and submits its packet automatically, door-proven); `rig
policy` handles adversarial input honestly (three review rounds);
messages carry their machine-of-origin end-to-end; a detector for a
class of Codex model-config drift that caught real cases on landing;
test-integrity work (runner refuses a dirty tree; hermetic test roots;
flaky fixtures isolated); governance-in-code (plan locks are chosen,
not inherited; wake is opt-in); the container tar-file hang cause
eliminated at the seam.
v0.5.2 contains v0.5.1 in full. One lineage, no divergence. No new
migrations in this release (v0.5.1 was 067 → 071 already; v0.5.2 stays
at 071). API surface preserved.
Formal CHANGELOG entry + docs/releases/v0.5.2.md follow in a small
docs commit on top of this release commit.
r1's finding on 44b28260c, repaired at their sized fix: adoptedIds collects only
ok adoptions, so an adopt-failed seat reaches the subset launcher; when the
launcher's own tmux classification proves it LIVE (alreadyRunning), the seat is
running — the earlier failed node for that logicalId is now dropped before the
fold, so triage can never name a running seat and the door's exact-need
acceptance holds in this corner too. Direction unchanged where it matters: a
seat NOT proven live keeps its named failure row.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 757636193164c93bedb928429b28bf91151af592)
Fails against 44b28260c as r1 measured: the stale failed node survives beside
the resumed one, triage names a RUNNING seat, outcome reads partially_restored.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 666bcc167e1cfec809c17aeb3ab705387f470d33)
The round-10 contradiction resolved as stamped (body hash 72757e81, dev-planner
non-author stamp): for a daemon-only crash the conductor now branches per rig —
LIVE panes compose the SHIPPED machinery (ClaimService.reconcileSession per
surviving session, then RestoreOrchestrator.launchNodeSubset over the remainder
for FR-7 per-seat resume verification); DEAD panes and adoptDeps-less callers run
the existing restore composition byte-unchanged (pinned by tests).
Fold rules, exactly the amendment's: attentionRowsFromNodes stays the single
non-OK authority, so adopted rigs land in the UNCHANGED closed union —
fully_restored when no seat needs attention, partially_restored otherwise, with
the exact per-seat needs (incl. the engineered non-resumable seat's --fresh
line) riding the existing triage rows. Mis-probe both ways lands on truthful
failure: probe-DEAD on a live rig still 409s through restore's own guard (no
clobber); probe-LIVE on a dead rig adopts EMPTY and returns not_attempted with
reason+remediation — the subset launcher is never reached without at least one
real adoption anchoring the rig as live. R9 held: adoption touches bindings/
sessions/events only; no queue state anywhere in the branch.
Route wiring composes context deps (sessionRegistry × tmuxAdapter liveness — the
same classification restore's 409 guard runs — claimService, launchNodeSubset
with the H1 adapters+fsOps threading); a degraded daemon missing any of them
gets undefined adoptDeps and behaves exactly pre-amendment. The round-10 door is
pinned in ROUTE form: live panes → adopt + subset verify, restore never called,
triage carries the exact --fresh need, verdict derived mixed.
Suites: conductor 27/27, route 8/8, full daemon 7557+/7568 — the two failures
are pre-existing environment tests (ps-summary latency budget passes in
isolation; seat-handover model-fidelity e2e fails IDENTICALLY on main — real
codex successor never becomes ready in 30s). Typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 44b28260c278cd1294f446464a6f37e7caf641b0)
Strong-form RED (a test failing against the WRONG implementation, per r1's
B12 distinction): 5 of 7 new pins fail on the shipped composition — the door
unit form reads 'failed' where the stamped amendment demands adoption; the
mis-probe, exact-need, subset-refusal and held/failed folds all fall through
to the 409 reading. The two passing pins are the point: DEAD-panes and
no-adopt-deps behavior are pinned byte-identical BEFORE the branch exists.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit ccc8078f1d8bca5814a124016b6ad2a9ad1d70b7)
Round-10 QA door gap 1: the non-zero-generation confirm text went to ViewState.notice, which the
daemon-down cockpit does not render — so the first ⏎ appeared to do nothing (j/k then filled the cmd bar).
Same notice-render seam class as round-2 H4.
Fix: crashCartOpts.confirm carries the message; renderScreen renders a ⏎ CONFIRM RESTORE banner at the TOP
of the cockpit (wrapped so the sentence is not clipped) when armed, so the confirm is visible where the
operator looks and the ⏎-proceed / Esc-cancel affordance is advertised in the cockpit. main.ts sets confirm
on restore-confirm and clears it on proceed/cancel.
Tests (crash-cart-in-shell): the confirm banner + proceed/cancel affordance render in the cockpit; without a
confirm the cockpit shows no banner (RESTORE EVERYTHING remains the primary action). tui full 390/390, tsc 0.
NOTE: this is ROUND 10 gap 1 only. Gap 2 (surviving-panes adopt branch) is desk-ruled in-scope and the plan
amendment is dev-planner-stamped (BUILD GO), but its build (compose the shipped reconcile-session/adopt path)
is deposited for a fresh focused effort — a deep composition not to be rushed at a marathon's tail. The
round-10 re-freeze/dispatch waits on BOTH gaps. NEVER pushed; branch+worktree only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit cf8281ca01d77bd2be266b020cc8e136952f4a26)
Guard verified round 8 (positive-form predicate correct, both live PTY doors green) and found the same
rule one level deeper — in the WALK. At depth-cap exhaustion with cause/errors children still unexplored,
collectTerminalAttempts counted the unresolved WRAPPER's OWN code as a terminal attempt: six wrappers deep,
a code=ECONNREFUSED wrapper whose cause is a code-less terminal → refused → down → RESTORE. Exhaustion
became evidence. Same false-zero shape (missing evidence read as agreement), now in the walker.
Fix (crash-cart-probes.ts): unanimity ranges over FULLY-RESOLVED terminals ONLY. A node with a cause or
non-empty errors[] is a wrapper — it recurses and NEVER counts as a terminal attempt. If depth is exhausted
while a node still has children, record an UNKNOWN (non-refused) attempt — never reinterpret an unresolved
wrapper's own code as terminal evidence. Cycles are detected by IDENTITY (a node already on the current
path) and also recorded as unknown. An unknown attempt has no code, so it fails ECONNREFUSED-unanimity by
construction → timeout/unverified. This closes the WALK family the way round 8 closed the sibling family:
anything the walker cannot fully resolve fails unanimity, so no depth or cycle can promote a fabricated down.
Tests: deep capped wrapper (ECONNREFUSED wrapper past the cap over a code-less terminal) → timeout; a
self-cycle (a.cause=a) → timeout (and it TERMINATES); a mutual cycle (a.cause=b, b.cause=a) → timeout.
Retained: code-less sibling, mixed-timeout, refused+unknown/abort, all-refused, nested-cause, AggregateError,
real-socket, conservatism, bare-wrapper. PROVEN non-vacuous: making exhaustion count the wrapper's code
turns EXACTLY the deep-capped test RED, all others green.
Scope: crash-cart-probes.ts + its test only. cli (2344) / tui (388) unchanged; daemon crash-cart 62 green,
tsc 0. Guard re-runs both live PTY doors + the deep-capped + cycle cases. NEVER pushed; branch+worktree only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit e476d3c5b9467ecb4d3f2370c4abbebdc8b4eebe)
Guard verified round 7 (mixed-timeout fixed, both live PTY doors green) and found one narrower edge — a
LOGIC SHAPE, not a new case. Round 7 implemented "no timeout/abort/unknown sibling" as negated code/name
Sets; a CODE-LESS terminal failure is in no set, so it was erased and `[ECONNREFUSED, code-less Error]`
still promoted to refused → down → RESTORE on ambiguous evidence. No-known-bad-sibling is not all-known-good.
Fix (crash-cart-probes.ts): the classifier is restructured to the guard's predicate in POSITIVE form.
collectTerminalAttempts walks the cause/errors tree and collects the TERMINAL ATTEMPTS (leaf nodes — a
node with a cause or non-empty errors[] is a wrapper and recurses, never counts as a terminal attempt; a
leaf may carry a code or NONE). refused ONLY when (a) ≥1 terminal attempt AND (b) EVERY terminal attempt
is recognizably ECONNREFUSED. Anything else — timeout, abort, other code, code-less, unrecognized — stays
timeout/unverified. The positive form closes the whole family: there is no complement left to miss.
(This subsumes the prior abort/timeout middle branch — the restructure is the guard-mandated shape change,
noted so it reads as intentional, not an oversight.)
Tests: refused + a CODE-LESS terminal sibling → timeout; a bare 'fetch failed' wrapper with no cause →
timeout (wrappers are not terminal attempts). Retained: mixed-timeout, refused+unknown, refused+abort,
all-refused (over-narrow guard), nested-cause, AggregateError, real-socket, conservatism. PROVEN
non-vacuous: swapping in the round-7 negated-Set classifier turns exactly the code-less-sibling test RED,
all others green.
Scope: crash-cart-probes.ts + its test only. cli (2344) / tui (388) unchanged; daemon crash-cart 59 green,
tsc 0. Guard re-runs both live PTY doors + the code-less-sibling case. NEVER pushed; branch+worktree only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 3b9e1b78584d3cb320ae8b64ba9bdb7b0122205a)
Guard verified the round-6 fix (both live PTY doors green: dead+refused → cockpit+RESTORE; dead+hanging →
cannot-verify) and found one narrow edge of that fix itself: classifyProbeError promoted on refusal-FIRST
precedence, so a MIXED multi-address AggregateError (ECONNREFUSED on one address + ETIMEDOUT on another)
became "refused" → confirmed-down → offered RESTORE EVERYTHING on AMBIGUOUS evidence. The detector contract
is explicit: DOWN requires positive refusal evidence and a timeout NEVER promotes.
Fix (crash-cart-probes.ts, precedence only): "refused" now requires ECONNREFUSED present AND NO non-refused
sibling (no timeout/abort/unknown code alongside it). A mixed aggregate → timeout/unverified. Cause-walking
is unchanged; the conservative default is unchanged; refusal no longer wins by precedence.
Tests (crash-cart-probes.test.ts): MIXED ECONNREFUSED+ETIMEDOUT → timeout; refused+unknown sibling → timeout;
refused+abort sibling → timeout; and an all-refused multi-address aggregate STILL → refused (the fix does
not over-narrow). PROVEN non-vacuous: reverting to refusal-first precedence turned the 3 ambiguous tests RED
while the all-refused case stayed green.
Scope: crash-cart-probes.ts + its test only. cli (2344) and tui (388) unchanged; the classifier is used only
by the crash-cart probe, covered by the 57-test crash-cart suite (probes 15 + detect + emit + conductor +
route). tsc daemon/cli/tui 0. Guard re-runs both live PTY doors. NEVER pushed; branch+worktree only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 26ac9d72abb6352fd5351027e8ac0e1098d11d6f)
Guard re-verdict NOT-CLEAR on 4261d3f16: all four prior blockers VERIFIED closed, but the guard replayed
the REAL public composition and found one layer deeper — bare `rig` + daemon down still shows no cockpit.
Node/Undici global fetch rejects a refused TCP with an OUTER `TypeError {message:"fetch failed",
code:undefined}` and the real `ECONNREFUSED` nested in `cause` (and, for a multi-address host, an
`AggregateError` whose `.errors[]` carry it). `classifyProbeError` checked only the OUTER `code`/`name`,
so a provably-dead daemon (dead pid + refused socket — positive DOWN evidence by the detector's own
contract) read "timeout" → "unverified" → the cannot-verify screen with NO restore. The chain otherwise
was correct (detect: refused + pid-dead → down → cockpit); the single broken link was the classifier.
FIX (crash-cart-probes.ts): classifyProbeError now WALKS the rejection's cause chain + AggregateError
`.errors[]` (bounded depth) — ECONNREFUSED anywhere → "refused" (the strong down signal); abort/
connect-timeout → "timeout"; else "timeout" (conservatism preserved, NOT weakened to a fabricated down).
Tests — the round's own lesson (a stubbed error shape is a claim about Node; buy it with a REAL socket):
- daemon `crash-cart-probes.test.ts`: the NESTED-cause + AggregateError shapes → refused; a nested
connect-timeout stays timeout; and a PRODUCTION-PATH test using a REAL refused socket (bind an ephemeral
port, close it, connect via the real global fetch) → "refused". All ALWAYS RUN (no skipIf). Proven
non-vacuous: reverting the classifier turned the nested + AggregateError + real-socket tests RED.
- cli `front-door.test.ts`: the BLOCKER-1 production-path test no longer stubs a constructed
DaemonConnectionError — it drives the REAL DaemonClient against a REAL closed socket (r1's exact rule).
Chain now proven at every link: real refusal → refused (new real-socket test) → down (existing detect
tests) → cockpit + RESTORE (existing emit/render). SCOPE: daemon source (crash-cart-probes.ts) + cli test.
Verified: cli full 2344/2344, daemon crash-cart suite 53 green (probes 11 + detect + emit + conductor +
route), tsc daemon/cli 0. tui unchanged since round 5 (388). Full daemon suite: environmental flakes under
concurrent load (crash-cart never flaked) — uncontended re-run confirmation to follow. NEVER pushed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit f18943027486f24994837153daa6c7b9a63553ea)
Guard NOT-CLEAR on b23965e3: the two reviewers proved the changed structure; the guard asked whether the
advertised user journey survives the REAL public composition from bare `rig`, daemon down, into the
crash-cart. It did not. Four bounded repairs, RED-first against the guard's own source discriminators.
SCOPE NOTE: B1+B4 touch CLI source and B3 touches daemon source — the four-round "daemon/CLI-unchanged"
claim ENDS; the reviewer delta scope widens to CLI + daemon this round.
BLOCKER 1 (the big one) — bare `rig` + daemon down now REACHES the crash-cart. front-door.ts
openMissionControl() exited on ANY non-ready probe, so launchTui() never ran and the cockpit was
unreachable from its entry point (my own front-door/tui-command tests locked the bug green). Fix: when the
transport is not healthy (connect → down, timeout → unverified), LAUNCH the TUI — its own `rig crash-cart
--json` probe makes the honest confirmed-down vs unverified distinction. A HEALTHY-transport diagnostic
(daemon up but cwd/command/permission drift) still degrades+exits (launching would MASK the drift).
Corrected the tests that encoded the bug + added a PRODUCTION-PATH test: the REAL probeFrontDoor
composition (only the socket stubbed to ECONNREFUSED) → the TUI launches. The compiled-wrapper tmux test
now proves the PUBLIC bin launches the TUI (chrome renders), isolated OPENRIG_HOME so it never reads the
live rig; the in-process belt was removed (its premise — a fast in-process daemon-down degrade — is
replaced by launch, and forcing a TTY in-process would spawn an interactive TUI).
BLOCKER 2 — the done header wore ✓ on EVERY verdict ("✓ FLEET RESTORE: all_failed"). Now the glyph is
DERIVED from the verdict: ✓ only for all_fully_restored; ✗ for all_failed; ⚠ for none_attempted/mixed.
BLOCKER 3 — awaiting-decision triage dropped the orchestrator's exact error/remediation (the concrete
`--fresh <logicalId>` command). attentionRowsFromNodes() now PRESERVES node.error for that status,
falling back to the generic sentence only when the node carried none — the "seat + exact need" door contract.
BLOCKER 4 — the CLI verbs `crash-cart restore-fleet` / `cancel-fleet` breached the "no new CLI verb v1"
fence (plan R10/Boundaries) and were dead since round 3 (the TUI drives the R2-sanctioned daemon route via
the client). Removed both verbs + their machinery + the fleet-verb test file; the base read-only
`crash-cart --json` verdict verb and the daemon route are untouched.
Verified: cli full 2344/2344 (−6 = the removed fleet-verb test), tui full 388/388, daemon crash-cart 20+6
green, all typechecks 0. Full daemon suite: running (B3 is the only daemon change; conductor tests cover it).
NEVER pushed; branch+worktree only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 4261d3f1691f7af3b81b179c8d37355353dfbbf0)
Round-3 verdicts: r1 CLEAR (reviewer-of-record; 1 LOW) + defers TUI to r2; r2 NOT-CLEAR on two render/
input-seam gaps. r1 named them one CLASS with the two already fixed: the UI advertises a capability the
state cannot honour. Per orch, this round closes the CLASS, not the two instances — round 4 is the last.
TUI-only; daemon/CLI source unchanged since 7c107f4e5 (r1's async-root + H5 clears transfer).
THE CLASS FIX — the restore key handling is now a PURE reducer (restore-input.ts: restoreKeyAction) that
main.ts executes; the render advertises affordances; a test extracts what each state ADVERTISES from the
rendered text and asserts each ACTS (r1's design — enumerating from handlers misses advertised-but-unwired
keys). Audit matrix (phase × render-state × input), every cell honoured:
running/normal : c cancel ✓ · ↑↓ scroll ✓ (overflow) · claim "restore continues" true
running/cancelled : NO c cancel offered · says "cancellation requested, current rig will finish" (HIGH-1) · c swallowed
detached/normal : r reattach ✓ · c cancel ✓ · dismiss ✓ · ↑↓ scroll ✓ (overflow) · "CONTINUING on the daemon" true
detached/cancelled: NO c cancel offered · says "cancellation requested — reattach to confirm" · c → reattach
done : (cancelled) tag ✓ · ↑↓ scroll ✓ (overflow) · any key dismisses
- HIGH-1 (r2): running/detached now read vm.cancelled — an accepted cancel is VISIBLE and the plain
`c cancel` is dropped; the render SAYS cancellation is requested and the current rig will finish.
- HIGH-2 (r2): scroll works in EVERY phase (restore-scroll.ts, phase-agnostic), so the "↑↓ scroll" the
footer advertises when overflowing is real and the lifecycle action row below the fold is reachable —
tested through the reducer (the input branch), not renderScreen(restoreScroll:max) alone.
- r1 LOW: detached `c` notice now "cancel requested" not "cancel sent" (no POST until reattach; the
biting case is the unreachable daemon that caused the detach).
Tests: crash-cart-restore-input.test.ts drives the advertise→act matrix with NON-VACUOUS coverage (every
affordance type advertised+honoured by some state) + the converse (cancelled states drop c cancel and do
not honour it) + running/detached scroll reachability. tui full 385/385, tsc 0; daemon crash-cart 25 +
cli 6 regression-green (no daemon/cli source changed). NEVER pushed; branch+worktree only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit b23965e305a31782203a0372111cf2a00991d436)
Round-2 verdicts: r1 CLEAR then SELF-REGRADED to NOT-CLEAR (ceding TUI conformance to r2); r2 NOT-CLEAR.
Two operator-path gaps in the now-connected lifecycle, both locked-requirement conformance, RED-first
against r2's exact probes + r1's two refinements. Daemon/CLI source unchanged since 7c107f4e5 (TUI-only).
HIGH-1 (R8 — cancel HONESTLY RENDERED, must stay OPERABLE):
- The poll ceiling no longer returns a frozen "running" frame — it DETACHES (phase "detached"): an
explicit, operable state that says the restore is CONTINUING on the daemon (r1's keeper), never a
live-looking screen with a dead cancel key. (r2's maxPolls:2 probe → detached, tested.)
- r1 refinement 1 (false confirmation): detached is a distinct phase, so `c` no longer shows a lying
"cancelling…" with no loop behind it — detached `c` sets the flag and REATTACHES, so the resumed
driver POSTs cancel and the operator SEES it land (observable, not a silent POST — r1 q1).
- r1 refinement 2 (the likelier inert path): the poll loop is now GUARDED — one transient poll error is
tolerated and retried, never rejecting the driver / clearing the view / false-reporting failure; only
a sustained streak (maxConsecutiveErrors) detaches. The counter RESETS on a good poll (r1 q2), so
scattered blips over a long restore never accumulate to a detach.
- `r` reattaches (resume the live view against the still-running attempt, skipping the kick); `c`
cancels+reattaches; any other key dismisses (restore continues on the daemon).
HIGH-2 (R5 — one KEYBOARD-WALKABLE aggregate list):
- The restore triage list is now vertically SCROLLABLE. crashCartShell reports contentMaxOffset and
windows the (wrapped) content at a scroll offset; main.ts's done view scrolls with ↑↓ / j-k. r2's exact
probe (28 rows at 120×32) proves the tail need is off-screen at offset 0 and REACHABLE at max offset.
A list that fits is unchanged (offset 0, maxOffset 0).
Verified: tui full 372/372, tsc 0; daemon crash-cart 25 + cli 6 regression-green, tsc 0 (no daemon/cli
source changed since the round-2 full-suite run). NEVER pushed; branch+worktree only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit c25ba1ca3966467c043dc441b6dced2d73410f6f)
r1 CLEARed the async root shape (untouched here); r2 NOT-CLEAR proved the operator layer was hollow —
the TUI delegated the whole lifecycle to a buffered child, so cancel/progress/triage never reached the
operator. Desk reversed the cancel re-scope: BUILD the operator lifecycle, no deferral. All five r2 HIGHs
+ r1's stored-verdict, RED-first against r2's probes:
HIGH-5 / r1 — verdict is DERIVED at read (GET handler), the stored FleetAttempt.verdict field removed
(ARCH-RULING Q2 / R6 — no second mutable truth). Daemon route test asserts verdict == f(returned counts).
HIGH-3 — TRUE progress from the rollup stream. New restore-lifecycle driver (crash-cart/restore-lifecycle.ts):
the TUI OWNS kick/poll/cancel via DaemonClient, retains the attempt id, and emits a frame EVERY poll — a
mid-run frame is observable, not only at completion. Proven at two altitudes: the driver test (running
frames with progressive counts) and an in-shell renderScreen test (RESTORING FLEET frame renders).
HIGH-1 — cancel reachable end-to-end: cockpit 'c' key (main.ts) → the driver POSTs the cancel endpoint
with the retained id; and a `rig crash-cart cancel-fleet <id>` CLI verb for the off-cockpit path. Driver
test asserts cancel hits the endpoint once with the id.
HIGH-4 — the SHIPPED triage renderer (triage.ts, previously unused) is now wired as the keyboard-walkable
list: attention seats + not_attempted remediations, each need on its OWN row. The shell wraps long needs
to the pane (new opt-in wrap) so the EXACT need is never clipped off the edge — r2 found both the need and
the remediation ABSENT in the old width-clipped footer; both now survive (in-shell test at 120 cols).
HIGH-2 — truthful confirmation. The restore is parameterless (no freshLogicalIds), so the confirm no
longer PROMISES fresh-prime; it describes the awaiting-DECISION the restore actually produces (fresh-prime
or skip, in the triage list) and names the deltas (R7 — no silent downgrade). Pure restoreConfirmMessage()
with a test asserting it never claims "will fresh-prime".
Dead code removed (the built-but-not-connected smell r2 named): restore-drive.ts (buffered child) +
restore-rollup.ts (clipped one-line summary) and their tests.
Verified: daemon crash-cart conductor 18 + route 7 = 25; cli restore-fleet/cancel 6; tui restore-lifecycle
3 + restore-render 6 + in-shell +6 + keys 6 + gate 5. Full suites: cli 2350/2350; tui 364/364; typechecks
daemon/cli/tui all 0. Daemon full: crash-cart deterministic-green; the transient failures under concurrent
load (ps-summary latency budget, slow-op-recorder shutdown timing) PASS uncontended (25/25), and
seat-handover-model-fidelity-e2e is the pre-existing real-codex env failure that reproduces on clean main
1f6d82c71 with zero B1 refs — none are B1 regressions. NEVER pushed; branch+worktree only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 7c107f4e525364400be1844cdee0c2b2691c5a18)
Reworks the crash-cart RESTORE CONDUCTOR from the review-NOT-CLEAR candidate (68770f255,
built-but-not-connected) to the locked async on-commit shape. Five findings wired against the
reviewers' production-path discriminators (a helper's green was never the bar):
r1 ROOT — async rollup stream (not a timeout raise). The route now starts the conductor in the
BACKGROUND and answers ON-COMMIT (202 + fleetAttemptId); a pollable status endpoint carries the
rollup as each rig completes. RestoreConductor.restoreFleet emits onRigDone per rig. A real
seconds-per-seat fleet restore no longer races any request timeout — the operation is unbounded by
construction, so the response is progressive rather than blocking. CLI polls to completion; TUI drive
returns the rollup.
H1 — the route threads the app's runtimeAdapters + fsOps into restore() (else the shipped
orchestrator fail-closes a pod-aware resume to awaiting-decision).
H2 — ⏎ CONSULTS evaluateOneClickGate: single-keystroke only when zero-generation; a rig with
non-resumable seats arms a confirm naming the deltas (next ⏎ proceeds/fresh-primes, Esc cancels).
H3 — cancel is reachable + effective end-to-end through the real route: a cancel endpoint sets the
running attempt's flag; the conductor honors stop-before-next-rig (proven: later rigs not_attempted,
beta never restored). (Operator-facing cockpit cancel TRIGGER deferred — the TUI delegates the poll
to the CLI verb; a cockpit key needs the TUI to own the kick/poll/cancel loop. Flagged for r1/r2.)
H4 — the TUI no longer discards the rollup: parseRestoreRollup + summarizeRestoreRollup surface
verdict + counts + attention needs + not_attempted reason/remediation on the notice line (survives
the post-restore refresh to normal TUI, when the cockpit is gone).
R3 — not_attempted carries reason + remediation (no-usable-snapshot and cancel-skip), never blank.
Verified: daemon conductor 18 + route 6, cli restore-fleet 4, tui keys 6 + rollup 4 GREEN. Full
suites: daemon 563/564 (the 1 failure = seat-handover-model-fidelity-e2e, a pre-existing real-codex
env failure that reproduces on clean main 1f6d82c71 and references zero B1 code), cli 2348/2348,
tui 361/361. Typechecks clean (daemon/cli/tui). NEVER pushed; branch+worktree only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit cf52dfba602018ab6de842f7131e7f0de2cd3ed3)
CLI: "rig crash-cart restore-fleet [--json]" POSTs /api/crash-cart/restore-fleet via
the daemon HTTP client (fail-closed if the daemon is not up) and renders the FleetRollup
(verdict + per-rig outcomes + triage rows). Reads --json from optsWithGlobals (the parent
crash-cart command also declares it). 3 pins RED-first.
TUI: main.ts performCrashCart splits "restore" from "start-daemon" — enter RESTORE
EVERYTHING now DRIVES the conductor via driveRestoreEverything (start daemon THEN
"rig crash-cart restore-fleet"), replacing the prior start-only labeled seam
(D-cart=CONDUCTOR); re-probes; a start failure never proceeds to restore.
Completes Atom D. cli+tui tsc 0; cli restore-fleet 3/3, tui crash-cart 9/9 GREEN. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 68770f255fdb0b7c286731cc697217640cca5c6d)
driveRestoreEverything: the enter primary action sequences the s step (start daemon)
THEN drives the C1 conductor; a daemon-start failure never proceeds to restore
(propagates). Deps injected (main.ts supplies execFile + the rig verb). RED-first:
2 pins watched RED then GREEN, tui tsc 0.
Compose-fence win: Atom A (daemon-down discovery) + the whole crash-cart TUI surface
(discovery/gate/triage/render/keys) ALREADY SHIP; the only gap was that main.ts
treated enter RESTORE as start-daemon-only (a labeled seam). REMAINING Atom D: a
"rig crash-cart restore-fleet" CLI verb (POST /api/crash-cart/restore-fleet) + wire
main.ts's restore action to exec it via this drive + the one-click gate. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 14fa6f027808632895170f42eee15f7a7deb8d81)
attentionRowsFromNodes maps a rig's restore nodes -> triage rows: seats needing
operator action (attention_required = live runtime prompt / awaiting-decision =
unresumable, choose / failed = the error), each with its EXACT need; running seats
excluded, never fabricated. Threaded: createDefaultRestoreRig surfaces per-rig
attention from the restore result's nodes -> ConductorRigResult.attention ->
aggregateFleetRollup unions them into attention_required (a view over the sequence).
The route now returns real triage rows (door-test: non-resumable seat on triage
with exact need). RED-first: mapper 2 pins watched RED then GREEN; 18/18 total,
daemon tsc 0. NEXT: Atom A (compose shipped loadCrashCartDiscovery) + Atom D (TUI
⏎). Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 422bf8fc170b736effde5abc747fb3d3eb57f7a1)
aggregateFleetRollup: pure aggregation over the conductor's per-rig sequence →
{ counts: closed-union Record (not_attempted first-class, never folded into
failed), sequence view (carries receiptRef), attention_required (union of per-rig
attention, a view) }. deriveFleetVerdict = DERIVED f(counts), NEVER stored (a
stored verdict could drift). The route now returns the FleetRollup + derived
verdict. RED-first: 4 aggregation pins watched RED then GREEN (15/15 total),
daemon tsc 0. NEXT: wire attention_required from the shipped per-rig restore-check
projection; then Atom A (discovery) + D (⏎). Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 95ab51c07854e5f2e2a58d51a7b140598a7f6523)
Thin route glue assembling the tested conductor pieces from context deps
(rigRepo/snapshotRepo/restoreOrchestrator): listRigsInKernelFirstOrder +
createDefaultRestoreRig(findLatestRestoreUsable, RestoreOrchestrator.restore) ->
RestoreConductor.restoreFleet -> the ordered per-rig sequence. Registered in
server.ts. RED-first: 2 route pins watched RED then GREEN (11/11 total), daemon
tsc 0 (incl. server.ts). Atom C aggregates the sequence into the fleet rollup;
Atom D wires the TUI. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit e3ecf9e2a36757c4be3e401db2cd55d4f3823f8b)
listRigsInKernelFirstOrder: the kernel rig (name 'kernel', the supervisor)
restores FIRST, then the rest in listRigs order; no kernel rig -> all rigs, none
fabricated as kernel. This is the listRigsInOrder source the conductor consumes.
RED-first: 2 pins watched RED then GREEN (9/9 total), daemon tsc 0. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit bdcf6e3666f8df948c3aaecc6ea655e7cae2138a)
createDefaultRestoreRig composes the shipped seams: findLatestRestoreUsable(rigId)
→ (null → not_attempted, restore never runs; never a silent substitute) →
RestoreOrchestrator.restore(snapshot.id) → its rigResult; ok:false with no result →
failed; onAttemptStarted's event seq → receiptRef (ledger lineage). Structurally
typed deps keep the conductor decoupled/testable. RED-first: 3 pins watched RED
then GREEN (7/7 total), daemon tsc 0. Plan-locked B1. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit f80dccfed4001dc6bec1143d49449359980ddead)
The daemon-side batch restore conductor's CORE: restores rigs kernel-first
(the founder's order), best-effort continue (one rig's failure never halts the
fleet), stop-before-next-rig cancel (the in-flight rig completes; later rigs
become not_attempted), and receiptRef pass-through. Per-rig restore is INJECTED
(restoreRig dep) — the conductor COMPOSES the shipped restore, never re-authors
it. The default dep (findLatestRestoreUsable + RestoreOrchestrator.restore),
the route/verb wiring, and EventBus receipts follow in this atom; A/C/D per the
locked plan.
RED-first: 4 pins (R2/R5/R8 + receiptRef) watched RED-by-name then GREEN;
daemon typecheck exit 0. Plan-locked B1 (content-hash 84401cd4). Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit d6ad8867df06a3e8ec78424452c577fa9c1940e6)
Founder-ruled engine-independent leg (2026-08-21 ~18:40Z; containers land 5.3,
this fix lands now). Root cause on record in the scoping report: the L6 runbook's
inline execFile invoker predated the stdinFrom tar-pipe contract (9d2f1f2cc) and
ignored it — an open never-fed pipe stdin left the in-container `tar -xf -`
blocked on read forever (row 42576855, the 08-12 seven-minute hang).
makeRealStagingDocker implements the contract as committed, tested code:
- no stdinFrom → stdin IGNORED (closed), so a stdin-reading child terminates on
EOF (the hang class dies even on a misused step);
- stdinFrom → two processes, producer piped into consumer with EOF forwarded,
DUAL-EXIT checked (a failing tar into a succeeding exec no longer masks an
empty stage), EPIPE on the wiring swallowed and the producer's death reported;
- consumer-exits-early → producer killed and the step FAILS named (this
invoker's own first cut hung exactly there — the producer's stdout backed up
against a pipe nobody drained; the RED test caught it at 20s);
- every step bounded by a timeout (default 120s) that kills both and returns a
NAMED failure, never an unbounded wait.
The runbook now IMPORTS the helper — an invoker inlined in a document drifts
behind the code contract with nothing to fail. 8/8 hermetic (sh stand-ins, no
engine) + 19/19 adjacent container suites; typecheck clean. NO live containment
claim: the real-engine proof (docker vs Apple container exec-stdin semantics)
explicitly waits on the 5.3 engine-substrate ruling, and the helper's docs say so.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 1f6d82c71df86d8fba149c0cfc99d18bc003baaa)
Hermetic (sh stand-ins, no engine). RED by construction: the helper module does not
exist yet (vitest: 1 failed file, no tests collected). The hang-class discriminator
is pinned: a stdin-reading child must terminate (closed stdin / propagated EOF),
which the L6 runbook's inline execFile invoker — the 08-12 tar-hang cause — cannot pass.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 6831d20001abb6c8dd549b557e8b0df16be359d6)
Founder ruling 2026-08-21: the web UI (packages/ui) went best-effort experimental
at the 0.5.0 TUI pivot; releases must not spend gate time or block on it. The
vitest:ui leg is removed, its exclusion-ledger resident (table-crash STRESS)
dies with it, and the absence is pinned by a test so it reads as ruled, not
forgotten. npm run test:ui remains for manual runs; it gates nothing.
(cherry picked from commit f12db236a3a9d696956b07ac08cbf0f439425cb9)
Sixth candidate, closing r2's round-5 HIGH: the retention branch returned
the cached {id,path,source} verbatim, so a selected id whose cached path
died while a same-id fallback path stayed readable sent the reader to a
nonexistent file — the temporal form of the dead-first-path defect the
adjacent pin already named. Remedy exactly as r2 sized it: retain the ID,
rebind path/source from the current poll's readable entry before returning
AND caching. Their three-poll discriminator is the pinned test
(first-observation → select at raw path → raw dies, derived readable →
returns derived). 38/38 per-file (12/14/12), typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 5b5b78289c130d5c9a1f6a2787b7e2f107a098dd)
Fifth candidate, closing r2's round-4 HIGHs and adjacent pin:
- HIGH-1: LiveClaudeRecordSelector is STATEFUL per seat — advancement is
observed across POLLS (the full poll interval is the observation window,
so every intervening write counts; the 1.5s in-poll probe and its
truth/indeterminate oscillation are gone). Exactly one record advancing
since the previous poll selects and RETAINS until the candidate id set
changes; r2's forgotten-advancement discriminator is the pinned test
(resolve on poll 2, retain through idle poll 3). First observation of a
disagreeing set is INDETERMINATE by construction and says so. The
unreadable-contender absolute rule survives, including after a prior
resolution.
- HIGH-2: the expiry is a BEHAVIORAL sentinel, not a filename:
rigspec-schema exports SPEC_VALIDATION_CAPABILITIES with the registry
contract in its doc-comment; modelsMatch consults it at RUNTIME (the
bridge is dead code once "model-pin-canonicalization" registers) and the
pin test goes red until the constant is deleted. Landing the advisory
behavior without the registration is the residual trust gap — named: the
registry line belongs on the 5.3 cut sheet.
- Adjacent pin: all paths are retained per id; a dead sidecar path no
longer masks a readable derived path for the same id (pinned test).
Live specimen probes, reported as measured: three simulated polls on the
idle seat walk the honest progression (first-observation → no-advancement,
both named); production's 60s window resolves at the seat's next write and
then retains. 37/37 with per-file collection confirmed (round 4's grep hid
a collection failure; counted per file ever since); typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit f9f7d0af7ee08cb345a63e0cea93ae0348ea1de6)
Fourth candidate, closing r2's round-3 HIGHs:
- HIGH-1: OBSERVED ADVANCEMENT replaces the recency window. An unreadable
contender with a different id ALWAYS yields a named INDETERMINATE — even
when the readable record is actively advancing (it may be the current
record before its first readable byte; r2's boundary case is the pinned
test). Multiple readable ids take a two-sample probe (stat, 1.5s, stat):
exactly one record advancing (size or mtime) wins; none advancing (idle)
or several advancing are named INDETERMINATE, re-probed each poll. r2's
two round-3 discriminators (unreadable contender + minute-fresh stale;
61s/60s neither advancing) are both tests and both fail closed. Unanimity
still resolves without a probe.
- HIGH-2: the bridge expiry is now EXECUTABLE — a tripwire test walks
packages/daemon/src and, the moment any *spec-validation-advisory* module
lands (the desk ruling's own name for the 5.3 leg), requires the bridge
constant to be EMPTY: landing the advisory turns this suite red until the
bridge is deleted. Pre-advisory, the same test pins exactly the one
measured pair. Dormancy-by-data (canonical relaunches) is unchanged.
Live specimen probes, reported as measured: three consecutive runs on
dev.planner return the named INDETERMINATE ("no record advanced during the
1500ms probe") — the seat is idle and its dormant contender will never
advance, so resolution happens when a write coincides with a probe window
and then settles the generation permanently. That convergence-vs-activity
coupling is the priced tradeoff of in-poll sampling; the faster variant
(cross-POLL advancement via cached prior samples, a 60s observation window)
is named for review rather than built unpriced. 37/37 across the three
suites; typecheck clean; the missing-async collection failure this round's
first cut hid from a grep is named here rather than elsewhere.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit ab497e80d7b245cac30f7fcb3cb55477957a2e00)
Third candidate for the B8 record-liveness requirement, closing r2's two
HIGHs on 6f616fa65 plus their alias addendum (desk-ruled 05:03Z):
- HIGH-1: "freshest file that exists" is not "record being written now".
selectLiveClaudeRecord is now FAIL-CLOSED: unanimous candidate ids resolve
with no recency demand; DISAGREEMENT (differing readable ids, or an
unreadable contender with a different id) resolves only when the winner
was written inside a 10-minute liveness window — otherwise a NAMED
INDETERMINATE listing every candidate, readable and dead. r2's
discriminator (unreadable current contender + readable stale record) is
the new test and returns INDETERMINATE, never confident stale truth.
- HIGH-2 + addendum: EXACT comparison restored — generic token containment
let one pin bless multiple distinct models (their "codex" cases are
pinned false). The full ruling arc is recorded at the comparator. One
provider-specific MIGRATION BRIDGE carries the transition: exactly the
measured pair fable -> claude-fable-5, mechanically self-expiring (inert
by data once the three alias-pinned seats relaunch canonical; removal
pinned to the 5.3 spec-validation advisory otherwise); the bridge test
asserts nothing else maps ("opus" deliberately NOT bridged — unmeasured).
Live specimen re-probe: selection still picks the actively-written
1cb8cd8b record (disagreement present, winner minutes-fresh — inside the
window); effective claude-fable-5 vs pin fable matches via the bridge.
35/35 across the three suites; typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 23f4565b0222f7dca00114c755dbfe03b75ad541)
D-a, second candidate — the first (067eb0995) was withdrawn by its author
before review and NOT-CLEARed independently by r2 on the same measurements:
its pane-argument join selected the STALE record on the very specimen
(claude rolls session ids internally; the launch argument records what was
LAUNCHED, hooks report what RUNS — the slice-13 lesson met from the other
side). Both directions of the trap are now measured live in one night: on
the B16 specimen the sidecar was stale and the argument right; on the D-a
specimen the argument was stale and the sidecar right. No single pointer is
generation-true.
The join is now RECORD-LIVENESS: gather every candidate session id the seat
is associated with (sidecar, newest registry hook row, pane launch
argument), resolve each to its transcript, select the freshest-mtime one —
the record being WRITTEN is the occupant. Zero readable candidates = named
INDETERMINATE listing every dead candidate. Codex keeps the pid-to-logs
join (it reads the live process's own log rows — no selection needed). The
module's own wrong first framing is struck visibly in its header. Also
carried: the withdrawn candidate's monitor plumbing survives (named reasons
ride the UNCHECKED warning + pendingSeats verbatim); the ledger-null
finding stands (77/77 tenures, r2-corroborated).
AND THE FIX EXPOSED THE NEXT DEFECT IN LINE: with the claude leg finally
reading the true record, modelsMatch's deliberately-exact comparison would
false-proclaim on every healthy pinned claude seat — the fleet's real pins
are SPEC ALIASES (live census: nodes.model = 'fable' and 'gpt-5.6-sol'),
the adapter passes the alias, the runtime reports the full id
(claude-fable-5). The exact-match ruling is REVERSED on this evidence,
struck visibly at the comparator: exact id OR pin-as-whole-hyphen-token
(fable ~ claude-fable-5; wrong family, partial ids, and numeric-only pins
still fail). Flagged prominently for review since r1 endorsed the old
semantics.
Eyes-on receipt on the live specimen: selection picks 1cb8cd8b via
liveness (mtime minutes old) over the dormant argument record; effective
claude-fable-5 vs pin fable now MATCHES — no false proclamation, and the
dispatched "opus-5 unpinned" premise is corrected in the record
(dev-planner corroborated from their own files). 30/30 across the three
suites + comparator tests; typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 6f616fa65f256595a36f1b83edc384fdff787045)
D-a (urgent, B8-family): the effective reader keyed on the name-keyed sidecar
(claude) and the newest registry token (codex) — and on dev.planner BOTH
went stale TOGETHER (sidecar session 1cb8cd8b == newest sessions row, while
the live pane ran --session-id daaeb7b4), so the detector quoted the OLD
generation's transcript and MASKED a real divergence on its first night.
Third cross-generation-keying specimen tonight (B16's sidecar overwrite,
slice-13's resume_type carry-forward, now this).
The join is now the LIVE PANE PROCESS — the only record that IS the
occupant: pane pid -> descendant runtime process -> claude --session-id
argument / codex thread-via-logs (async, the F1 discipline). The sidecar
becomes corroboration only; on mismatch the CURRENT transcript is derived
beside it (same seat, same project dir) and read — the stale record can no
longer supply the answer. Every no-answer path returns a NAMED reason that
rides the UNCHECKED warning and pendingSeats() verbatim (honest
INDETERMINATE, never a silent old-gen read).
FINDING FOR THE ROW, measured live: the fix shape's named join —
occupant_tenures.native_session_id_at_boot — is NULL on every tenure on
this box (registerSession mints via mintOccupantTenureBestEffort with null);
the ledger join is unpopulated in practice, which is why the pane join
carries this fix. Backfilling the ledger is separate work.
Eyes-on receipt on the live specimen: live join resolves daaeb7b4; sidecar
mismatch detected; derived live transcript reads claude-opus-5 (the real
unpinned state) where the old path read claude-fable-5 (the proclaimed
mask) — the exact orch-verified defect, reproduced then fixed. Suites:
current-generation-record 4 new tests (specimen-shaped process table, named
reasons per failure leg), monitor reason-threading updates, 26/26 across
the three model-divergence suites; typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 067eb0995c1343ae63da68bfe6ea029d2c0e0a15)
B16 rework (r2's live door NOT-CLEAR): deliverRestorePacket did sendText
then IMMEDIATE C-m, while the proven transport path settles 200ms between
paste and submit (session-transport.ts "Wait 200ms", spike-proven). Live
result: the multi-KB packet sat STAGED-UNSENT as collapsed paste blocks in
the successor's input box — r2 measured 46s until their manual Enter — while
the handover committed complete. The product shipped the exact
staged-not-consumed class this rig has measured by hand all week.
The fix is the shared sequencing at the one seam: settle via the service's
injectable sleep (now stored on the service; it previously only forwarded
deps.sleep to the launcher) between sendText and C-m, comment citing the
transport contract and the live measurement. New test pins the ORDER
(send_text -> sleep(200) -> submit) through an injected sleep spy.
Receipts: handover service + cutover e2e + packet suites 53/53; typecheck
clean. The model-fidelity e2e fails in THIS worktree AT MAIN TOO (twice on
the branch, once detached at ac308df49, same 30s real-codex readiness
timeout) — environment under current box load, not candidate-caused; named
rather than hidden. Door re-proof per r2's standard (successor JSONL
receives the packet with NO manual key, live daemon) needs the next interim
roll after fold.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 9618c41f6ddf407e82368d17f758bf04682a2ad8)
(cherry picked from commit 48ac0405e11b2b217272b668f55287f95d6a9b1f)
r1's B4 follow-on (F2): the sidecar's load->mutate->rename had no concurrency
guard, so interleaved writers could erase each other's writes — benign for a
binding (TOFU re-learns) but a dropped CONFLICT record is the silence the
known_hosts lesson forbids. recordHostObservation now holds an mkdir-based
advisory lock (atomic everywhere) across the read-modify-write, with bounded
retries, stale-lock takeover after 2s (a crashed writer never wedges the
sidecar), and fail-open to an unguarded write if the lock cannot be won —
availability over the rare loss, and the conflict re-records on the next
contradicting observation. Tests: real 6-process parallel contention with a
contradicting writer (binding never silently adopted, conflict record
survives — labeled a contention smoke, not a deterministic discriminator),
stale-lock takeover, sequential baseline; suite 22/22; typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit bf9fa799441acd6b5788a1a68ac3dc28762bf786)
(cherry picked from commit 48278ea2156c44705fe7e37fcd0cd4fb8b7d34d5)
r1's B15 finding: vitest.workspace.ts is deprecated and stops being read at
vitest 4 — the guarantee would silently expire, reopening the unguarded
root-invocation hole in exactly the way that cost the QA cycle. Same seven
lines as a root vitest.config.ts test.projects entry, which the next major
keeps reading. Replayed the root invocation: 14/14 through the package
config, no deprecation warning.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 711500ef6a815d344d7b5658215bdd2b57f671c8)
(cherry picked from commit fbe88fb93621e2698a1125e4c9e02a4322671328)
B15 root-caused: the "13 reproducible Node-22 CLI failures" in the ask suites
were a harness artifact, not a product defect. The per-package vitest configs
carry the D12 hermetic-env setup (scrub connection env, fixture-scoped
OPENRIG_HOME, fetch guard), but dev-qa's invocation ran vitest from the REPO
ROOT, where no config exists — so the suites ran unguarded inside a seat,
ambient OPENRIG_URL routed the mocked-deps CLI to the live daemon, and all 13
"failures" were the live daemon's rig-not-found response. Discriminated both
ways at their exact tip: root+ambient = 13 fail; package-config or scrubbed =
14 pass.
This workspace file makes a root invocation resolve each package's own
config, so D12's "the guarantee lives where it cannot be skipped" becomes
true for the skip path that fired. Replayed dev-qa's exact commands from
root, seat env unscrubbed: 3-file isolation 14/14; full 13-file matrix
66/66.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 86df12d13653785702f72432c4eb260c60d41836)
(cherry picked from commit 3cf37af2d5043b79df42dc81094f749d525a03a6)
F1 (B12's completion under the same ruled intent). r1's live measurement:
post-cut healthz spread 14ms-7.68s, with codex_thread_id.resolve_home the
dominant real blocker at 28.9s/15min — per-PID execFileSync("ps eww") bursts
inside the 8-attempt capture loops.
The proven B12 pattern applied at the seam: defaultResolveHomeDirByPid is
now async (runAsyncSite + promisified execFile); the ResolveHomeDirByPid
type accepts sync or promised returns so injected test stubs stay sync while
every caller awaits. Both consumer chains were ALREADY async loops
(refresher captureCodexThreadId 8x250ms, adapter captureFreshThreadId
20x250ms with await-yields between attempts), so caller semantics are
preserved and the ruled falsifier (sync callers that cannot await) does not
fire — the conversion alone suffices; no extra loop yield needed. The
refresher's internal helper now takes the RESOLVED home instead of the
resolver (the await lives at the caller).
Two discriminator tests ride the B12-T suite: the real ps-eww path resolves
a SPAWNED child's HOME from its environment (the vitest worker's own env is
not ps-readable on this platform — measured, so the probe target is a child
with a known HOME), and the same ordering property as the other converted
sites (0ms timer fires before the spawn resolves; RED on the pre-F1 sync
shape by construction). Refresher suite + B12-T suite 28/28; typecheck
clean. Door test (r1's healthz profile replay + slow-op aggregate ~29s -> ~0
for the site) runs post-fold at the next daemon roll.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit d426bfa407b6d9c194edfb79037ad07705120c5d)
(cherry picked from commit 06127b4d3b7ca2fcff24d022823f1ba9499be4ed)
r1's NOT-CLEAR on 20eef1342, measured not read: my monotonic-progress guard
degraded to ONE-BYTE steps when a single line exceeds the 512KB window
(droppedBytes fills the window, next >= end, min(end-1, next) = end-1) —
each step re-reading 512KB. On a real 6MB rollout with a 2.17MB line r1
killed the scan past 30s (~1.4TB of reads extrapolated), synchronously, on
the daemon loop — the exact B12 class this file's own header invokes. And
the shape is real: 44 lines >= 512KB across this box's rollouts, longest
8.2MB, sitting in the very deep-scan files the cap exists to serve.
The fix is r1's simulated one-line rule: keep fragment-sized step-back, but
when it yields no progress, step a FULL WINDOW (end = next < end ? next :
start). A wider-than-window record still cannot be recovered whole — the
already-accepted tradeoff — but the scan terminates. Receipts: r1's exact
suspect file (019fefd0, 6MB) reads gpt-5.6-sol in 6ms (was killed at 30s);
full-box census unchanged at 186/188 in ~0.5s; new regression test with a
2MB single line between signal and EOF (the pathology needs a record wider
than the WINDOW — r1's 20KB case could not see it) bounded under 2s and
still reaching the signal. 12/12 suite; typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit da09a8117cef32f59f325d17f4da9212e711d1a0)
Three truth repairs from r1's delta verdict (CLEAR; these close its findings):
- STRADDLE (the one that could bite): the fixed 4KB overlap lost any
boundary-straddling record longer than itself — r1 proved a 20KB record
vanishing, real world_state records reach ~22KB (48% exceed 4KB), and the
usual failure is a STALE model read: a model change landing in a straddled
record reports the previous model, the exact silence this detector exists
to end. The overlap is now SIZED FROM THE DROPPED FRAGMENT (r1's
cleaner-still option): the next window ends just past the cut record's
tail, so it is re-read whole by construction, with a monotonic-progress
cap for the (unobserved) wider-than-window record. The self-contradicting
comment above the step is gone. New test: a 20KB sole-signal straddler
swept across the boundary reads whole; the small-record case stays.
- COMMENT TRUTH (r1 correction, kept visible as wrong): collaboration_mode
is NOT absent from July-era world_state — 14/72 records in the very file
carry it. It is SPARSE within world_state while turn_context.model recurs
every turn (232x in the same file); turn_context is preferred for
density, not existence, and the world_state belt cannot be dropped. The
old wrong sentence is struck in place.
- RECORD CORRECTION to the previous commit body: the census's "2 nulls both
zero-byte stubs" was wrong — r1 measured both at 18,707 bytes
(session_meta + one event, no turn yet). They are the ARMING case, the
state observable-pending exists to expose, not empty files.
21/21 both suites; typecheck clean. Also carried from the verdict, r1's
correction in this candidate's favour: the deep scan is load-carrying on
real data (33/188 files read only via the scan, furthest signal 2.49MB from
EOF, zero shallow-vs-deep contradictions) — not a fixture-only property.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 20eef13420d0f4da9caafa942dde624378d9fbe7)
r1's B8 required addition, both recommended legs plus a format discovery
their census surfaced:
- The codex read now scans BACKWARD from EOF in 512KB windows (4KB straddle
overlap) capped at 8MB — bounded, never a stall — and prefers
turn_context.payload.model over world_state.collaboration_mode.model:
chasing r1's misses at source showed the old (July-era) rollout format
carries NO collaboration_mode at all, so window depth alone could never
fix those files; turn_context.model exists in BOTH formats and recurs
every turn. Census across all 188 rollouts on this box: 186 read (incl.
the 161MB old-format file, 3ms), 2 nulls both zero-byte stubs, 480ms
total.
- PENDING IS OBSERVABLE: a pinned generation with no effective read is
counted per poll and, past 10 polls, named ONCE in a loud warning as
pinned-but-UNCHECKED (seat, pin, why a divergence would currently be
invisible); pendingSeats() exposes the set. Detection silence was the
failure class one layer under channel silence — a bare continue could
skip a long-running codex seat forever with nothing recording it.
5 new tests (deep sparse signal found; scan cap honored as bounded null;
window-straddling record read via the overlap; unchecked named once, not
spammed; late signal clears pending). 20/20 across both suites; typecheck
clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 21d424a0f37a5232b6c70cf4e96865abf2ea56e7)
B8, founder-ruled into 5.2. Both live specimens (a 400-rejected pin and an
environment-invalid launch) ended the same way: the seat silently running a
model nobody chose, no proclamation anywhere. The detector is the pm-ruled
cause-agnostic comparison — the runtime's EFFECTIVE model vs the seat's
PINNED model — with the cause string riding as diagnosis only, never as the
trigger.
- EFFECTIVE reads per runtime, from the runtime's own record (both specimens
proved the requested echo lies): claude = the newest assistant turn's
message.model in the transcript; codex = the newest world_state's
collaboration_mode.model in the rollout. Bounded tail reads (512KB) by
contract — provider records reach hundreds of MB, and a whole-file read
is the B12 class. No signal in the tail = null = PENDING, never assumed.
- ONE VERDICT PER OCCUPANT GENERATION, polled: a claude seat has no
assistant turn at readiness, so the check arms at generation appearance
and fires at the first reliable read. Every-occurrence semantics at the
generation grain: each new occupant of a diverged seat proclaims again;
one occupant never spams.
- FOUR CHANNELS per the desk target ruling: the seat's own rig's orch.*
seats + the operator seat derived from workspace.operator_seat_name +
the fleet oversight judgment seat (locally resolvable, else a NAMED
deferral — no daemon-side cross-host send seam exists yet and the
deferral says so) + human-via-Slack as the DS2 named deferral
("deferred: M1 not landed") until the M1 gateway lands. Every channel's
outcome (delivered/failed/deferred) rides the durable
seat.model_divergence event — silence is structurally impossible.
15 unit tests (divergence fan-out, generation grain, pending-vs-match,
named unreachable channels, record-failure isolation, real-shape reader
fixtures incl. the bounded-tail case); event union extended; wired in
startup at a 60s poll. Live door test (kill a pin on a live seat, three
channels receive + the Slack deferral line visible) is operator-driven
post-fold + daemon upgrade.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit fb0299798789dc7fb8c880f2ac6be5fd422fa573)
r2 round 3 (the unknown-read-as-safe class): collectRefSites narrowed raw
declarations to string|null before the authoritative validator saw them, so a
PRESENT non-string permission_policy value was erased — reported absent/floor
at rig level, silently omitted at member level — while `rig spec validate`
rejects the same value. Sites now carry the RAW declared value (undefined =
key truly absent, everything else present) and every present value goes to
validatePermissionPolicyRef, whose non-string branch surfaces the
authoritative error with exit 1. Member precedence keys on PRESENCE, so a
present-but-invalid member override surfaces as its own site defect instead
of vanishing behind the rig ref. Three new tests pin the class (non-string at
rig, non-string at member, non-string in list's custom set); 21/21 + parity
green; typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 5664bf12b1b33e067375f30da5893dcd76493856)
(cherry picked from commit ac308df493494cedb5b8654c7a330d471df78915)
review-r2's two HIGHs on the first cut of the verb, both capability-vs-ruling:
- HIGH-2 (the sharp one): `current` ran a private string classifier and
exited 0 blessing refs the product's own validation refuses — r2 drove a
traversal ref to a confident "custom policy" verdict. The verb now runs
the AUTHORITATIVE validator/resolver: byte-equivalent CLI twins of the
daemon's permission-policy modules (policy-ref, policy-spec, path-safety
under lib/, pinned by a new byte-parity test — the scaffold-placeholder
twin arrangement). `current` validates every declaration site (rig +
member overrides, member > rig), exits 1 on any invalid ref with the
authoritative error verbatim, and reports WHAT WOULD APPLY per site:
origin, resolved target, surface, launch posture, content-resolution —
an unresolved custom file surfaces its advisory-floor consequence
explicitly instead of vanishing.
- HIGH-1: the custom halves of list/show. `list --spec` discovers the
custom policies referenced by the spec set (resolved, posture-visible,
invalid ones named); `show <ref> [--spec]` validates and opens a custom
policy spec anchored at the spec's directory — sealed-contract errors
print and exit 1; a missing target names the resolved path and the
advisory floor.
Built-ins, deliberate-none, the honesty pin, and the apply/record flow are
unchanged. 17 policy-verb tests (r2's four door cases among them: traversal
refusal, valid full-bypass custom, missing-custom surfacing, spec-set
discovery) + 3 parity pins; setup/index/release-surface suites green; CLI
typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 9392144cc30e2596db812bad6f311186f54d6a5e)
(cherry picked from commit 31ad3ea9a7edf9ca870ebbf96aea230ccc51b5eb)
The second half of the B7 ruling (RULING-rig-mode-rig-policy-naming): with
the context-mode verb renamed to `rig mode`, `rig policy` returns as the
top-level permission-policy verb. v1 is read-heavy with one write path:
- list/show teach the built-in template set (locked/standard/open/yolo) and
the reserved deliberate-none choice, each with its mandatory-prefix ref
form (builtin:<name> | none).
- current --spec classifies the recorded permission_policy value for a rig
spec: absent = the floor, none = deliberate, builtin template, or custom
relative path — display-side mirror of the daemon's policy-ref semantics;
resolution stays daemon-side at launch.
- apply <name> --spec records the choice through the SAME comment-preserving
setup flow (recordPermissionPolicyStep, now exported); rig setup --policy
stays as the setup-step composition, not an alias.
The binding honesty pin rides every teaching surface verbatim: OpenRig bakes
NO allow/ask/deny permission policy — the harness-native permissions are the
control surface; posture is recorded into RigSpec, never enforced at
runtime. 7 new tests (pin presence, list shape, unknown-name refusal,
absent/builtin/none classification, comment-preserving apply round-trip,
no-spec refusal); setup suites unchanged green; effect-checked live via the
worktree CLI (list, apply, current round-trip). The as-built cli-reference
gains the section. Known gap: the RUNNING daemon still serves
/api/rig-policy, so `rig mode` daemon-backed subcommands 404 against it
until the next daemon upgrade — the rename lands as one unit at fold+upgrade.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 258f2959b873dc44d394ea8d94bd08fbc30eab16)
(cherry picked from commit 3d214713bcdebde6c6e50e1d1844edc425ce4075)
B16, r2-proven live: the successor pane showed no labeled recap, no record
path, no native-absence line, with the predecessor JSONL present on disk.
Root cause at source: the fresh cutover resolved the recap AFTER
createSuccessor — but the claude sidecar is keyed by SESSION NAME and the
cutover reuses the canonical name, so by resolution time the just-booted
successor had overwritten the sidecar the resolver reads. A fresh successor
transcript yields zero exchanges, so the resolver returned an honest null,
silently, every time. (The codex leg resolves via the predecessor's DB
resume token and never raced — which is why only the claude leg died.)
Three changes:
- ORDERING: the recap resolves at step 1b, beside the departing-context
capture, BEFORE the successor exists. Resolution is a pure read; nothing
downstream of it depends on the launch. A service test pins the order
(resolver invocationCallOrder < launchHarness).
- OWNERSHIP GUARD: the resolver verifies the sidecar's session_id against
the departing session's recorded resume token when both are present; a
mismatch is a NAMED unavailable verdict citing both id prefixes — never a
silent null and never another tenure's recap. Missing ids fail open.
- LOUD NULL (per the ruling): the resolver returns unavailableReason
instead of null for every no-recap outcome, and the packet renders
"--- Predecessor recap unavailable: <reason> ---" so an absent recap is
distinguishable from the feature never running. An unwired resolver names
itself, and a LEGACY resolver that still returns null (the pre-B16
contract — the model-fidelity e2e injects one, which caught a crash here
on the first cut of this commit) folds into the same named-unavailable
path instead of throwing.
Resolver deps change: readClaudeTranscriptPath -> readClaudeRecord (path +
session_id ride one read); routes/seat.ts wiring updated. Suites at this
tree: resolver 8/8, packet 6/6, handover service 46/46, cutover e2e and
model-fidelity e2e both green (61/61 across the five), daemon typecheck
clean. Runtime adoption at the next daemon upgrade; r2 re-drives the live
claude door test then.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit c5c36c40ebdbd768a02f90b902a2aa937d787c82)
The rig mode section was re-verified against b13a8e4c7 (the rename
completion); the stamp names that source state per the as-built convention.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 5f45e857b5ca8442a528bd06c0b8ace29a286f9e)
(cherry picked from commit 14ec37a39898ca5c058a9238d891e61dd72a52dd)
Completes the slice-09 verb rename directed by the PM ruling
RULING-rig-mode-rig-policy-naming-2026-08-07 (clean rename, no alias, no
deprecation — zero adoption founder-confirmed; 0.5.2 is the
clean-introduction window). USER-FACING: the context-mode verb is now
`rig mode` (set/show/effective/cite/unset/defaults unchanged); `rig policy`
is being reintroduced separately as the permission-policy verb per the same
ruling. The API path rides the rename: /api/rig-policy -> /api/rig-mode.
This commit carries the CONTENT half of the rename: command registration
(new Command("mode")), all identifier renames (rigModeCommand/RigModeStore/
rigModeStore context key/rigModeRoutes), import paths, help text, test
titles, and the as-built cli-reference section. The FILE renames landed in
a57319df0 — which was meant as a one-line docs commit and swept the staged
renames without their content, leaving the tip incoherent until this lands
(named plainly: that sweep was a commit-hygiene miss on this seat).
Deliberately NOT renamed: DB tables and migration files (041_rig_policy,
058_rig_policy_provenance — append-only shipped history) and the
permission-family RigPolicyProvenance identifiers, which now correctly read
as the NEW `rig policy` family. Slice-09 suites 106/106 across both
packages; both typechecks clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit b13a8e4c71c6366ef5f8e80124febfc3ef5953da)
(cherry picked from commit e5060e0b67db02208c8ab5f0a33f3832af951cca)
The user-facing command rename shipped inside a57319df0, whose subject names
only the B12-T docs change - 13 of its 14 files are the rename (commands, lib,
daemon routes, six tests; symbols and imports renamed coherently; typechecks
clean). AUTHORIZED: PM ruling 2026-08-07 - rig policy to rig mode, clean rename,
no alias, zero adoption founder-confirmed, filed for 0.5.2 as the
clean-introduction window with a negative directive against earlier callouts.
This record exists so 0.5.2's release notes - generated from commit messages -
introduce the rename as ruled: users type 'rig mode' from the next install;
'rig policy' ends. Found by review-r1's delta discipline; docs/as-built
cli-reference update rides B7's completion. Historical records (CHANGELOG,
v0.3.2 notes) intentionally unchanged.
(cherry picked from commit 2aa992be7f2b58a34572db57a1043c0a74bac519)
review-r1's non-blocking finding: the header still described the superseded
wall-clock design; a reader could distrust the test or restore the bound and
reintroduce the flake. One line, aligned with the inline note.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit a57319df09531ae9866d62ade7c15a3a5fe28abe)
All 87 refresher tests inject sync stubs, so nothing exercised
runAsyncSite/defaultListProcesses — the async conversion was green-by-vacuity.
This drives the REAL default at both sampling sites (a real ps invocation)
and asserts the ordering property the pre-B12 code violated: a 0ms timer
armed after the call fires BEFORE the spawn resolves. The old execSync
implementation finished ps inside the call, so its promise settled on the
first microtask ahead of any timer — the assertion reads false there,
deterministically, with no wall-clock bound (a measured 60-80ms of spawn
setup under load makes any fixed bound environment-hostage; the comment
records that measurement).
Both defaults exported for the test with the reason stated at the export.
Door-test revert leg run locally: with both sites byte-patched back to the
sync shape the two discriminator cases fail on exactly that assertion; bytes
restored and verified identical; 4/4 green on the candidate; typecheck clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit dc4f4df2d6a5581e2a58f50eccc26ee62a89beb6)
Folds the fixture fix from candidate 818bf68e6 (r1 CLEAR c2f0cc7b, door-tested
on the supported-runtime loaded UI leg). The candidate's second hunk - deleting
the vitest:ui ledger resident - is DELIBERATELY NOT folded: it executed B13's
door test as originally specced (resident dies), which was superseded minutes
earlier when the resident was re-scoped to the table-crash STRESS specimen
(fd8aad810). The resident now survives for specimen 2 and dies at its expiry or
specimen 2's fix. Partial fold by desk custody of the ledger file; patch-id
intentionally differs from the candidate and this message is the record.
(cherry picked from commit b86f44a04fa8979d01b70b838f62616cd905b040)
The plan-lock verb defaulted locked-artifacts to IMPLEMENTATION-PRD.md with no
content check and no selection flag, so the strongest structural claim in the
SDLC could freeze placeholder bytes nobody chose — three live locks did (05a,
slice-14 tonight, slice-13 minutes later). Two composed repairs at the write
path:
- REFUSE a contentless default: when the derived set is only the PRD pin and
that file is missing, unreadable, or still shipped-template scaffold, the
approve refuses (plan_lock_contentless) with the two exits named. Authored
detection is line-level over the shared placeholder grammar, so free-prose
PRDs count and only the untouched scaffold (and absence) refuses.
- --locked-artifacts <paths> on the CLI (plan-lock only; local fast-fail on
delivery/mission): an explicit slice-relative set that REPLACES derivation
entirely — each path validated to exist, scheme/absolute/escape rejected,
first-wins dedup. Rides POST /api/scope/approve as lockedArtifacts and
composes with --re-approve, so a wrong auto-written set is amendable.
The old missing/EISDIR "fail-open singleton" tests now pin the refusal; the
slice-14 scenario (scaffold PRD, ruled PLAN+SPEC set) is replayed green as a
test. Approve error surfaces carry the daemon's action line through the CLI.
Scope family 113/113 daemon + 226/226 CLI; both typechecks clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit eb69856e532620bb3698dde3f2100778ed5ac700)
review-r2's B6 blocker: the auto_register validator accepts whitespace-padded
enum values (it validates the trimmed raw), but autoRegisterAllowed() compared
the unnormalized string — so a config both twins accepted (" all ") silently
behaved as "off". The gate now trims at the enforcement seam. New
store-to-enforcer discriminator test: a real SettingsStore hands " all "
through from env, and the gate opts the fleet in. Suite 16/16.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 59404a835e593caa74a0866901848ee13e42d7ef)
Review repairs on the slice-13 candidate (review-r2, both blocking findings +
the completeness note):
- ps-compact's fixture rows now carry hostSelfId, mirroring what the daemon
actually serves since 80fc53028 — the ratio guard measures real surfaces.
- The compact/full ratio re-based /3 -> /2.8, named and grounded in the AC-1
comment: one ruled scalar joined BOTH projections; no heavy field returned,
and heavy-field omission stays directly asserted in AC-7.
- hostSelfId joins ALLOWED_NODE_FIELDS so the selective --fields surface
accepts what the default projection now carries.
ps-compact 16/16, ps-http + ps-current-rig green, CLI typecheck clean. The
slice's IMPLEMENTATION-PRD now carries the authored B5 subset with a
post-build-freeze provenance note (was scaffold placeholders).
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 9245ef0f0a619f55243146a341051a14679fe458)
B6 founder ruling: the watchdog machinery stays, but a fresh seat no longer
gets an idle-gate job by default.
- Two new settings keys in both twins (daemon settings-store + CLI
config-store, lockstep lists/env/paths/defaults/constraints):
policies.idle_gate_qitem.auto_register ("off" default | "all"), and
policies.idle_gate_qitem.opt_in_sessions (comma-separated canonical
session names that get a job while the mode is off).
- WatchdogAutoRegistration.ensure() creates a NEW job only when the fleet
opted in or the seat is named; a seat with an EXISTING job keeps being
maintained regardless, so pre-existing jobs survive the default flip.
- assertCoverage() is gate-aware: no job on a non-opted seat is the ruled
state (null), not a "missing" warning at every seat mint and startup audit.
Tests: the existing registration suite now runs under the explicit fleet
opt-in env (it exercises the machinery, not the default); a new describe
covers the ruled default (fresh seat gets nothing, opt-in seat gets exactly
its job, "all" restores fleet-wide, surviving job maintained + audited).
Key-set pins updated in both twins (52 -> 54). Watchdog family, settings,
handover suites all green; both typechecks clean. The live rig door test
(rig watchdog list) lands at the next daemon upgrade.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit b2719f974e0a3b04be6c8e0e6e9d6351604b07ac)
Slice-13 safe subset (fixes 2 and 3; token surfacing stays out):
- /api/rigs/:rigId/nodes (and node detail) stamp every row with hostSelfId,
the serving daemon's boot-reconciled self-id — a daemon only inventories
its own seats, so attribution at the source keeps a merged multi-host
roster legible as partial instead of authoritative. Explicit null before
the boot reconcile; never host.name (display-only). The CLI compact
projection carries the key through; the -A fan-out's caller-alias hostId
stamp is unchanged and complementary.
- The session_identity hook route stamped resume_type "codex_id" for every
non-pi runtime, so claude-code seats carried a codex-typed label over a
correct token value (live on this rig: runtime claude-code, resumeType
codex_id). The label now derives from the runtime via the shipped
validateResumeToken seam; an unmapped runtime skips the persist
(tokenPersisted: false) rather than guessing. The scrape/refresher writers
were already runtime-split; mislabeled stored rows self-heal at each
seat's next SessionStart hook.
6 new route-level tests (attribution present, null-before-reconcile, detail
parity; claude_id/codex_id/unmapped-skip). These prove the daemon surface;
the two-host door test stays operator-driven. Adjacent daemon suites 65/65;
both package typechecks clean.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 80fc530283234113d5a29ff3ca80f6c3e4f24fbe)
Two review repairs on the host-bindings candidate:
- The daemon twin's resolveHost now carries a comment naming its divergence
from the CLI twin as deliberate (placement resolves operator aliases; the
daemon never sees 3-part targets per BR-1), so the next person diffing the
twins finds the ruling instead of a bug. The parity suite compares only
validateHostRegistry and cannot see this divergence.
- loadHostBindings treats a declared sidecar version other than 1 as
unreadable (empty, fail-open) instead of mis-parsing a future shape
through v1 eyes. Absent version still reads as v1.
Bindings suite 19/19; parity suite 36/36.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit c4918429bc9f2b5c5393399e37fcd60a98c0fa83)
The registry's hostId join key shipped with nothing populating it. This adds
the population wiring on the A2+Source-1 branch:
- New host-bindings.json sidecar beside hosts.yaml (hosts.yaml's known_hosts):
machine-learned alias->selfHostId bindings, disposable, never touching the
operator file. TOFU on first contact; a later contradicting observation is
recorded on the binding and NEVER silently adopted.
- rig host doctor (http leg) learns the peer's selfHostId from the healthz
body it already holds, reporting a host-identity-binding row: pass on
first-contact/confirm, loud fail naming both ids on contradiction with a
delete-to-relearn fix. Learning is injected via DoctorDeps.learnHostBinding,
absent-by-default in fixtures (fail-open).
- Resolution merges the sidecar: resolveHost gains an optional learnedBindings
set ranked below registry id and declared hostId; resolveCrossHostTarget
loads it so a pasted reply hint ending in a learned self-id resolves and
normalizes to the alias. A conflicted binding surfaces a non-fatal stderr
warning in send/capture/transcript naming both ids.
- rig host ls: HOST-ID column falls back to the learned binding, flags
id-changed! with the full story under the table; --json rows gain additive
learnedHostId + idChanged keys.
Unit-tested: 18 new tests (TOFU, conflict no-overwrite, flap keeps conflict,
fail-open corrupt/missing sidecar, resolution ranking, doctor learning rows);
full CLI suite 2319 passed. These prove the mechanism, not the live door
test, which stays operator-driven. Effect-checked live on this box:
worktree-CLI doctor against the parent daemon learned its selfHostId into
the real sidecar first-contact, and ls renders it (the parent is a NAMED
host, so its self-id equals the alias there; the generated-id path is
unit-proven). Daemon-side placement resolution deliberately unchanged: the
daemon never sees 3-part targets (BR-1) and placement resolves operator
aliases; flagged for review rather than mirrored as dead surface.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 80bab0899254245cc5ccac6daf69aedb65087ab6)
Both pre-existing, neither candidate-attributable, both green single-file at this
tip and red only in the loaded parallel gate (receipts in the entries). vitest:
the ps-summary-stall D2 timing pin (owner: its pin row). vitest:ui: slice-11's
documented p5-mobile specimen - the founder's conditional ruling triggers (the
cut gates on the UI suite) so slice-11's one-fixture fix returns to the 5.2 wave.
Expiries 2026-08-28, a week inside the cut ceiling: both residents self-die
before the final 5.2 cut, forcing the fixes or removal.
(cherry picked from commit eb81a9163db41488f82e922b260753f76091e2b3)
Claude-code seats run in the tmux alternate screen, which keeps no scrollback
buffer, so the from-record boot recap is the permanent claude-runtime leg of
scrollback-preserving handover, not an interim stopgap. Three changes:
- Bound each recap exchange at 500 chars (configurable) with a visible
truncation marker pointing at the record; the exchange COUNT was bounded
at 6 but per-exchange content was not, so one pasted-file exchange could
flood the successor pane.
- Label the packet recap section "replayed from record, not the live
terminal" so the replay cannot read as native history.
- Reword the receipt line to describe the record path itself: durable,
true, grep-able — not human-scrollable.
Comments reframed from stopgap to the per-runtime split (codex native
scrollback is the money proof; claude gets the labeled recap). All 61
handover/recap tests pass, including the tmux cutover money-proof e2e.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit b388c264326921d949c7382c660de4e6d969d26a)
The parity pin packages/cli/test/fanout-contract-parity.test.ts ("byte-equivalent
across CLI and daemon") went RED on main when slice-14's perf commit b4aca75a4 added the
selfHostIdSource provenance block to the daemon twin
packages/daemon/src/domain/hosts/fanout-contract.ts but not its byte-identical CLI twin
packages/cli/src/lib/hosts/fanout-contract.ts — unseen because the CLI suite did not run
at that fold. Deterministic, pre-existing, red since b4aca75a4.
Byte-mirror daemon -> CLI (the ruled direction: the daemon side carries slice-14's
reviewed boot-derived provenance; the reverse would delete the perf fix and re-open
ps-summary-stall-red). The two files are now byte-identical. Same lockstep-twin pattern
as precedent 20f5e91bc.
Receipts: fanout-contract-parity 2/2 GREEN (was RED); cli typecheck (tsc) exit 0;
targeted fanout importers (parity + ps-http) 22/22 GREEN. Branched off c5635c229.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit b8d446069f910f1df5668ee14356babc7dc9731e)
The default openrig-start.md carried the factory operating model (mission-slice-sop,
plan-lock flow, skill-library routing) hardcoded into every seat's boot context on every
instance regardless of the rig's actual mode. Founder-ruled walk-back (2026-08-21, the
5.3 context-layer direction in minimal form): the overlay's only jobs are identity
(rig whoami --json, re-run after any compaction/restart/restore), the two peer verbs,
a thin-transcript honesty warning (capture is unreliable on some runtimes - little or no
output does not mean the session was quiet), and an explicit ask-dont-infer close.
The asset test is rewritten from pinning the old content to guarding the contract:
positive guarantees asserted wrap-safe, the walk-back pinned as ABSENCES (factory SDLC
and skill-library routing may never ride the default overlay - opt-in layers arrive by
profile/startup config), and a 2500-char ceiling so accretion fails loudly. Trimmed
overlay has run every seat on this box since 2026-08-17.
Reviewed: review-r1 NOT-CLEAR qitem-20260821005112 (RED test caught) then CLEAR
qitem-20260821010025 after the test rewrite; both verdicts re-run by the reviewer.
(cherry picked from commit c5635c22909f49cbb9d4ba06714953f100b06bcf)
B12-S (release-0.5.2, ruled safety fix). run-scenarios.mjs mapped argv straight to
resolve(a), so a dash-prefixed arg became a bogus path that threw INSIDE the run loop
and was counted as a failed "scenario" WHILE the remaining files still ran host-mode —
so `run-scenarios.mjs --container x.yaml` printed one [ERROR] plus a partial-success
summary a reader could mistake for container mode having run.
Pre-scan argv: any arg starting with '-' prints a teaching [REFUSED] naming the exact
flag and exits nonzero (2) WITHOUT running any scenario. A flagless invocation is
byte-identical. + a refusal test (nonzero exit, names the flag, zero scenario output).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 3324f6811d77a8140f851b3d8757e4b3a297d1ff)
B12 (release-0.5.2). The daemon degraded live: transport sends 17-40s because two
periodic sampling sites ran ps synchronously and froze the event loop for the whole
spawn, queuing HTTP requests behind them.
- domain/sync-site-wrap.ts: add runAsyncSite, the async twin of runSyncSite, routing
through the recorder's already-declared async runStage seam.
- adapters/codex-runtime-adapter.ts + domain/resume-metadata-refresher.ts:
defaultListProcesses now uses promisify(execFile) via runAsyncSite instead of
execFileSync via runSyncSite; the injectable listProcesses dep is a sync|async union
so existing sync test stubs stay valid; call sites (both already async) await.
maxBuffer 8MB for large process tables.
Scope (honest): closes the two list_processes sampling sites ONLY. This does NOT claim
all event-loop blocking is gone.
KNOWN REMAINING sync site (r1, not fixed here): codex_thread_id.resolve_home is still
synchronous and loop-nested per-PID; r1 measured n=1700 -> 58.8s. Filed as a follow-on,
out of this commit's scope.
Unrelated pre-existing base-state RED: the FLEET-STATUS fold pin
(ps-summary-stall-red.test.ts, /api/ps + /api/rigs/summary per-rig SQL fold-scans) is
independent of this change (its stall app never instantiates the changed classes).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit fdbdb362a84da5ff921e09ca8aa354a8e6e6d214)
All three came from ONE mistake in the folded change: putting SPEC.md at the
FRONT of a list without asking what that list does with order. In a
first-match-wins lookup, front means wins. In a later-wins merge it means LOSES.
In a scan-everything loop it means the shadowed file gets read too.
The both-present condition is LIVE on three nodes in this workspace today
(openrig-work/missions/release-0.5.2 and two under the VM workspace). It is not
user-exposed only because the installed CLI predates the fold.
1. MERGE PRECEDENCE. readPrimaryFrontmatter merges later-wins, so prepending
SPEC.md let a stale README.md or IMPLEMENTATION-PRD.md overwrite the live
SPEC.md field by field, silently. The node file now sits in the SLOT README
always held: IMPLEMENTATION-PRD, selected node, PROGRESS — PROGRESS still last
as the lifecycle cursor. extractDisplayName is left alone: it is
first-match-wins, where SPEC-first is correct.
2. ACCEPTANCE SCANNING. buildAcceptance scanned every candidate, so a
both-present slice parsed the shadowed file too. Now it scans the SELECTED
node file once plus the existing non-node documents in their existing order.
The observable damage is POLLUTION, not duplication — identical rows collapse
under the existing dedup, which is why a first version of this test passed
against the defect and had to be rewritten to make the two files DISAGREE.
3. AUDIT ADVISORY. A new low-severity shadowed_node_file finding in BOTH audit
twins, mission and slice level, naming that SPEC.md wins. Low severity never
flips the exit code, and the CLI test asserts ok stays true — it must never
gate a build. A finding on only one twin would mean an operator sees a
different truth depending which surface they asked, which is the same class of
defect as the two-naming-systems problem this work exists to fix.
The live stale release-0.5.2 README.md is deliberately NOT removed: it is
reconciled host/VM state and the real proof case.
RED verified against main 521729318: stale README won the merge (from-README-STALE
over from-SPEC), the PRD won too (from-PRD), and the shadowed file's STALE
obligation appeared in acceptance rows.
(cherry picked from commit 562192e2c0b7dd95545ae5d57bf6f5e677072faf)
1. RESERVED IDS APPLY TO THE JOIN KEY TOO. hostId got the format regex but not
the reserved-set check, so hostId: local validated. A join key names a real
machine's own identity and can never be one of the tokens that are not
machines. Both twins, plus parity fixtures for local/kernel/host/external.
2. THE DERIVATION DID NOT MATCH THE MINT BRANCH, AND THE REAL PATH IS THE CASE
IT GOT WRONG. SettingsStore DEFAULTS host.name to 'localhost' — a reserved
display token the reconciler refuses as a seed — so on a freshly generated
host the candidate is present-but-unusable. Testing for mere presence made
the most ordinary machine there is, one nobody named, report indeterminate
instead of generated. Admissibility is now exactly the mint branch's:
non-reserved AND registry-id-valid. Reserved and format-invalid seeds both
fall back to a generated id, so both now read generated.
The three-state model is unchanged and indeterminate still means what it
meant: an admissible name that DISAGREES with the live id.
3. TWO SPELLINGS OF ONE HOST ARE NOT A CONFLICT. --host mm2-host beside a target
suffix of @host-84c37990 named the same entry and was rejected as ambiguous —
punishing an operator for pasting back the reply hint we printed them, which
is the workflow this slice exists to make work. The registry branch now
compares RESOLVED entries. A genuinely different host, and an --host that
resolves to nothing, both still conflict loudly; the self-id home-route branch
above it is untouched and still compares literally.
Repair 2 verified RED against bf0ba9a5d: localhost default and every refused
seed returned indeterminate.
(cherry picked from commit 521729318868d884caefd1d86c2174ea7168e6a3)
My own regression, caught by ps-summary-stall-red and fixed here.
The first cut of section 2c derived selfHostIdSource inside the /healthz handler,
which meant constructing a SettingsStore and reading the operator's host.name on
every probe — file I/O on the hottest path in the daemon. The N=24 real-HTTP
ps+summary burst test failed immediately; it passes with daemon src from main.
The source has exactly the same lifecycle as the id it describes: resolved once
at boot, read for free thereafter. It now sits beside setSelfHostId in
fanout-contract and is published from startup using the SAME host.name candidate
the reconcile already read, so the two can never disagree and no extra read is
added anywhere.
Worth keeping in view: /healthz is probed by liveness, fanout, ps, and the CLI's
own self-id lookup. Anything added there is added to all of them.
(cherry picked from commit b4aca75a4959eea54ca08071ed55ba1bf9b91f92)
Slice 14 section 2c. MULTIHOST ONLY.
A host running a GENERATED id is in a materially different state from one running
its registered name — remote callers cannot resolve it — and nothing told anyone
which state they were in until a cross-machine message failed.
rig host ls: a HOST-ID column showing the entry's join key, or the word 'unbound'
where the entry has never learned its peer's identity. --json emits hostId
explicitly as null rather than omitting the key, so 'never learned' is a value a
consumer can read instead of an absence indistinguishable from not looking. The
shipped bare-array contract stays additive-only.
whoami: this host's own selfHostId plus its source. host.name above it is a
DISPLAY name; the self-host id is what this daemon stamps into every envelope and
what a remote registry must resolve. Conflating them is the defect.
healthz: selfHostIdSource beside the existing selfHostId, same absent-before-boot
-reconcile contract.
THE SOURCE IS DERIVED, AND ONE OF ITS THREE ANSWERS IS 'INDETERMINATE'.
self_host_identity stores host_id/minted_at/reconciled_at and nothing about
provenance, so there is no record to consult; recording it properly is a migration
and belongs with the population work, not section 2. So: named when the configured
host.name IS the live id, generated when nobody named the host and the id carries
the fallback shape, indeterminate otherwise — which is exactly the CONFLICT state
the reconciler already warns about, where an operator names the host after minting
and the id is deliberately not re-keyed. Calling that 'generated' would be a false
claim about a machine someone did name, and an unknown reading as a known state is
the defect this slice exists to fix.
(cherry picked from commit 24ad2aab00f6cc61155608c05b5c491e83cf4bbe)
Slice 14 section 2a+2b. MULTIHOST ONLY — nothing changes on a single machine.
A peer's reply hint renders member@rig@<selfHostId> and the in-code justification
for always appending the host is that the hint is verbatim-usable. It is not: the
receiver's registry knows that machine only by a human alias, resolution matched
the suffix against h.id alone, and the hint died as 'no registered host'. The
failure is asymmetric — a hint routes only when the sender's self-id happens to
be what the receiver wrote down.
HostEntry gains an OPTIONAL hostId: the observed, immutable self-id of the host
the entry points at. id is MY label for that machine; hostId is what that machine
calls itself. Absent on every entry that exists today, which IS the migration —
absent resolves exactly as it does now, and nothing here populates it.
resolveHost matches id OR hostId, id first across the whole registry so the human
alias stays the intentional handle and the tiebreak is defined rather than
accidental. cross-host-target normalizes sugarHost to the matched entry's id:
the suffix may have matched a join key, and everything downstream compares
against h.id. (The plan said this file needed no change; its own RED expects the
alias back, which requires it.)
Both validator twins get the same optional-field rules — the join key is an id
and earns id's rules. The writer twin needed no edit: it routes through the
validator, so hostId rides through once validation preserves it, and the write
test proves it rather than the comment claiming it.
8 new parity fixtures pin the twins; a schema edit landing on one copy fails them.
(cherry picked from commit 6bd5d169ca321df9b5b70d3d60daca3b670d29b7)
The founder ruling renamed the chain file and the product stopped being able to
read its own work tree: `rig scope mission ls` DROPPED release-0.5.2, and
findMission threw "contains no README.md" at a fully authored mission. Meanwhile
~59 dormant missions and every historical proof receipt are README-backed, so a
migration is not on the table.
Contract: SPEC.md wins when present; README.md alone still resolves, with no
warning and no migration prompt; both present means SPEC.md wins and audit
advises. New scaffolds author SPEC.md. Writers mutate the file the resolver
selected and never create a second authored file beside it.
TWO RESOLVERS, ONE CONTRACT — not one shared function. The daemon cannot import
packages/cli (no @openrig/cli dep, stated in source in three places), so
lib/scope/scope-fs.ts and domain/scope/node-file.ts are deliberate twins. Each
says so, pointing at the other.
Existing multi-filename precedence lists are PREPENDED, not reordered.
IMPLEMENTATION-PRD.md ahead of README.md on slice bodies was a local decision for
that surface; withSpecFirst() puts SPEC.md in front and leaves the rest alone
rather than imposing one global ranking.
Sites: CLI listMissions / findMission / buildMissionInfo / buildSliceInfo, the
scaffold writers, the three move/rename frontmatter updates (a SPEC-backed slice
moved with a README-only guard would silently never get stamped), audit readers,
progress-rail target and backfill. Daemon slice-indexer, scope-view-projection,
slice-detail-projector, workspace-doctor, review/gather, routes/missions,
routes/scope-audit.
scope-approve.ts was NOT in the shaped site list and is the one that mattered
most: it hardcoded README.md and threw "not a declared <tier>", so
`rig scope slice approve` — the plan-lock, the gate the whole SDLC flow runs
through — refused every SPEC-backed slice. Found by opening the file the shape
flagged as unverified.
Thirteen existing CLI tests asserted the old scaffold filename. Only reads of a
JUST-CREATED node were moved to SPEC.md; every hand-written README fixture stays
README, because that corpus IS the backward-compatibility proof. One new test
pins that create authors SPEC.md and leaves no README beside it — the reads alone
would also pass if create wrote both.
cli 2281 passed / 1 pre-existing (dist-dependent --version); daemon 7398 passed /
9 pre-existing in 4 dist- and tmux-dependent files. tsc clean on both.
(cherry picked from commit 2cdc744c4ffd722fcf51c00f486b6d01e528d537)
Follow-up commit on the same branch rather than an amend — af34c2f94 is the SHA
the review cites, and rewriting it would erase what was reviewed.
1. REFUSE IN BOTH DIRECTIONS. The previous revision refused only drift that
dropped seats, reasoning that a spec declaring MORE is a bundle whose job is
to bring the rest up. That misread what the rows are: `nodes` is the rig's
PERSISTED topology, not a list of currently-running sessions. A restore that
raises seats the spec never declared yields a different rig just as surely as
one that drops them. Disagreement is the test, not direction; --allow-drift
remains the honest way through. driftDropsLiveTopology is replaced by
specDivergesFromLive, and the refusal wording no longer claims a loss.
2. VALIDATE BEFORE ASSESSING. A malformed spec has no trustworthy topology, and
assessing first meant a broken file answered 409 — sending the operator to
reconcile a rig when the real answer was "this does not parse". Schema
validation for the selected format now runs first and keeps its existing 400;
the duplicate validate calls further down the two branches are removed.
3. ENFORCEMENT FAILS CLOSED. assessSpecLiveDrift inherited a catch-all that
turned any repository or query failure into null — read as "no drift", which
exported the bundle. A guard that reports clean because its own query threw is
worse than no guard. It now propagates and the route 500s. describeSpecLiveDrift
keeps its non-throwing reporting contract, wrapping the call itself; the two
contracts differ on purpose and both are pinned.
Five new tests, each verified RED against af34c2f94: over-declaring spec 201→409,
malformed pod-aware 409→400, malformed legacy 409→400, dropped nodes table
201→500. The pre-repair 201 on a broken database is the sharpest of them — a
failed comparison produced a successful export.
daemon 7392 passed / 9 failed in 4 files, identical to the pristine-source
baseline (dist- and tmux-dependent integration files). cli bundle 26/26.
tsc --noEmit clean on both packages.
(cherry picked from commit 218c5b8112356cf29df42a291f4dafef75cf8ea0)
`rig bundle create` copies an operator-named spec verbatim into the bundle's
rig.yaml. When a rig has been expanded at runtime — and nothing writes the spec
back, so this is guaranteed rather than accidental — the .rigbundle validates
clean, installs clean, and rebuilds a SMALLER rig than the one running.
Build B (eef9e17ab) already DETECTED this and returned a `warning` on the 201.
Detection was not enough, in two ways this commit closes:
1. A warning attached to a success is read at the one moment it is least
affordable. A .rigbundle is a recovery artifact. Drift that would DROP
running pods or seats now refuses with 409 naming the delta; --allow-drift
proceeds and stamps the divergence into bundle provenance, so the artifact
carries its own caveat instead of laundering it.
2. The human CLI success path printed Bundle created / Name / Hash and dropped
the warning field entirely — detected-then-hidden is indistinguishable from
never-detected for the operator holding the artifact. It now prints. The
--json branch already exposed it by printing the whole response; unchanged
and pinned by test.
Legacy v1 specs export through the same endpoint and produced the same wrong
artifact, so they are compared too — one comparator, two readers. A flat reader
is required: parse a legacy rig's flat logical_ids with the pod-aware reader and
every id reads as malformed, the live rig reads as EMPTY, and the guard reports
"nothing is running" on the one path that exists to notice what would be dropped.
Only the drop direction refuses. A spec declaring MORE than is live is a bundle
whose job is to bring the rest up; refusing that would fire the guard on exactly
the recovery case it protects.
Supersedes one Build B assertion: `SILENT for a legacy non-pod-aware spec` was a
scope limit, not a property of legacy specs. The old expectation is kept named
and explained in place rather than deleted.
Tests: 7 new route-level cases (pod-aware + legacy refusal, allow-drift +
provenance stamp, and three preserve pins — not-live, conforming, declares-more)
plus 4 CLI cases. RED verified against unmodified main for both formats: 201
where 409 is required, and nothing in provenance.
(cherry picked from commit af34c2f94ad632d1b6affce11b3b258384b6a541)
Formal CHANGELOG [0.5.1] entry + docs/releases/v0.5.1.md file, both
authored from the intent-vs-delivered trace at
openrig-work/missions/release-0.5.1 (not derived from 362 commit
subjects). Content structure matches prior release-notes shape.
Known-limits wording carried verbatim per the trace: container mode is
scaffolded but not driven (scoped forward); three of the intended
seed scenarios run green, the tenth cannot be built without two
capabilities that do not yet exist (scoped forward).
Companion to the Release 0.5.1 version-bump commit below; GH Release
body will use docs/releases/v0.5.1.md via gh release create
--notes-file at ceremony time.
2026-08-11 23:12:32 -07:00
1508 changed files with 157742 additions and 17847 deletions
description:Something OpenRig does is wrong, or a command fails
labels:["bug"]
body:
- type:markdown
attributes:
value:|
Thanks. Include a small reproduction and the relevant versions.
This report will be public. Remove API keys, tokens, private prompts, personal details and private paths from commands, logs and configuration.
For a suspected vulnerability, follow our [security policy](https://github.com/mvschwarz/openrig/security/policy) instead of posting details here.
- type:input
id:version
attributes:
label:OpenRig version
description:Output of `rig --version`
placeholder:0.5.14(2edc1b7a)
validations:
required:true
- type:input
id:platform
attributes:
label:OS, Node and tmux versions
placeholder:macOS 15.6, Node 22.22.1, tmux 3.5a
validations:
required:true
- type:dropdown
id:harness
attributes:
label:Harnesses involved
multiple:true
options:
- None / not sure
- Claude Code
- Codex
- Pi / Oh My Pi
- Other (say which below)
validations:
required:true
- type:textarea
id:repro
attributes:
label:What you ran and what happened
description:The command(s), relevant redacted output, and what you expected instead. Paste text where you can; screenshots are useful for visual issues.
render:shell
validations:
required:true
- type:textarea
id:state
attributes:
label:Instance state that might matter
description:Fresh install or upgrade? Include only relevant redacted lines from status output or your rig spec; do not upload full transcripts, credentials or your instance directory.
- type:checkboxes
id:checks
attributes:
label:Checks
options:
- label:I searched existing issues and Discussions for this.
required:true
- label:I am on the latest published version, or I said which version above.
description:Something you want OpenRig to do that it does not
labels:["enhancement"]
body:
- type:markdown
attributes:
value:|
For open-ended ideas or "how should this work" questions, [Discussions › Ideas](https://github.com/mvschwarz/openrig/discussions/categories/ideas) is the better place. Use this form when you can name the outcome you want.
This request will be public; use examples without credentials, private prompts or personal details.
- type:textarea
id:outcome
attributes:
label:What are you trying to do, and what stops you today?
description:Describe the situation, not the solution. One real example beats a general case.
validations:
required:true
- type:textarea
id:proposal
attributes:
label:What would you like OpenRig to do?
description:A command, a setting, a behaviour. Rough is fine.
<!-- Thanks for the PR. Three short answers are all a reviewer needs. -->
## What a user gets
<!-- One or two sentences: the behaviour before, the behaviour after. Link the issue if there is one. -->
## How you verified it
<!-- What you actually ran and saw, and which revision/local changes you tested. The documented checks are `npm run build`, `npm test`, and `npm run lint`; say what you could not run. Redact credentials and private information from logs or screenshots. -->
## Anything you were unsure about
<!-- Design choices, edge cases you did not cover, places a reviewer should look hardest. Empty is a fine answer. -->
## If this is security-related
<!-- Exploitable? Report it privately first (SECURITY.md). Otherwise: how does this go wrong for someone using OpenRig as designed (your own machine or a trusted private network), with evidence? Who causes it, and how do they reach the install? What does the change cost everyone else? See CONTRIBUTING.md. -->
- [ ] One concern per PR; no version bump; no `CHANGELOG.md` edit
- [ ] Tests added or updated where the change is testable
- [ ] I listed the checks I ran, their results, and any checks I could not run
startup child identity, and cross-host origin attribution.
- Diagnosis `show`/`list --json` now return summaries; use `--full --json`
for complete evidence. Queue previews also print an exact full-read command.
Root, CLI, daemon, and web UI are `0.5.11`; terminal TUI remains `0.1.0`.
Migration head remains `079_workflow_lifecycle_parallel`.
The health-agent experiment remains a POC, and the Herdr journey requires no
plugin. Neither is a new cold-agent architecture or a plugin/adoption claim.
See [the full release notes](docs/releases/v0.5.11.md) for operational guidance
and the retained verification limits.
## [0.5.7] - 2026-09-01
**Status**: release-cut candidate. Final release-wide verification and public
publish remain release-manager gates.
### Summary For Installing Agents
- **Package versions**: root, CLI, daemon, and web UI are `0.5.7`; the terminal
TUI remains `0.1.0`.
- **Migration**: head advances `076` → `077_node_session_source`; normal daemon
startup applies the ordered migration list automatically.
- **Theme**: elastic topology lifecycle and coherent work delivery.
### Elastic topology lifecycle
- Create a rig without hand-authored YAML; grow existing or new pods in single
or batch operations; and preserve faithful spec show/export, including a
member's declared agent-image source.
- Refuse unsafe shrink while work is active. Explicit drain fallback preserves
queue identity and state, checks that the destination session is live, and
removes only the intended target; ordinary cleanup stays direct.
### Coherent work delivery
- Compose product-journey SDLC context across world, project, mission, and
slice layers, with useful non-overwriting scaffold defaults.
- Accept optional project YAML enrichment with explicit warnings and default
fallback, load Markdown by section address, and support explicit project
selection, switching, root isolation, and ambiguity refusal.
- Project the complete catalog of public plugin-only skills together with the
helper files their instructions reference.
### Deferred and post-release
- Restore honesty is deferred to `0.5.8`.
- Cleaned parent-host two-project dogfood runs after release.
See [`docs/releases/v0.5.7.md`](docs/releases/v0.5.7.md) for the full notes.
## [0.5.6] - 2026-08-30
**Status**: shipped; **"world-building as context engineering."** OpenRig 0.5.6 ships the context system's next layer, makes long-running agents durable, and makes messages to humans trustworthy. Three themes: the context a seat runs on is structured, tagged, and verifiable; an agent seat can now outlive its own context window without a human managing the transition; and anything routed to a human carries proof of delivery. **v0.5.6 contains v0.5.5 in full.**
### Summary For Installing Agents
- **Package version**: bumps from `0.5.5`.
- **Migrations**: THREE new (apply automatically on daemon start; no manual steps). Head advances `073` → `076`:
- New verbs: `rig view show execution`, `rig scope resolve-notes`.
- New watchdog condition: `usage-threshold` (`rig watchdog`).
- New continuity policy: `compaction_strategy` in the AgentSpec (four modes) with a declared `mechanic`.
- New reference docs at stable paths: `docs/reference/sdlc-conventions.md`, `docs/reference/mission-install.md`, `docs/reference/lore-routing.md`, `docs/reference/planning-dial.md`, `docs/reference/wave-sdlc.md`, `docs/reference/release-boundary.md`, `docs/reference/product-management-pass.md`.
- **Context packs**: 41 ship (up from 39).
- **Delivery defaults**: deliberately wide open (post at `NOTICE` and above, interrupt at `ALERT` and above). Tune down from measured volume with the two dials rather than pre-filtering.
### The context system: structured, tagged, verifiable
- **The mission install.** A documented five-layer convention (`docs/reference/mission-install.md`) for installing project context into a seat: the pieces, their arrangement, the seat's own position, the current contract, and a derive-your-own-delta step that proves the install was consumed rather than skimmed.
- **Lore routing.** What a seat learns locally routes into addressable, rig-local lore packs (`docs/reference/lore-routing.md`) — carried with a maturity stage from birth, and structurally excluded from anything that ships.
- **Taxonomy everywhere.** Every pack entering `rig context` carries its class — WORLD / LORE / SKILLS / MISSION — surfaced in listings, with a loud refusal for untagged packs.
- **A public world pack with checkable claims.** The public onboarding pack is authored as atoms whose claims each carry a check that can fail, with the verifier shipped inside the pack — public content that cannot rot silently.
- **A complete, self-retrievable skill index.** The skill router's index now covers the full shipped set, is retrievable through the routes it teaches, and carries a recurrence test so coverage gaps stay found.
- **A substance gate for what ships.** A whole-surface review pass over every packaged file (bundle and npm surfaces included) runs as a named release step; this release shipped with all 5,316 packaged files passing.
### Continuity: seats that outlive their context windows
- **Continuity policies in the agent spec.** `compaction_strategy` is now live configuration with four modes: `default-compaction`, `managed-compaction`, `handover`, and `apprentice-handover`. Modes apply to Claude seats (positive runtime matching — Codex seats keep their native behavior) and resolve through the same spec-default < profile < member precedence as the rest of the agent spec.
- **The apprentice handover, automated.** A seat declaring `apprentice-handover` gets two armed triggers: near its context threshold, the system creates a fresh successor seat and runs its onboarding install; nearer the wall, it mints an owned handover work item for the seat that executes the swap. The executing seat is declared configuration (`mechanic:` on the policy) — never inferred from topology, and arming refuses loudly if it is missing.
- **Managed compaction with honest restores.** Seats in `managed-compaction` mode get a pre-compaction prep nudge (deposit your state before the window closes) and, after a configured restore, a durable receipt recording how much usable context the restore actually recovered — so a restore that silently refills the window instead of recovering width is visible.
- **Context-usage triggers as a primitive.** `rig watchdog` gains a usage-threshold condition: fire any command when a seat's context usage crosses a calibrated per-seat threshold. The continuity modes are built on it; your own automation can be too.
### The human layer: delivery you can prove
- **No more silent fall-through.** One destination resolver classifies every dispatch: terminal-bound seats use the terminal; registered humans and virtual seats route through the gateway; nothing falls through to a terminal lookup that cannot succeed. Unregistered external addresses get a structured, teaching refusal.
- **Delivery receipts on the work item.** Every gateway post writes its receipt atomically on the queue row: `posted` (with the message timestamp and channel), `transport-failed` (with the error), or — derivably — `never-posted`. `rig queue undelivered` now consults that ledger first and returns only genuine failures. "Did this reach them?" is one read.
- **Notification levels, one vocabulary.** Queue transitions classify as `RECORD` < `NOTICE` < `ALERT` at write time — a human-required decision is `ALERT`; system ceremony events are `NOTICE`; the rest is durable record. Two independent dials configure the minimum level that posts and the minimum level that interrupts.
- **A delivery-rules engine.** Stored per-human preferences and availability decide how each message class is delivered. An `off` preference suppresses interruption — never the durable record, the post, or the receipt. Away windows defer routine traffic into digests (4-hour and daily), with exactly-once delivery by construction: durable episode identities, retry until a real receipt, and structurally non-overlapping digests.
- **Files dropped in Slack become work.** A file or image upload lands as a work item with the file transferred to local storage and referenced by path — authenticated download, size-bounded, per-file failures named, and no URL or token ever written to a row. A park blocked on a registered human automatically enters the alert path and posts exactly once per episode.
### Operational honesty
- A rate-limited seat gets exactly one timed wake at its stated reset.
- A seat holding claimed live work while sitting idle gets woken by the system, consuming the arbitrated activity oracle — with exactly one wake per park episode.
- Refocus state keys to the current occupant of a seat; a fresh occupant never inherits a predecessor's baselines.
-`rig ps` counts claimed in-progress work as assigned work.
- Stuck-sweep findings stop re-minting after closure; cross-host custody verification completes durably instead of refreshing forever.
- Blocker actuation is unified: one helper, cross-host closure honest, delivery after the transaction commits.
- Rig expansion and serialization preserve what they were given: `session_source` versions, `services`, and member overrides survive round-trips; fork forwards model, role, restore policy, and label.
-`rig view show execution` derives who is building what, what is next, and each item's completion rung (locked / built / reviewed / folded / adopted) at read time — unknowns render as INDETERMINATE, never as idle or done.
### Process and reference
- The SDLC conventions document is now a component menu: choose per mission from the simple flow, the wave model, or the rigorous overlay, with planning rigor as a dial. Four full references ship at `docs/reference/`: `planning-dial.md`, `wave-sdlc.md`, `release-boundary.md`, and `product-management-pass.md`, joined by `mission-install.md` and `lore-routing.md`.
---
## [0.5.5] - 2026-08-27
**Status**: shipped; **"the ambient-attention release"** — the fleet notices, retries, escalates, diagnoses, and onboards on its own so you can stop hand-babysitting the work in flight. **v0.5.5 contains v0.5.4 in full** — one reconciled lineage.
### Summary For Installing Agents
- **Package version**: bumps from `0.5.4`.
- **Migrations**: TWO new. Head advances `071` → `073`:
**Stop babysitting the work.**`rig parked <seat>` diagnoses "is anyone silently stuck" with confidence and teaching inline; the standing stuck-sweep runs without you and routes findings. Baton wakes retry, aggregate, and escalate on their own — hand off the row and stop chasing nudges. Parks carry their own wake and read HEALTHY. `rig ps` gains a typed ACTIVITY column from one oracle. The Slack human layer is live end-to-end, and fresh installs onboard themselves.
### What you can now do
#### Diagnose silent-stuck without capture arithmetic
`rig parked [seat]` answers "is anyone silently stuck" as a derived diagnosis — activity × open obligations, with per-input confidence and remedy taught inline. **Reach for it when:** a seat looks idle and you suspect dropped work; before any `claimedAt` arithmetic or pane capture.
#### Hand off work; stop babysitting nudges
Failed baton wakes retry, aggregate, and escalate on their own (transitions ARE the ladder state, restart-safe; per-destination aggregation; rungs deliver-and-advance; unconfirmed-with-no-pickup escalates without re-send). **Reach for it when:** you hand off work — the row is enough. Watch `rig view show escalations` for the aggregates.
#### Consume the standing stuck sweep instead of running it
Overdue + undelivered become routed findings with derived evidence inline. **Reach for it when:** you used to hand-run `queue overdue` / `undelivered` on a timer — stop; consume its findings instead.
**Caveat (5.6 backlog):** cross-host successor visibility is a proven false-positive class; treat cross-host-lineage findings as unverified until the 5.6 detector fix. Contained fleet-wide one-per-condition; delta teaches the caveat.
#### Park a row on a real blocker without waking the attention machinery
`queue block --on <blocker>` records the wake; held-with-live-wake is not flagged; auto-unparked owners get an honest wake. **Reach for it when:** imminent-but-blocked work; for not-imminent work use the workspace instead (the queue is a conveyor).
#### Read seat activity from one oracle
`rig ps` carries a true `ACTIVITY` column — typed taxonomy (working / idle-at-prompt / needs-input-as-count+reason / unknown) with evidence ladder + visible rung degradation, seat-keyed across swaps. **Reach for it when:** any "is it thinking or stuck" question — the oracle beats capture; the TUI reads the same truth.
#### Execute seat handover, not just plan it
`rig seat handover --source fork:` (carries live context) or `rebuild` (primes from the durable chain and names its priming artifacts). Mid-swap failures record honestly. **Reach for it when:** replacing an occupant — no more dry-run-only planning surface.
#### Reach human decision owners directly through Slack
Gateway running, thread-per-seat, exactly-once inbound reconciliation, escalation loudness (mention) distinct from routine. Humans are addressable members; `rig gateway human` has full fragment-lifecycle verbs. **Reach for it when:** anything must reach a human decision owner — an escalation-class send arrives loud on their phone.
#### Onboard fresh installs without hand-walking
Fresh installs get focused onboarding packs by default (config off; existing rigs untouched). **Reach for it when:** standing up a new rig or seat — stop hand-walking the eight pieces.
#### Trust refocus to fire only when it should
Refocus fires at a context threshold or on demand — NEVER at session start (post-compaction both runtimes; usage threshold Claude-only). **Reach for it when:** a long session loses the plot — trigger it. A fresh seat getting a refocus is now a bug to file, not noise to ignore.
#### Author scopes with the one-file convention
`scope mission create` → `NOTES.md` + intent-bearing `SPEC`; `slice create` → `SPEC`-only (no IMPLEMENTATION-PRD) + acceptance `PROGRESS`; release missions get a capability-delta scaffold. `repair` is ADDITIVE — stamps append, never rewrite author frontmatter. **Reach for it when:** creating any scope node — stamp verification is a mechanical strip again.
#### Bind daemons safely from any managed environment
Daemon bind intent has provenance: an inherited `OPENRIG_HOST` never selects single-bind; adoption gates test ALL required listeners. **Reach for it when:** daemon maintenance from any managed environment — no more `env -u` ceremony.
### What to STOP doing (each was correct under 0.5.4 and is wrong now)
1.**STOP the per-lock author-reconstruction protocol.**`scope repair/approve` used to rewrite author frontmatter; now stamps APPEND — strip-reconstruction is mechanical; the protocol is retired.
2.**STOP diagnosing parks by `claimedAt` arithmetic + capture.**`rig parked` derives the diagnosis with confidence and teaching; capture is a fallback glance, not the method.
3.**STOP storing deferred work as queue rows.** The queue is a conveyor for imminent sequential work; deferred work lives in the mission workspace and re-mints when due.
4.**STOP authoring `IMPLEMENTATION-PRD.md` and `MISSION_NOTES.md` on new work.**`SPEC.md` is the one spec file; `NOTES.md` is the chain name; locks bind SPEC-only.
5.**STOP routing contact through an orchestrator by default.** Any agent escalates directly; orchestrators/PMs also send judgment-worthy updates.
### Landed, not yet drivable (recorded so nobody reaches for it)
- Delivery preferences + availability (stored, validated) — no rules engine consumes them yet (0.5.6 slice 01).
- S01 operator rung — human-layer connector delivery leg lands in 0.5.6 (S11 territory). Escalation view + daemon health carry the floor now.
-`@external` addresses on the direct send path fall through to tmux (routing fix in 0.5.6 wave-2). Reach humans via the gateway path (queue/escalation), not raw `rig send` to `@external`.
- Multi-human topology — single-human ships as an honesty marker; multi-human is 0.5.7.
### Known Limitations (0.5.6 backlog)
- **Cross-host successor visibility false positives** in the stuck-sweep detector — 39 active findings at census (20 proven FP, 19 unverified); contained fleet-wide; fix lands in 0.5.6 wave 1.
**Status**: release candidate; **"the honesty release"** - commands distinguish absence, uncertainty, staging, and completed effects instead of collapsing them into reassuring output. **v0.5.4 contains v0.5.3 in full** - one reconciled lineage, no product divergence.
### Summary For Installing Agents
- **Package version**: bumps from `0.5.3`.
- **Migrations**: none in this release. Migration head stays at 071 (matching 0.5.3).
- **Node engines**: unchanged.
- **API surface**: existing commands remain; new seat lifecycle verbs are `rig seat set-model`, `rig seat stop`, and `rig seat clean`.
### Headline
**Trust what the command says, including when the answer is uncertain.** Transport probes now classify found, absent, and indeterminate outcomes through one resolution path. Delivery verification distinguishes text staged in a prompt from text consumed by an agent. Queue, refocus, preview, import, and lifecycle surfaces preserve the state and refusal that actually occurred.
### What you can now do
#### Resolve transport failures without inventing a missing seat
Send, capture, nudge, and walk use one classified session probe. A transport blip is reported as indeterminate rather than `Session not found`, while genuine absence remains explicit. Recovery output names what was checked and what action is available.
#### Tell staged delivery from consumed delivery
`rig send --verify` classifies the pane effect instead of equating typed text with a completed turn. Single-recipient, fan-out, and JSON results derive from the same outcome model, so a staged prompt is not reported as consumed.
#### Deliver unknown-sender messages honestly
An unattributable message is delivered with an unknown-sender notice instead of being refused. Where a reply path exists, the sender is told that attribution was unavailable and should follow up with a signed message.
#### Manage one seat through supported lifecycle verbs
`rig seat set-model`, `rig seat stop`, and `rig seat clean` provide audited, single-seat operations. Stop and clean refuse when any affected session is live or indeterminate; set-model is proven against a real managed successor rather than only stored configuration.
#### Trust state-preserving queue and refocus behavior
Adding a queue note no longer changes terminal state. Envless owned queue listing refuses instead of silently changing scope. Refocus keeps due state until delivery is visible. Bounded previews say they are bounded.
#### Refuse duplicate running rig names at every import path
Create and import routes now share one running-name guard. A duplicate running name returns a teaching `409 rig_name_running` refusal instead of spending resources or degrading into a generic server error.
### Stability and authority fixes
- Startup boundaries are hermetic across scratch/test environments.
- The bundled `openrig-core` plugin has an explicit advancing authority, so an older runtime cannot silently overwrite newer installed guidance.
- Stale lab refocus registrations are retired.
- CLI and daemon refusal codes survive route and import boundaries without fallback noise.
### Known Limitations (0.5.5 backlog)
- **Writer-layer disk cap** - the one-time VM cleanup recovered headroom, but the permanent cap at the layer that writes snapshots is unfinished.
- **Repair/approve frontmatter rewrite** - the known rewrite defect remains outside this release.
- **Observation pipeline performance** - shared pane/process observation remains measurement-gated; 0.5.4 does not claim the broader polling/census redesign.
---
## [0.5.3] - 2026-08-26
**Status**: shipped; **"the context release"** — pull exact context by address instead of reading files. **v0.5.3 contains v0.5.2 in full** — one lineage, no divergence.
### Summary For Installing Agents
- **Package version**: bumps from `0.5.2`.
- **Migrations**: none in this release. Migration head stays at 071 (matching 0.5.2). Nothing to apply on a v0.5.2→v0.5.3 upgrade.
- **Node engines**: unchanged.
- **API surface**: preserved from v0.5.2; no CLI-command or flag removals or renames.
**Pull exact context by address instead of reading files.**`rig context get <pack-ref>/<file>#<H2-slug>[/<H3-slug>]` returns the exact span bytes of one section. The `openrig-skills` router teaches the three-step: ask → ref → load. Refocus resolves refs automatically. Handovers write a durable, superseded-chain seat recap. Composition is by reference, so there is no second copy to drift.
### What you can now do
#### Pull exact context by address, not by reading files
`rig context get <pack-ref>/<file>#<H2-slug>[/<H3-slug>]` returns the exact span bytes of one section. The `/<file>` component is required even in a one-file pack. A bogus slug fails loud and lists the addressable sections. `rig context list` shows every shipped entry's ref and name; `rig context show` inspects one. Refs are canonical full paths (e.g. `skills/<namespace>/<name>`). Cross-source access (`seat:` / `mission:` atoms) is an authoring affordance: declared in pack manifests and granted via `rig context profile --rig/--seat/--mission` during composition — not an ad-hoc argument to `get`.
**Reach for it when:** you need one section of a shipped skill or library entry and would previously have read or copied the whole file.
#### Discover context by asking, not by guessing paths
The `openrig-skills` router teaches the three-step: ask → ref → load. Run `rig context list` for refs, match your moment to an entry's `when`, load by ref.
**Reach for it when:** you don't know which skill or context applies, or nothing is projected after a cold boot.
#### Write a durable seat recap
`rig context recap-write` maintains a seat-scoped `RECAP.md` beside `LEARNED` with a collision-safe superseded chain; restore packets carry an authored-recap pointer.
**Reach for it when:** handover, compaction boundary, or any moment your successor would otherwise re-derive your state from scrollback.
#### Trust refocus to resolve refs
The refocus hook resolves library refs automatically (ref beats file), fails loud but non-blocking, and delivers pointers — the shipped channel for pointing running seats at new content.
#### Fetch bounded domain expertise on demand
`rig context get` serves expertise packs by path-like ref, e.g. `skills/process/context-engineering`. The old `context-pack:` colon form was removed and the CLI says so.
**Caveat that pack:** it is titled "Traditional Context Engineering — 2024-2025 Snapshot" for a reason — provisional, historical, non-normative by the project owner's ruling. Current OpenRig skills, explicit user rulings, and measured practice outrank it on any conflict.
### What to STOP doing (each was correct under 0.5.2 and is wrong now)
1.**STOP copying library content into seat or mission files to compose context.** Compose by ref — addressing means there is no second copy to drift. Nothing catches the copy automatically; it is still a defect by rule.
2.**STOP treating a `MODEL DIVERGENCE` proclamation as alias noise.** The canonicalize-then-compare fix ended the pinned=fable / effective=claude-fable-5 false-positive class, and the monitor now isolates per-seat throws. A proclamation on this runtime is a real escalation — act on it.
3.**STOP confusing explicit-path requirements with permission.** The guards described in this release demand explicit paths (e.g. `OPENRIG_SKILL_CANON_ROOT`); that mechanism does not grant authorization. **Historical guidance correction (2026-09-10):** the earlier approval statement was project-specific, not a universal rule. Current project and mission authority determines whether local commits, apply operations, pushes or PRs require approval.
4.**STOP hand-placing evidence or trusting a placeholder PRD as the proof contract.**`rig proof add` pairs evidence to the contract, and a pristine scaffold PRD can never silently become that contract: the authored SPEC serves with a named advisory (`contractSource` in the echo tells you which source bound).
### Landed but not yet drivable (recorded so nobody reaches for it)
Eval CASES ship (14 selection/loading cases), but no runnable grading pass exists today. `--provider rig` is the live proof-contract door and is NOT YET DRIVEN — it says so itself and redirects to fake (the wiring is exactly the convergence-test driver work in flight). `--provider fake` replays only transcripts supplied via `--transcripts <map.json>` and errors every case without one. When the live door is driven, the next release promotes this to a capability.
### Known Limitations (5.4 backlog)
- **Reply-hint self-qualify** — still open from 5.2; reply-hints on some cross-host messages address themselves via machine-ID rather than registered host name; substitute the registered host name from `rig host list`. Fix in 5.4.
- **Eval grading pass** — CASES ship; no runnable grading pass yet. Driven `--provider rig` lands in 5.4.
- **`rig send --verify` does not detect staged-unsent** for multi-line sends toward Claude seats — carried from 5.2 backlog.
- **Claude transcripts thin under fullscreen upsell** — carried from 5.2 backlog.
- Plus internal debt items ledgered for maintenance.
---
## [0.5.2] - 2026-08-22
**Status**: shipped; test system exercises for real, crash-cart fleet-restore conductor, reliability fixes shipped through the test system. **v0.5.2 contains v0.5.1 in full** — one lineage, no divergence.
### Summary For Installing Agents
- **Package version**: bumps from `0.5.1`.
- **Migrations**: none in this release. v0.5.1 shipped migrations 068-071 (068 CREATE + 071 DROP-IF-EXISTS pair, net no-op for any v0.5.0→v0.5.1 upgrader); v0.5.2 stays at 071.
- **Node engines**: unchanged.
- **API surface**: preserved from v0.5.1; no CLI-command or flag removals or renames.
**A test system that catches real bugs on its own product, plus a crash-cart fleet-restore conductor.** The reliability fixes in this release were found and validated by scenarios that drive the real product through the same commands a person uses. Type bare `rig` at a dead daemon and you now reach a truthful cockpit; one Enter restores the fleet kernel-first with surviving tmux panes adopted.
### Crash-cart fleet-restore conductor
Type bare `rig` at a dead daemon and reach a truthful cockpit — no cryptic error, no silent failure. One Enter restores the fleet kernel-first, adopts any surviving tmux panes (never clobbers them), and presents an exact per-seat remediation walkable in one keystroke. Cancel is honest. Destructive restore is only offered when adoption cannot succeed. Ten build rounds, four independent gates, non-author QA door test.
### Daemon event-loop no longer hangs under load
A class of daemon event-loop freeze under load is fixed and measured. If your daemon was going unresponsive under fleet-wide activity, that's this class.
### Unattended Claude seat handover
Outgoing Claude seat writes a recap and submits its packet automatically at handover (compaction, session boundary, explicit handover); incoming seat picks up with the recap in hand rather than a cold start. Door-proven end-to-end.
### `rig policy` honest under adversarial input
Malformed or hostile policy input now produces honest structured errors rather than silent failure or unexpected behaviour. Three review rounds.
### Cross-host messages carry their machine of origin
Messages sent across hosts now include the machine they came from, so multi-host coordination reads correctly at the receiving end. Enforced end-to-end via door tests.
### Codex model-config drift detector
A new detector for a class of Codex model-config drift that would previously go unnoticed. Caught real cases on landing.
### Test-integrity
- Test runner refuses to run against a dirty tree — a green run means green source.
- Hermetic test roots.
- Flaky fixtures isolated.
### Governance in code
- Plan locks are explicit (chosen when you ask for one), not inherited from ambient state.
- Wake is opt-in.
### Container tar-file hang eliminated at the seam
A class of container tar-file hang is dead by construction — not worked around, but the seam that made it possible is closed.
### Known Limitations (5.3 backlog)
- **Reply-hint self-qualify** — reply-hints on some cross-host messages address themselves via machine-ID rather than registered host name; using the reply-hint directly may fail with "no registered host X". Substitute the registered host name from `rig host list`. Fix in 5.3.
- **`rig queue handoff --summary`** warns yet the field is unsettable on its own output. Fix in 5.3.
- **`rig send --verify` does not detect staged-unsent** for multi-line sends toward Claude seats. Multi-line sends can stage silently.
- **Claude transcripts thin under fullscreen upsell** — upstream fullscreen upsell writes `"tui":"fullscreen"`; one accepted prompt can re-flip a fleet. Fix + pin + prevention in 5.3.
- **Test runners can silently skip on unbuilt trees** — a final-gate rule (build CLI + assert zero skips) is adopted for the release ceremony; product-side fix in 5.3.
- **`seat-handover` model-fidelity real-Codex end-to-end** fails identically on main (environment-vs-product distinction unrooted). Isolation fix in 5.3.
- Plus internal debt items ledgered for maintenance.
---
## [0.5.1] - 2026-08-12
**Status**: shipped; a test system for the product, plus reliability fixes shipped through it. **v0.5.1 contains v0.5.0 in full** — one lineage, no divergence.
### Summary For Installing Agents
- **Package version**: bumps from `0.5.0`.
- **Migrations**: additive only. Existing v0.5.0 databases upgrade by running `rig daemon start` on the new daemon.
- **Node engines**: unchanged.
- **API surface**: preserved from v0.5.0; no CLI-command or flag removals or renames.
**A test system for the product, plus reliability fixes shipped through it.** Scenarios drive the real product through the same commands a person uses, so it can prove its own behaviour without a human checking by hand. The reliability fixes of this window ship through the test system — proving its value on its first cycle.
### Test system
- **Stub runtime for tests** — a fake agent runtime that scenarios can drive. Lets scenarios exercise the daemon end-to-end without needing a real Claude or Codex process.
- **Scenario format + runner** — a text-based scenario format and an executor that runs scenarios against the real product. Includes a real fix that surfaced from writing the runner: text-matching against a pane or transcript could never match its two intended surfaces; now it does.
- **Seed scenarios** — three seed scenarios that run green end-to-end against the real product. See Known Limits for the seed-scenario one that could not be built.
### Container test bed — scaffolding
- **Scaffolding for container mode** — argument builders, image-identity stamping, runbooks, and a unit suite. **The runner does not yet execute in containers** — this is scaffolding you will see in the tree but that no test currently drives. Container-mode execution is scoped forward.
### Reliability fixes shipped through the test system
- **Honest render for daemon transport failures** — the operator sees the actual failure rather than a silent no-op or generic error.
- **Queue-persistence** — two fixes ensuring queue records reflect what actually happened after they were written.
- **Cross-machine send regression** — found and fixed during verification. The test system doing its job: proving product behaviour across machines and catching a regression before release.
### Small features
- **Per-agent model from spec** — each agent's model comes from its spec rather than a fleet-wide default, so different agents can run on different providers or models.
- **Token usage tracked over time** — the daemon records token use so operators can see trends and predict when a seat is approaching a usage limit.
- **Machine-of-origin on messages** — messages carry the machine they came from, so multi-host coordination is legible.
### Other
- **UI timing hardening** — UI tests no longer fail on load-timing flakes.
- **Queue records tell the truth after they are written** — a class of wrote-it-and-it-did-not-stick issues closed.
- **Unreached-message visibility** — show messages that never reached an agent. On landing, this surfaced nine real handoffs that had been silently lost since 8 August.
### Known Limits
- **Container mode is scaffolded but not driven** — no test currently executes in a container. Scoped forward.
- **Seed scenarios: three of the intended set run green** — the tenth cannot be built without two capabilities that do not yet exist in the product (a scenario cannot mutate the running system's shape; the terminal interface exposes navigation state rather than fleet inventory). Both scoped forward.
---
## [0.5.0] - 2026-08-06
**Status**: shipped; mission control TUI + context library + permission-policy built-ins + provider usage observability + plan amendment + honest CLI + build discipline. **v0.5.0 includes everything from v0.4.8** — there is no divergence between the two releases.
@@ -288,7 +1071,7 @@ Building on the v0.4.8 permission-policy foundation, v0.5.0 ships the built-in p
- **New shipped starter: `rig up factory-rsi`** — the single-rig recursive-self-improvement factory MVP. One rig, seven seats (`plan-planner`, `build-implementer`, `check-qa`, `review-reviewer`, `dogfood-tester`, `release-manager`, `orch-lead`), running the new `factory-rsi` workflow. A launch-tier product starter (a `product-team` sibling), workspace-agnostic — point `--cwd <repo>` at whatever the loop should improve.
- **New builtin workflow: `factory-rsi`** — the inner loop `plan → implement → qa_check → review → release`, with `qa_check`/`review``failed` → `implement` (bounded remediation), engine-routed — never an orchestrator relay. Dogfood is **decoupled** from this gated loop: the dogfood seat runs out-of-band against the **shipped** product and feeds its findings into the next plan (the RSI edge, ungated — no loop-stop in the MVP; the continuous out-of-band runtime mechanism is refined in a later release). The remediation loops are sanctioned only by the enforceable `loop_guards.max_hops`; a trip is an exception routed orchestrator-first (`exception_routing`), and `rig workflow resume` grants one more bounded window.
- **Recorded-state cycles**: the next plan's input is the *recorded* dogfood findings (`evidence_ref` / the packet trail), never a seat's chat memory — the RSI feedback is durable recorded state.
- **Publish stays a human act**: the release leg is two steps — `release_prep` (the release-manager PREPARES notes/docs/PR and records the evidence; un-gated, runs first) hands off to `release_signoff`, which holds the ship decision at a human gate (`gate.target: human@kernel`). Prepared artifacts exist before sign-off; no seat pushes, tags, publishes, or upgrades a host.
- **Publish stays a human act**: the release leg is two steps — `release_prep` (the release-manager PREPARES notes/docs/PR and records the evidence; un-gated, runs first) hands off to `release_signoff`, which holds the ship decision at the configured human gate target. Prepared artifacts exist before sign-off; no seat pushes, tags, publishes, or upgrades a host.
- **Rides the merged engine, no new machinery**: runs on the workflow engine + spec language + exception model as shipped, with the v0 hardcode seam (`target.rig: factory-rsi` + `preferred_targets` pin each role 1:1 to a seat) — no binding-layer dependency, no engine change. Runtime config: seats inherit their runtime's default model (no per-seat pin); plan/build/release/orch run on claude-code, and qa/review/dogfood run on codex for cross-runtime diversity against the builder.
**Status**: wrap-gate CLEAR; lifecycle push / npm publish / tag held for founder-auth.
**Status**: wrap-gate CLEAR; lifecycle push / npm publish / tag held for authorization from the project owner.
### Summary For Installing Agents
@@ -619,7 +1402,7 @@ Five read-commands flip from firehose-by-default to compact-by-default — close
### Known Limitations / Carry-Forward
- **Plugin-lineage drift in `openrig-core`** — the openrig-core plugin skill lineage is divergent/stale; full re-sync is OPR.0.4.1.4 (rides 0.4.1). Boot-path layers (canonical + hub cwd) verified current in wrap-gate AC-3 sweep. `rig skill audit` (slice 10) is the runtime mechanism for future drift detection.
- All earlier "PUSHED to 0.4.1" carry-forwards from the original wrap-gate were RESTORED to 0.4.0 during the wrap: slice 34 (`rig ps` current-rig default + `-A`/`--all-rigs` + `resumeTokenPresent`) landed — see Token-Efficient Defaults; slice 35 (`rig scope` stage/verified/reconcile) landed — see New Top-Level CLI Verbs. Real-terminal-related slices 38 + 39 also shipped via founder live-dogfood forward-fix authorization 2026-06-21. Nothing of substance carries forward to 0.4.1 from the original wrap-gate set.
- All earlier "PUSHED to 0.4.1" carry-forwards from the original wrap-gate were RESTORED to 0.4.0 during the wrap: slice 34 (`rig ps` current-rig default + `-A`/`--all-rigs` + `resumeTokenPresent`) landed — see Token-Efficient Defaults; slice 35 (`rig scope` stage/verified/reconcile) landed — see New Top-Level CLI Verbs. Real-terminal-related slices 38 + 39 also shipped via the project owner's live-dogfood forward-fix authorization on 2026-06-21. Nothing of substance carries forward to 0.4.1 from the original wrap-gate set.
### What To STOP Using
@@ -634,10 +1417,10 @@ Five read-commands flip from firehose-by-default to compact-by-default — close
- Wrap worktree clean on `8d55ea60`.
- CLI surfaces source-verified against the command modules at the release SHA.
- The historical CLI→skill cascade sweep reported the internal canonical skill source → product mirror at `packages/daemon/specs/agents/shared/skills/core/openrig-user/SKILL.md` → hub cwd `.claude/skills/openrig-user/SKILL.md` + `.agents/skills/openrig-user/SKILL.md`as byte-identical (reported md5 `e2aa9176`). This records the historical report, not a fresh mirror check.
- cli-reference.md updated for 6 changed commands + new `rig skill` section; `last-verified-against-source` bumped to `8d55ea60`.
- Stale-pattern grep (`dumps everything` / `--notify required` / `rig down 404`) returned 0 hits across active SKILL.md locations.
A harness wraps a model. A rig wraps your harnesses. Define your agent team in YAML, boot it with one command. Claude Code and Codex in the same rig, managed as one system.
OpenRig turns AI coding agents from a pile of terminal sessions into a persistent, organized team. A rig is the team that doesn't go away. If you've got tabs full of agents you're afraid to close, this is the layer you're missing: it's open source, it's local, and you're four commands away.
OpenRig turns AI coding agents from a pile of terminal sessions into a persistent, organized team. Talk to a lead agent about the outcome you want; it can coordinate specialists across teams and bring youresults and decisions that need your attention. Start with a repository and one useful change, then keep the team's work and context at the same addresses.
The OpenRig UI is experimental and in maintenance mode. It is not under active development; support is best-effort. The CLI is the primary supported interface. Contributions welcome.
**Guide:** [Getting started](docs/reference/getting-started.md) · **Stuck?** [Help](docs/reference/help.md) · **Questions:** [Q&A](https://github.com/mvschwarz/openrig/discussions/92) · **Updates and demos:** [@_feralmachine on X](https://x.com/_feralmachine)
## See it running

**Start here:** [the guided first-use path](docs/reference/getting-started.md): install, launch a two-agent team in your repository, and get one reviewed change.
Not setting this up today? Get the next walkthrough and occasional OpenRig updates → https://openrig.dev/follow
## Install and first run
Requires Node.js 22 or 24 and tmux, on macOS or Linux. On a Mac with Apple silicon, use Node.js 22 ([compatibility history](docs/releases/v0.5.15.md#known-compatibility-limitation)). Native Windows is not supported yet, and WSL2 has not been tested. Launching a rig writes provider hooks and workspace trust settings. Before running the commands below, read [what OpenRig changes on your machine](#what-openrig-changes-on-your-machine) and back up the relevant files.
To install with Bun instead, run `bun add -g @openrig/cli`. OpenRig still runs on Node.js, so install Node.js 22 as well. Bun may block this package's postinstall script, in which case the Node.js and SQLite check described under [what OpenRig changes on your machine](#what-openrig-changes-on-your-machine) does not run at install time.
It runs locally: a daemon, a SQLite database, a CLI, and a dashboard. The agents are ordinary Claude Code and Codex sessions in tmux. `product-team` is the fuller starter, 7 seats, 4 Claude and 3 Codex, with an orchestration HA pair plus development and review pods.
Choose the working account you already have: **Claude Code, Codex, or both**. Reuse an explicit choice; no second subscription is required. `rig setup --dry-run` previews the broader setup, but applying `rig setup` checks both harnesses and cmux. It is optional for the [selected-provider path](docs/reference/getting-started.md#choose-your-providers).
Because 4 Claude seats run at once, single-plan users should expect provider throttling. For the light-footprint path, use `conveyor`, a 4-seat starter with 2 Claude and 2 Codex.
Before launching, your agent asks once: **“Allow your agents to run OpenRig commands without repeated permission prompts?” Yes — recommended / No — keep prompts.** This covers every `rig` command, including starting/stopping agents and configuration, at personal project scope unless you explicitly choose user-wide sessions. It is not global YOLO or permission to invent work. On Yes, the agent [adds and verifies native rules](docs/reference/getting-started.md#have-your-agent-configure-permissions); No or no answer leaves settings unchanged. An existing explicit choice is reused. Say “Undo the OpenRig command allowances added by this setup” to remove only its additions.
Check `tmux -V` and only your selected CLI/login: `claude --version` plus
`claude auth status`, or `codex --version` plus `codex login status`. If needed,
sign in once with `claude auth login` or `codex login`; do not install or log in
to an unused provider.
| Team | Starter | Models |
| --- | --- | --- |
| Two Codex agents | `first-project` | Both `gpt-6-astra` (unchanged) |
| Two Claude agents | `first-project-claude` | Configured native Claude default |
| Claude owner + Codex checker | `first-project-mixed` | Claude default + `gpt-6-astra` |
All three use the same owner/checker roles and task. Show the selected runtime,
configured model and command before launch; confirm the account supports the
model instead of silently falling back. The kernel starts automatically and
selects from available authenticated providers independently of these two project
agents. A missing unused provider is not a setup requirement.
```bash
# Preview the smaller starter
rig specs preview conveyor
# Boot the light-footprint path
rig up conveyor
cd /path/to/your/repository
starter=first-project # or first-project-claude or first-project-mixed
rig specs preview "$starter" --kind rig
rig up "$starter" --cwd . --plan
rig up "$starter" --cwd .
rig tui --shared
```
## First Run
The kernel provides separate operational support and the shared dashboard. To detach without stopping the dashboard, press Ctrl-b then d; `rig tui --shared` returns to that view. Plain `rig tui` opens an independent view. Closing a viewing terminal does not mean you should relaunch the team.
After `product-team` boots, `rig ps` shows the fleet map and `rig ps --nodes --rig product-team` shows the running pods and seats. When you're ready to shut the team down and bring it back, use the same rig name:
Check project-seat readiness with `rig ps --nodes --rig "$starter"` and resolve any authentication, trust or permission prompt before assigning work. Then give the owner one bounded outcome from your repository:
```bash
rig ps
rig down product-team
rig up product-team
rig send "dev-owner@$starter"'Implement <one useful change>. Track the task in the queue and return its ID. Keep it local, verify the behavior, ask dev-check in this rig to check the exact candidate, and record the result and how I can try it.'
rig queue list --destination "dev-owner@$starter" --limit 1000
```
`rig ps` is a fleet glance. `rig down product-team` snapshots the team and stops it. `rig up product-team` brings it back by name from that snapshot.
Sending a message does not itself create a queue item; the owner records the task. Read the final artifact and the review of its exact candidate, then return to the same owner for the next change. [The guided first-use path](docs/reference/getting-started.md) covers readiness, a useful task, a reviewed result, Herdr/cmux terminals and recovery.
Not setting this up today? Get the next walkthrough and occasional OpenRig updates → https://openrig.dev/follow
- **Releases:** [GitHub Releases](https://github.com/mvschwarz/openrig/releases) and npm `@openrig/cli`
We aim to acknowledge issues and pull requests within one day; see [CONTRIBUTING.md](CONTRIBUTING.md#what-to-expect-from-us) for review targets.
## What OpenRig changes on your machine
OpenRig writes instance state, provider integration and workspace files as part
of setup and operation. These include **trust settings and executable hooks**.
The summary below follows this source revision; check `rig --version` when
using a published package, since repository guidance can be ahead of npm.
| When | What changes and why |
| --- | --- |
| **npm installation** | Installs the CLI, bundled components and dependencies under your npm prefix (with Bun, under Bun's global directory). OpenRig's postinstall checks the Node.js version and that the SQLite module loads; Bun may block this script. It does not run daemon or provider setup. |
| **`rig setup`** | Attempts missing tools and writes an OpenRig block in `~/.tmux.conf` for mouse support and scrollback. On macOS it can install cmux and enable its automation socket control in `~/.config/cmux/settings.json`. `--full` adds workstation tools. `--dry-run` shows setup's plan without applying it. |
| **Daemon startup** | Creates/updates instance state under `OPENRIG_HOME` (normally `~/.openrig`), including its database and managed plugin resources. Seeds the `openrig-skills` discovery skill in `~/.claude/skills` and `~/.agents/skills`, subject to existing version ownership. With `runtime.codex.hooks_enabled` enabled (the default), writes Codex hook configuration and trust records as described below—even before a rig launches. |
| **Rig/seat launch and attachment** | Creates tmux sessions, supplies seat identity and daemon connection environment, and projects selected guidance, skills, plugins and runtime resources into the workspace. Managed startup pre-trusts the workspace. Claude context collection can also be provisioned for attached sessions and refreshed during monitoring. |
| **Explicit permission configuration** | The built-in bootstrap does **not** add `rig` command allow rules. Agent-guided setup recommends Yes and requires your actual answer before the agent [adds rules at your chosen scope](docs/reference/getting-started.md#have-your-agent-configure-permissions). No/no answer preserves settings; existing choices and stricter rules remain relevant. Broader access is separate. |
The provider files are separate from instance state. Here `~` means the daemon
user's home; changing `OPENRIG_HOME` alone does not isolate provider configuration.
- **Protect** a seat where you type by hand: `rig seat set-typing-guard <seat> --enabled true --reason <text>` holds automatic messages and wakes instead of typing them into that seat (off by default; see `rig seat set-typing-guard --help`)
- **Connect** Slack through an app you create in your own workspace; the experimental `rig slack manifest` prints that app's manifest ([setup guide](docs/reference/slack-app-setup.md))
Every agent runs in a tmux session you can attach to, inspect, and work with directly.
## Starter Rigs
OpenRig's hero starter is `product-team`, the fuller product-development rig:
Use `first-project`, `first-project-claude`, or `first-project-mixed` for the
same focused first-use path on your selected providers. `product-team` is an optional
larger product-development example:
```bash
rig specs preview product-team
rig specs preview product-team --kind rig
rig up product-team
```
Use it when you want the week-one experience OpenRig is built around: an orchestrator HA pair, development work, review work, and enough moving pieces for the coordination layer to matter.
Use it when you want a larger product squad: two orchestrators, implementation, QA, design, and two independent reviewers.
For a smaller starter, use `conveyor`:
```bash
rig specs preview conveyor
rig specs preview conveyor --kind rig
rig up conveyor
```
`conveyor` is the smallest shippable software factory, one command. It keeps the footprint lower for single-plan users while still showing a real handoff path through intake, planning, build, and review.
`conveyor` is a four-seat starter mixing Claude Code and Codex. It shows a handoff path through intake, planning, build, and review; `first-project` remains the smaller two-seat starting point.
Also ships: `implementation-pair`, `adversarial-review`, `research-team`, and `secrets-manager` (HashiCorp Vault managed by a specialist agent).
@@ -83,10 +206,10 @@ rig specs ls
## How It Works
OpenRig is a local daemon + CLI + MCP server + React UI, built on tmux.
OpenRig is a local daemon + CLI + terminal UI + MCP server, built on tmux. The older React web UI remains in maintenance mode with best-effort support.
```
CLI / UI / MCP
CLI / TUI / MCP
|
Hono HTTP daemon
|
@@ -95,16 +218,33 @@ Hono HTTP daemon
SQLite + tmux + runtime adapters
```
- **CLI**: 40+ commands designed for both humans and agents. Every mutating command ends with what happened, current state, and next action.
- **UI**: Explorer sidebar, topology graph with pod grouping, node detail panel, system log, chatroom.
- **MCP**: 17 tools so agents can manage their own topology (`rig_up`, `rig_ps`, `rig_send`, `rig_chatroom_send`, etc.)
- **Runtimes**: Claude Code, Codex, and terminal nodes. Adapters for Pi and OpenCode in development.
- **CLI**: Commands for both humans and agents to launch teams, inspect state, send messages, track owned work, and manage context.
- **TUI**: Topology explorer, table and graph views, seat details, Specs, Projects, Terminals, Feed, and System. Navigate with the keyboard, mouse, or command bar.
- **MCP**: Tools so agents can manage their own topology (`rig_up`, `rig_ps`, `rig_send`, `rig_chatroom_send`, etc.)
- **Runtimes**: Native Claude Code and Codex sessions, terminal nodes, and a Pi adapter using an RPC runner inside a terminal pane.
## Terminal UI and Workspaces
The TUI shows the team's coordination state; herdr and cmux show the actual agent terminals alongside it. Use `rig tui commands` to list the TUI's command-bar navigation, or [try the interactive TUI tour](https://openrig.dev/tour/workspace).

*Captured from the interactive TUI demo using fictional project data.*
With herdr installed and connected, open the starter's terminals together:
```bash
rig terminal open first-project --provider herdr
```
For cmux, use `--provider cmux`. In the TUI, a rig's detail view has a `term ▸ rig <name>` link that opens every running seat of that rig in the default terminal provider; with herdr that is up to 16 seats per tab, in a workspace named after the rig. The underlying sessions remain accessible through tmux. See the [terminal workspace guide](docs/reference/getting-started.md#share-the-dashboard-and-return-to-it) for setup and returning to an existing view.
- **AgentSpec**: Reusable agent blueprint with skills, guidance, hooks, profiles, and startup contracts.
- **Pod**: Bounded context group. Agents in a pod share memory and can maintain each other's context.
- **Seat**: A stable role and address in a rig, such as `dev-owner@first-project`. The conversation occupying it can change while its identity and authored context remain.
- **Pod**: A group of related seats with shared guidance and context. Each agent still has its own context window.
- **Discovery**: `rig discover` fingerprints existing tmux sessions. `rig adopt` brings them under management.
- **Snapshot/Restore**: `rig down --snapshot` captures full state. `rig up <name>` restores from latest snapshot. Restore reports per-node outcomes (resumed, fresh, or failed).
- **RigBundle**: Portable archive with vendored AgentSpecs and SHA-256 integrity. Share topologies across machines.
@@ -122,13 +262,83 @@ rig send vault-specialist@secrets-manager "Check Vault health and report status.
Requires Docker for service-backed rigs.
## Upgrading an existing instance
For an existing installation, follow the [upgrade procedure](skills/_canonical/core/openrig-upgrade/SKILL.md) and the [0.5.14 release notes](docs/releases/v0.5.14.md). Preserve live seats during the upgrade; `rig down` is not an upgrade step. Upgrading to 0.6.0 also requires Node.js 22 or 24: see [Moving off Node 20](#moving-off-node-20) and the [0.6.0 release notes](docs/releases/v0.6.0.md).
### Moving off Node 20
OpenRig 0.6.0 supports Node.js 22 and 24 only. Its SQLite binding
(better-sqlite3 13) requires Node 22 or newer. Node 20 is no longer supported;
the install check refuses it with an explanation.
If you run OpenRig on Node 20, switch Node first, then reinstall the CLI under
the new Node (a version manager keeps a separate global package set for each
Node):
```bash
nvm install 22# or 24; fnm or your package manager work the same way
npm install -g @openrig/cli
rig --version
```
Your existing OpenRig data stays where it is. The daemon reopens the same
database under the new binding and applies any pending migrations in place.
Restart the daemon under the new Node by following the upgrade procedure above.
### Crossing the 0.5.9 layout boundary
The migration below still applies when upgrading from a pre-0.5.9 instance.
0.5.9 makes `$OPENRIG_HOME/context` the addressable context library, writes
Claude telemetry to `state/context-usage` (and provider telemetry to
`state/provider-usage`), and installs the default System World at
`context/system/system-world.yaml`. Existing instances cross this boundary by
an **Agent-Operated Migration** from the shipped `openrig-upgrade` skill. The
target runtime reads canonical-first with legacy-fallback while new writes use
the canonical roots; a custom context-library root stays stable during
activation. This is not a directory rename to do while an old collector writes.
```bash
# SKILL_DIR is the installed openrig-upgrade skill directory.
`--help` prints the phase grammar without inventorying the instance. No phase
flag intentionally runs the read-only plan; unknown options fail nonzero before
plan or mutation.
Every phase emits JSON. Stop on any issue or incomplete receipt and follow its
`next` action; do not continue from copied legacy telemetry or retry a partial
mutation blindly. Preparation leaves legacy state and collector settings in
place. Verification accepts exact tail bytes only when that same seat has newer
paired context and provider samples under `state/`; finalization revalidates the
accepted tails, copies the library without overwrite, and switches config last.
The helper never removes the legacy telemetry or library. Retirement follows
separate stable runtime, writer, reader, and recovery proof. Daemon, database,
seat, plugin, and release lifecycle actions remain agent-owned.
## Requirements
- Node.js 20, 22, or 24 (even-numbered LTS releases; odd releases like 25 lack native addon prebuilds)
- Node.js 22 or 24 (the supported versions in this release). Node 20 is no
longer supported. Node 26 and other versions are untested. On a Mac with Apple
silicon, use Node.js 22: see the [compatibility history](docs/releases/v0.5.15.md#known-compatibility-limitation).
- tmux
- macOS or Linux. Native Windows is not supported yet, and WSL2 has not been tested
Optional:
-cmux for `Open CMUX` node surface controls
-herdr or cmux for terminal workspaces showing the agents together
- Docker for service-backed rigs and managed apps
## Setup and Troubleshooting
@@ -139,30 +349,31 @@ Optional:
Both commands support `--json` for agent-driven workflows.
Managed runtime boot (during `rig up`) may modify runtime config for core bootstrap and spec-selected runtime resources. `rig setup` discloses these paths so agents know what may be changed:
- global Claude: `~/.claude/settings.json` for minimal OpenRig command allowlisting
- global Claude state: `~/.claude.json` for managed workspace trust and onboarding completion
- project Claude: `.claude/settings.local.json` for context collector/activity hooks and selected `claude_settings_fragment` resources
- project Claude MCP: `.mcp.json` for selected `claude_mcp_fragment` resources
- global Codex: `~/.codex/config.toml` for workspace trust and selected `codex_config_fragment` resources
Before setup or managed launch, review [what OpenRig changes on your machine](#what-openrig-changes-on-your-machine), including provider trust, hooks and selected runtime resources.
Already-running adopted sessions may need restart before they pick up newly written runtime config.
**For agents:**Ask the user whether they want core setup (`rig setup`) or the fuller workstation path (`rig setup --full`) before choosing the invocation. Inspect the result with `--json` and use `rig doctor` to finish any remaining machine-specific issues.
**For agents:**First ask which available provider(s) the user wants, reusing an explicit choice. Recommend the working account they already have and follow the selected-provider guide above. Offer core setup (`rig setup`) or the fuller workstation path (`rig setup --full`) only when they want those additional tools. Inspect the result with `--json` and use `rig doctor` to finish any remaining machine-specific issues. When OpenRig itself misbehaves, read [docs/reference/help.md](docs/reference/help.md) (installed agents can run `rig context get help`; the same text is at [openrig.dev/help/agents](https://www.openrig.dev/help/agents)): it covers the next step, known problems, and how to reach the team if you're still stuck.
## Comparison with Claude Managed Agents
Anthropic shipped Claude Managed Agents — a cloud-hosted, Claude-only runtime at $0.08/session-hour. OpenRig is the local side: open source, cross-harness, runs on your machine, costs nothing.
OpenRig is open source and self-hosted, with Claude Code and Codex in the same team. You operate it on your own infrastructure; the selected providers' model usage costs still apply.
"description":"OpenRig is an open-source multi-agent harness: define a team of Claude Code and Codex agents in YAML, boot it with the rig CLI, and coordinate the agents through tmux sessions, a durable queue and a local daemon.",
"folders":[],
"excludeFolders":[
"docs/releases",
"packages/daemon/specs",
"packages/daemon/test",
"packages/test-system",
"skills/_canonical/process",
"skills/_canonical/pm",
"spike",
"docker",
"**/node_modules",
"**/fixtures"
],
"excludeFiles":[],
"rules":[
"Install with npm install -g @openrig/cli. OpenRig requires Node.js 22 or 24 and tmux; the first-project starter also requires an authenticated Codex CLI.",
"Preview before launching: rig specs preview <spec>, then rig up <spec> --cwd . --plan, then rig up <spec> --cwd .",
"Check seat readiness with rig ps --nodes --rig <rig>, not only daemon health from rig status.",
"Inside a rig, an agent runs rig whoami --json first and treats its output as its identity.",
"rig send <session> \"message\" types into a peer's terminal and presses Enter; rig capture <session> reads that peer's screen.",
"Work another agent must act on goes in the queue (rig queue create or rig queue handoff); a message informs but does not assign work.",
"Use rig start to bring rigs back after a reboot or daemon restart.",
"Run rig --help for the current command surface; the CLI is authoritative over older examples."
3. Two-step tmux send: a unique file/buffer pasted with `paste-buffer -d -r -p`at every payload size → ~200ms delay → separate `C-m`. Bracketed paste preserves multiline input in supporting TUIs; the payload never enters a shell argument. A successful paste proves transport execution, not runtime consumption.
Select the actual fields present in that file. Redirect intentional full CLI
output to a file before inspecting selected fields; use `set -o pipefail` when
piping a command so a formatter cannot hide its failing exit.
- Binary: `rig`
- Top-level command groups: `64`
- Output mode: human-readable by default; many commands also support `--json`
@@ -116,9 +259,34 @@ Subcommands:
-`logs [--follow]`
Notes:
-`start`launches the daemon process and accepts runtime overrides for port, host, and DB path.
-`start`additively reconciles the canonical instance layout before launching
the daemon process, then accepts runtime overrides for port, host, and DB
path. Direct daemon first start uses the same initializer. Existing files are
preserved and wrong-type managed paths are refused before any write. See
`docs/reference/instance-layout.md`.
- Startup uses a local `daemon-start.lock` reservation before instance initialization,
shared by `daemon start`, `start`, and `up`. A concurrent supported launch fails
without spawning another daemon or running its pre-bind database initialization.
Success requires the spawned child's numeric PID from `/healthz` at every required
listener and a live child through atomic `daemon.json` publication. Missing or
mismatched process identity, an invalid listener plan, child exit, and unresolved
probes cannot publish success. Use a matching CLI/daemon pair; a legacy endpoint
without PID evidence is insufficient. This local reservation does not serialize
old binaries or direct execution of the daemon entrypoint.
- Startup checks physical liveness after the synchronous state writer as well as
before it; queued child events alone cannot prove that boundary. If publication
fails verification, startup rejects and withdraws only state matching this
launch's PID, start time, listener and DB. It never removes a replacement owner.
- Failed process inspection is uncertainty, not proof that a child exited. Cleanup
waits for the owned child's exit evidence; an error event is insufficient.
- Failed startup signals only its own child and waits boundedly for its exit. If
cleanup cannot be confirmed, the reservation remains and the error names the PID.
An interrupted launcher can also leave `daemon-start.lock`. Inspect its recorded
launcher/child PIDs, `daemon.json`, and `daemon.log`; only after proving both
processes absent, archive that reservation before retrying. There is no timed
takeover: a dead launcher may have left a live, unbound child.
-`logs` reads daemon log output and can follow it.
- **Deploy identity (v0.4.4, OPR.0.4.4.11 FR-6/7)**: a PACKAGED build (built via `scripts/build-package.sh`) is stamped with `{semver, commit, dirty, builtAt}`; the daemon's `/healthz` payload carries the four stamp fields additively and `rig --version` renders `<semver> (<commit8>[, dirty])`. A source/dev run has NO stamp and adds NOTHING (never an invented SHA) —`/healthz`keeps its legacy body and `--version` prints the plain semver. This is the 30-second stale-deploy diagnostic: an unstamped or old-commit `/healthz` on a long-running host means you are looking at an older deployed build, not the source tree. Source: `packages/{daemon,cli}/src/build-info.ts` (`stampFields`), `packages/daemon/src/server.ts` (`/healthz`), `packages/cli/src/version.ts`.
- **Deploy identity (v0.4.4, OPR.0.4.4.11 FR-6/7)**: a PACKAGED build (built via `scripts/build-package.sh`) is stamped with `{semver, commit, dirty, builtAt}`; the daemon's `/healthz` payload carries the four stamp fields additively and `rig --version` renders `<semver> (<commit8>[, dirty])`. A source/dev run has no build stamp (never an invented SHA);`/healthz`still reports its runtime PID and `--version` prints the plain semver. This is the 30-second stale-deploy diagnostic: an unstamped or old-commit `/healthz` on a long-running host means you are looking at an older deployed build, not the source tree. Source: `packages/{daemon,cli}/src/build-info.ts` (`stampFields`), `packages/daemon/src/server.ts` (`/healthz`), `packages/cli/src/version.ts`.
### `rig status`
@@ -154,6 +322,18 @@ Supported keys:
-`transcripts.enabled`
-`transcripts.path`
-`workspace.root` (and other workspace-rooted paths used by `init-workspace`)
-`snapshots.periodic.interval_seconds` (default `300`) — interval between periodic snapshots
-`snapshots.periodic.retention_keep` (default `10`) — number of periodic snapshots to retain per rig
@@ -168,7 +348,7 @@ Precedence:
Notes:
-`--with-source` (top-level) and `--show-source` (`get`) report per-key source/default for honest provenance.
-`init-workspace`scaffolds the default workspace at `~/.openrig/workspace/` (or the `--root` override) with mission/slice folders. `--force` overwrites scaffolded files (does NOT remove directories); `--dry-run` previews without writing. New in v0.3.0.
-`init-workspace`additively scaffolds `missions/`, `exhaust/`, `SPEC.md`, `project.yaml`, `workspace.yaml`, and `.gitignore` at `~/.openrig/workspace/` (or the `--root` override). `--dry-run` previews without writing. `--force` is deprecated compatibility and preserves existing files. New in v0.3.0.
-`snapshots.periodic.*` (v0.3.4): the daemon-side scheduler takes periodic snapshots per rig at `interval_seconds` and retains the newest `retention_keep`. At restore time, newest-wins between `auto-periodic` and `auto-pre-down` snapshots.
Legacy env compatibility: the original runtime keys still accept deprecated
@@ -429,7 +609,7 @@ Notes:
### `rig snapshot`
Usage:
-`rig snapshot <rigId>`
-`rig snapshot <rigId> [--intended-seats <ids>]`
-`rig snapshot list <rigId>`
Subcommands:
@@ -437,7 +617,9 @@ Subcommands:
### `rig restore`
Usage:`rig restore <snapshotId> --rig <rigId>`
Usage:
-`rig restore <snapshotId> --rig <rigId>`
-`rig restore status <attemptId> --rig <rigId> [--json]`
Important:
-`--rig <rigId>` is required by the source code, even though the help text does not visually mark it as required.
@@ -445,6 +627,9 @@ Important:
Notes:
- Human output prints each restored node and any failed node error.
- Non-zero exit if any restored node fails.
- A started asynchronous restore prints its attempt id. `status` derives the
original and current intended-set verdict, snapshot selection, historical
exclusions, and unresolved intended seats from that durable attempt.
-`nodeRef` (optional) can be a logical ID or node ID for the single-target form.
-`--seats <ids>` (v0.3.4, slice 11) takes a comma-separated list of logical IDs for node-granular managed partial restore — launch a named subset of seats while holding the rest. Retires the prior `pod_aware_launch_unsupported` dead-end.
-`--hold-reason <reason>` records the reason non-target seats are being held; surfaced via observability so the held state is auditable.
-`--snapshot-id <id>` selects one exact restore-usable snapshot instead of
applying the automatic choice. `--plan` previews a multi-seat subset and its
-`--through` accepts either one path-like context ref or an ordered list of existing local files; mixing the two forms is rejected.
- Sends one piece at a time through the normal transport and waits `--pace` between pieces (default `10s`; `500ms`,`10s`, and bare seconds are accepted). There is no trailing delay.
- Sends one piece at a time through the normal transport and waits `--pace` between pieces (default `10s`; duration overrides require an explicit `ms` or`s` suffix). The same explicit-unit grammar applies to `--consume-timeout`, `--consume-poll`, and `--turn-timeout`; bare numbers are refused. There is no trailing delay.
- A missing local file or missing/unreadable ref member aborts before the first send, so a walk delivers every piece or none.
- Where a generation record resolves, each piece must appear as a complete user message in the newly appended record and its corresponding native turn must close before the next piece. Only CRLF line endings and surrounding whitespace are normalized; internal whitespace, missing middles, shared prefixes, and tails do not qualify.
- Claude closure follows the message's UUID ancestry through an assistant response to `turn_duration`. Codex uses a `response_item` user message within a named `task_started`/`task_complete` turn; queued input or a different turn's completion is insufficient. These receipts prove delivery and turn completion, not semantic comprehension.
- Codex generation lookup joins the bound process's post-start log thread IDs to retained native CLI conversations. Auxiliary title threads cannot replace that identity; zero or multiple matching conversations remain unverified instead of being selected by recency.
- Codex records resolve through the verified current pane/process and native thread table, including before token telemetry exists. The rollout header must identify that thread. Missing or ambiguous identity is reported as unverified. A generation/file replacement or unreadable record during a verified walk aborts it; it never silently continues into a replacement occupant.
-`--json` reports `consumptionVerified`. If the initial generation-record probe is unavailable, legacy delivery remains possible with an explicit unverified advisory and `consumptionVerified: false`.
### Cross-host execution (`--host <id>`)
@@ -816,6 +1040,21 @@ commands use single-hop SSH CLI-side shell-out; HTTP-transport commands talk to
the remote daemon API. The local daemon is not involved in SSH routing. The
remote host is expected to have its own managed `rig` available on `$PATH`.
HTTP sender attribution uses the originating instance's persisted self-host
identity, read without creating or changing its database. Explicit DB config
wins; otherwise the last daemon launch's DB selection is retained. This works
while the local daemon is stopped. The destination's identity and the configured
display name are never used as the origin.
Ordinary local requests retain bare seat names. For an explicit `OPENRIG_URL`
(or legacy `RIGGED_URL`), a bounded health probe preserves bare addressing only
when the target's self-host identity matches the local one. A different,
unavailable, or ambiguous target—including a loopback forwarding endpoint—gets
the known origin suffix. Already-qualified senders remain unchanged. If the
local origin cannot be read, delivery proceeds with `origin-unknown:v1`
provenance and a diagnostic after a successful response; queue forwarding
preserves that uncertainty rather than attributing the sender to the relay.
Hosts are declared by the operator in `~/.openrig/hosts.yaml`:
- **`--full`** → add body + chain-of-record + full tags + transition history to whichever scope is selected (field-breadth axis).
- **`-o json|wide`** → encoding, compact-by-default. `-o json` does NOT imply full body (compact JSON is token-safe + machine-parseable); `--full -o json` returns the full JSON.
- **`--mine`** → just the caller's items.
- **`--owned`** → obligations assigned to the caller (`destination_session` only).
- **`--mine`** → the caller's source-or-destination union, including rows the caller authored but does not own.
- Pipeline use must enable `set -o pipefail`; otherwise the shell reports only the downstream formatter's status and can mask a nonzero `rig` read such as a timeout.
-`--destination <s>` / `--source <s>` / `--state <csv>` keep working and compose with the new flags.
- The four axes (scope × history × field-breadth × encoding) are orthogonal and composable. The bare unscoped firehose that aggregated cross-rig + full-history (~64,000 tokens on the live host) is retired as a default — opt-in via `-A -a --full`.
- Use `rig queue show <qitemId>` for the full single-item view (kubectl `describe` / docker `inspect` pattern).
- Use `rig queue show <qitemId>` for the body preview; `rig queue show <qitemId> --full --json` returns the original complete record. Preview JSON adds `readView` with completeness, omitted content, full JSON byte size and the exact expansion command.
-`overdue [--json]` — in-progress qitems past closure_required_at
-`lease-acquire [options]` — acquire the active classifier lease for the caller.
-`lease-heartbeat [options]` — send a heartbeat for an active classifier lease (extends TTL).
-`lease-heartbeat [options]` — send a heartbeat for an active classifier lease (extends TTL). A lease already past its TTL refuses with `lease_expired`; acquire again instead.
-`lease-show [options]` — show the currently-active classifier lease.
-`reclaim-classifier [options]` — operator verb to reclaim the active classifier lease. Use `--if-dead` to refuse if holder is still alive.
-`classify <streamItemId> [options]` — project a stream item with classification fields. Idempotent on `stream_item_id`; requires an active lease.
-`list [options]` — list project classifications with filters.
-`classify <streamItemId> [options]` — project a stream item with classification fields. Idempotent on `stream_item_id` (first write wins). Requires `--lease-id`: the result is refused (`lease_mismatch`, `lease_expired`, `lease_held`) unless that exact lease is active, unexpired and held by `--session` at write time. Optional `--area`, `--scope-ref` (with `--candidate-set-version`), `--duplicate-of`, `--needs-human true|false` (omit when unknown), `--classifier-version`, `--taxonomy-version`, `--attempt-id` with `--execution-id`. Without `--attempt-id` the result is a manual classification, not bound to the attempt ledger. Every supplied field must be a string (IDs and versions non-empty); `--needs-human` omitted means unknown.
-`list [options]` — list project classifications with filters (`--session`, `--destination`, `--area`, `--scope-ref`, `--needs-human true|false|unknown`).
-`show <projectId> [options]` — show one project classification.
Notes:
- Lease semantics: only one classifier holds the active lease at a time. Heartbeats extend TTL; lease expiry frees the slot for the next acquirer.
- Lease semantics: only one classifier holds the active lease at a time. Heartbeats extend TTL only while the lease is unexpired. Re-acquiring your own expired lease issues a new lease id, so results bound to the old id are refused; another session must use `--evaluate-deadness-first` or the reclaim verb.
- Attempt ledger (daemon HTTP, used by a classifier occupant): `POST /api/projects/attempts/begin`, `/attempts/:id/abstain`, `/attempts/:id/fail`, `GET /api/projects/eligible`. Abstentions and errors are recorded there, never in the classification row; errors retry with bounded backoff, then end `exhausted`. Every `begin` (including a timeout or error retry) returns a new `executionId`; only the current one can abstain, fail or bind a result (`attempt_superseded` otherwise), because a renewable lease never proves an older execution stopped.
-`classify` enforces L1→L2 foreign-key existence: the referenced `stream_items` row must exist or the call is rejected before any state mutation.
-`reclaim-classifier` is the operator-side verb to recover from a hung classifier; `--if-dead` adds a liveness guard so a still-heartbeating classifier is not stolen from.
@@ -1191,16 +1433,206 @@ Phase D extends the policy enum with `workflow-keepalive` (the policy deferred f
### `rig workflow`
-`validate <specPath>` — validate a workflow spec file; returns structured ok/error report (role resolution, step uniqueness, allowed-exits consistency, optional seat liveness). Spec-only: it takes no rig context (OPR.0.4.6.FAC1 arch ruling — rig-coverage checks happen at instantiate).
<text> --created-by <session>` — compile and start an eligible lifecycle;
`--entry-owner` and `--rig` retain their normal workflow meanings. The opaque
operation key makes exact replay idempotent and conflicting reuse refuses.
- `instantiate <specPath> --root-objective <text> --created-by <session>` — create a new instance + entry-step qitem in the same daemon transaction; `--entry-owner <session>` overrides the default entry owner. **OPR.0.4.6.FAC1**: `--rig <name>` binds the instance to a rig (overrides the spec's `target.rig` DEFAULT; persists as `boundRig` on the instance, rendered by `show`/`trace` and carried in `--json`). On a bound instance, a role with **no `preferred_targets`** resolves to a live capable SEAT on that rig by the pure capability policy (running agents declaring the role, managed seats only, required runtime, least pending backlog, deterministic coordinate tiebreak). Unknown rig = structured `bound_rig_unknown`; a bound-rig role no seat structurally declares = `bound_rig_role_uncovered` (existence at any lifecycle state satisfies it — liveness is checked when the step projects). No `--rig` and no spec default = unbound, byte-identical pre-FAC-1 behavior.
- `run <specPath> …` — accepts the same `--rig <name>` binding (run instantiates too).
- `project --instance <id> --current-packet <qitem-id> --exit <handoff|waiting|done|failed> --actor-session <session>` — close the current packet AND project the next-step packet IN THE SAME daemon transaction (transactional-scribe; lost handoffs impossible by design). `--result-note <text>`, `--blocked-on <ref>`, `--next-owner <session>` modify behavior.
- `list [--status <s>]` — list instances; optionally filter by status (`active`/`waiting`/`completed`/`failed`).
- `show <instanceId>` — show one instance.
- `revise <instanceId>` — inspect authored versus bound lifecycle input without
writing. The result names changed sources/steps, composition, compatibility,
and an apply command containing the inspected version, digest and operation
key. Fill in the actor and decision before using `--apply`.
- `operation <key>` — recover the original lifecycle creation or revision
receipt and current instance after a lost response or later source edit.
- `trace <instanceId>` — show the instance + its append-only step trail (audit-only).
- `continue <instanceId>` — idempotent inspector; in v1 returns the current state.
The owner-as-author + workflow-as-transactional-scribe contract is enforced by the daemon. The owner of a packet decides when it closes; the workflow runtime atomically records the closure AND creates/projects the next qitem per the workflow spec, in a single daemon transaction.
### Project-owned release profiles
`project.lifecycle.profile` selects a key in `project.lifecycle.profiles`. Each
selected entry contains `required_steps` (a nonempty list of stable obligation
IDs) and `workflow` (the ordinary workflow language). The reusable example at
Adding `re_present_max_seconds` opts into an existing queue/watchdog timer that
repeats with exponential backoff: the example waits 5, 10, 20, 40, then 60 minutes
between reminders. The cap must be an integer at least as large as the initial
delay. No new packet is created. Repeated waiting acknowledgments retain the
schedule; changed structured `closureEvidence` or a new blocker resets it.
`rig workflow project --evidence-ref <ref>` records an attributed reference as
closure evidence, without interpreting it or substituting for typed acceptance. Omitted
evidence preserves the prior evidence, and object key order is immaterial.
A transition on the exact qitem blocker makes the reminder due on the next
scheduler tick (normally within one second), resetting the initial delay.
Wake delivery receipts do not count as progress. The workflow's own waiting
acknowledgments are on its frontier packet, not its upstream blocker; a repeating
wait cannot name itself as its blocker. Blocker completion retains native
queue unpark/wake behavior. Terminal or rerouted packets retire their timer.
Restart reconciles blocker transition identity and resumes the persisted delay;
replaying the same transition does not create another wake. A live repeating
timer can have an unconsumed wake: these are separate facts in park status.
Hard human gates are authored using the existing `gate` field; a reminder does
not manufacture a gate or satisfy one.
## Operational Inspection
Read-only inspection commands for compaction planning, workflow heartbeat, and seat handover observability. Default mode is read-only across this section.
— create a deliberate blank occupant for exactly one existing seat. No
continuity source is used; a live managed occupant requires `--stop`, while
adopted or unmanaged ambiguity refuses.
- `clear-attention <session> [--reason <text>] [--json]` — evidence-gated, operator-attested, audited reconcile of a stuck `attention_required` seat.
Notes:
@@ -1273,26 +1709,60 @@ Notes:
Usage: `rig context <subcommand>` — browse, preview, compose, and manage operator-authored context packs. This noun never delivers; delivery belongs to `send`, `broadcast`, `walk`, and `queue create`.
Subcommands:
- `work-install [--project <id>] [--mission <id>] [--slice <id>] [--deliver] [--runtime <claude-code|claude|codex>] [--cwd <agent-working-directory>] [--topology <ids>] [--apply-skills] [--json]` — resolve project/mission/slice Markdown plus `project.yaml install.skills` in one plan. With `--runtime`, composes system, topology, and project selectors and reports per-skill provenance/status; `--apply-skills` safely reconciles the owned harness projection into `--cwd` (default: the caller's current working directory), not the workspace metadata directory. Omitting `--runtime` skips skill inspection regardless of `OPENRIG_RUNTIME`; applying skills requires an explicit runtime.
- `profile <name-or-ref> --situation <fresh|handover|post-compaction> [--runtime <claude-code|claude|codex>] [--profile <id>] [--budget <tokens>] [--rig <rig> --seat <seat>] [--mission <mission>] [--slice <slice>] [--json]` — compose the selected atom graph and explicitly granted context. Runtime defaults to `OPENRIG_RUNTIME`, otherwise Claude; an unknown nonempty environment value warns and falls back to Claude. An explicit flag overrides the environment.
- `list [options]` — list all context packs in the library.
- `preview <name-or-ref> [options]` — show the assembled bundle without delivering it.
- `compose --out <ref> --from <files...>` — compose ordered files into a durable context ref without delivering it.
- `sync [options]` — re-walk discovery roots and refresh the library index.
-`add <source-dir> [options]` — install a context pack from a local directory into the configured context store.
- `add <source> [--git] [--checkout] [--pack <relative-path>] [--name <ref>] [--json]` — install a directory/manifest URL, or clone a Git repository with `--git`. Git discovery checks the repository-root manifest and `.openrig/context-packs`; multiple packs require an explicit `--pack`. `--checkout` selects an existing local checkout whose branch may be merged by an update.
- `source inspect <ref> [--json]` — distinguish the selected revision/digest, current served edits, checkout branch/revision/status/conflicts, and locally known upstream divergence. It does not fetch or prove consumption.
- `source update <ref> [--json]` — explicitly fetch and merge the selected checkout branch’s upstream, then publish its clean declared pack inputs. Dirty checkouts or edited served selections stop before update. Conflicts/unavailable upstream retain the old served selection and both Git sides.
- `rm <ref> [options]` — remove a context pack by its path-like ref.
Notes:
- Context packs are operator-authored bundles of context (manifest + files) intended to prime a managed seat with a coherent starting context.
- Both `profile` and `work-install` accept `claude-code`, its alias `claude`, and `codex`. Invalid explicit values fail during CLI argument parsing, before context lookup or projection. JSON metadata retains the consumer's existing keys: `profile.runtime` is `claude` or `codex`; `skillProjection.runtime` is `claude-code` or `codex`. Both Claude spellings produce the same selection and metadata within each command; manifest runtime keys remain unchanged.
- For example, `rig context profile world-public --situation fresh --runtime claude-code` and `rig context work-install --runtime claude-code` use the same runtime spelling. Profile atoms that read seat context still require both `--rig` and `--seat`.
- `preview` is the canonical read-only way to inspect the assembled content.
- This command family is delivery-free; context-window inspection is not part of this noun.
Git source example (use the intended instance and existing Git credentials):
- `validate [root] [--kind <kind>] [--no-recursive] [--require-frontmatter] [--max-files <n>] [--json]` — walk a workspace root, parse each `.md` file's YAML frontmatter, and emit a structured gap report. Advisory only — never modifies files. Default root: `cwd`. `--kind` validates against a specific workspace kind (`user | project | knowledge | lab | delivery`). `--max-files` (default `10000`) hard-caps the walk; v0.3.2 slice-01 GA enforces strict-int regex on this flag.
-`doctor [--workspace <path>] [--strict] [--json]` — v0.3.2 slice-21 FR-5 (+1 check v0.4.4 slice 23). Run an 8-check workspace-readiness diagnostic against the daemon's resolved workspace (workspace root, missions folder, file allowlist, daemon alignment, daemon reload, optional slice docs, MISSION_NOTES presence, SDLC convention sections — the last advisory-warn per `docs/reference/sdlc-conventions.md`). Read-only. Default exit-code: non-zero only on `fail`; `--strict` makes warn-or-fail non-zero. The CLI overlays `OPENRIG_FILES_ALLOWLIST` from the operator's shell env so doctor reflects the operator's intended allowlist even when the daemon's env is unchanged.
- `doctor [--workspace <path>] [--strict] [--json]` — run an 8-check workspace-readiness diagnostic against the daemon's resolved workspace (workspace root, missions folder, file allowlist, daemon alignment, daemon reload, optional slice docs, current `NOTES.md` or readable legacy notes, and SDLC convention sections). Read-only. Default exit-code: non-zero only on `fail`; `--strict` makes warn-or-fail non-zero.
Notes:
- v0 surface was intentionally narrow (`validate` only); v0.3.2 added `doctor` as the operator-facing readiness diagnostic.
@@ -1334,14 +1804,14 @@ the scope tree (missions, slices, sub-slices) per
- `ls [--mission <name>] [--state <state>] [--json]` — list slices in a mission (or across all missions). `--state` filter: `active | closed | shipped | all` (default `active`).
- `show <slice-path> [--mission <name>] [--json]` — inspect a single slice (frontmatter + README + children). `slice-path` is absolute, relative-to-substrate, or `NN-slug`; `--mission` hints the mission when path is just `NN-slug`.
-`create <mission> <slug> [--template <kind>] [--title <text>] [--json]` — create a new slice in a mission. `--template` default `placeholder`. **v0.4.0 (slice 33)**: scaffolds a canonical `PROGRESS.md` (the structure the OpenRig PROGRESS UI page parses) plus a convention-correct README with proper frontmatter, for EVERY template. **v0.4.4 (slice 23)**: EVERY template kind additionally emits the SDLC convention sections (`## Intent` / `## Mini-requirements` / `## Proof contract`, kind-specific body below them), a `proof/` dir, `PROOF.md`, and an `IMPLEMENTATION-PRD.md` skeleton carrying the elastic-middle note — the shapes the Living Notes UI projects. Conventions SSOT: `docs/reference/sdlc-conventions.md`.
- `create <mission> <slug> [--template <kind>] [--title <text>] [--intent <text>] [--depends-on <dot-id...>] [--json]` — create a mode-neutral slice scaffold: one intent-bearing `SPEC.md` with the three convention sections, `PROGRESS.md`, `PROOF.md`, and `proof/`. `depends_on` accepts same-mission sibling slice dot-IDs and is advisory build-order data. Every template kind emits the same file set; mode richness composes through the template seam.
- `progress <slice-path> [--mission <name>] [--status <state>] [--milestone <text>] [--owner <session>] [--note <text>] [--json]` — **v0.4.0 (slice 33)** new verb: append / set / update progress entries deterministically. Writes the canonical structure the OpenRig PROGRESS UI page reads. Replaces hand-editing `PROGRESS.md` with markdown.
- `stage <slice-path> <new-stage> [--mission <name>] [--successor <id>] [--json]` — **v0.4.0 (slice 35)** new verb: set the slice's `stage` frontmatter (wip / provisional / established / canonical / superseded / retired) deterministically. `superseded` REQUIRES `--successor <id>` (rejected otherwise + records the successor); `retired` warns "do not use"; invalid stages rejected with the valid set named.
- `verified <slice-path> --against "<source>" [--mission <name>] [--json]` — **v0.4.0 (slice 35)** new verb: stamp the slice's `verified` line with `verified: <today> against <source>`. `--against` is MANDATORY (bare timestamps rejected — the anti-stale keystone per `conventions/scope-and-versioning` §2). Overwrites the prior verified line.
@@ -1353,7 +1823,8 @@ Two subcommand groups: `slice` and `mission`.
- `ls [--json]` — list missions (top-level folders with `README.md`).
- `show <mission> [--json]` — inspect a single mission.
-`create <name> [--template <kind>] [--id <dot-id>] [--title <text>] [--json]` — create a new mission (mints a stable dot-ID into frontmatter). `--template` auto-selects when name matches`release-X.Y.Z`; `--id` overrides name-pattern inference. **v0.4.0 (slice 33)**: scaffolds a canonical `PROGRESS.md` plus a convention-correct README with proper frontmatter, per `conventions/scope-and-versioning/README.md`. Fixes the v0.3.x bug where newly-created missions were missing `PROGRESS.md` entirely.
- `create <name> [--template <kind>] [--id <dot-id>] [--title <text>] [--intent <text>] [--depends-on <dot-id...>] [--no-notes] [--json]` — create a mission with an intent-bearing `SPEC.md`, `NOTES.md`, `PROGRESS.md`, and `slices/`. `depends_on` accepts same-project sibling mission dot-IDs and is advisory build-order data. `--no-mission-notes` remains a deprecated alias for `--no-notes`; the old notes-template environment variable remains readable with an advisory.
- `graph <mission> [--json]` — show slice dependency nodes, the current ready/waiting sets, and advisories for malformed, cross-parent, or absent dependencies. Stale edges never block or crash the reader.
- `progress <mission> [--status <state>] [--milestone <text>] [--owner <session>] [--note <text>] [--json]` — **v0.4.0 (slice 33)** new verb: append / set / update progress entries on a mission's `PROGRESS.md` deterministically. UI-valid by construction.
- `stage <mission> <new-stage> [--successor <id>] [--json]` — **v0.4.0 (slice 35)** new verb: set the mission's `stage` frontmatter deterministically. Same enum + `--successor`-required-for-superseded rules as the slice variant.
- `verified <mission> --against "<source>" [--json]` — **v0.4.0 (slice 35)** new verb: stamp the mission's `verified` line. `--against` MANDATORY.
@@ -1365,9 +1836,9 @@ Convention compliance: `rig scope` together with slice 33 (`PROGRESS.md` + scaff
The conventions these verbs operate live in ONE shipped document: `docs/reference/sdlc-conventions.md` (copied into the assembled CLI package); the operating procedure is the packaged `mission-slice-sop` skill.
-`rig scope slice|mission approve <target> [--scope spec|delivery] [--actor <session>] [--on-behalf-of <human>] [--json]` —**v0.4.4 (slice 19 FR-9)** the two staged-approval locks, one daemon-side write path (frontmatter stamp + append-only audit row land together; no half-stamp). `--scope spec` = the **plan-lock** ("the PRD matches my intent; this artifact set gets built"); `--scope delivery` (default) = the **proof-lock** (terminal sign-off; fires the freeze). Approval is freeze/sign-off — never proven-green (proven-green requires recorded C1 verdicts).
- `rig scope slice|mission approve <target> [--scope spec|delivery] [--actor <session>] [--on-behalf-of <human>] [--json]` — the two staged-approval locks, one daemon-side write path. `--scope spec` plan-locks the node's `SPEC.md`; `--scope delivery` (default) is terminal sign-off. Approval is freeze/sign-off, never proven-green.
- `rig proof add <slice-path> --artifact-type <guard|qa|rev1-r1|rev1-r2|adjudication> --verdict <CLEAR|BLOCKING|CONCERNING|PASS|NOT-CLEAR> --candidate-sha <sha> --money-evidence "<line>" [--file <path>|--body <text>] [--evidences <refs>] [--media <refs>] [--self-check <text>] [--json]` — **v0.4.4 (slice 19 FR-8; `--media` via the corrective §3.4)** drop a proof artifact into `<slice>/proof/` with the machine-readable C1 header, validated at drop time (closed sets above). `--evidences` (item text or 1-based index) joins the drop to the slice's `## Proof contract` items — the pairing the Living Notes DELIVERED section renders; `--media` (proof/-relative refs, containment-checked, never absolute) names the curated media the drop stands behind, projected into the DELIVERED items' proof set. Contract/self-check outputs are advisories (exit 0), never gates.
-`rig scope audit <mission> [--json]` — extended **v0.4.4 (slice 19 FR-10 + slice 23)** with C1-header and IMPLEMENTATION-PRD backstops plus the convention-section advisories (missing `## Intent`, missing/malformed `## Mini-requirements`, missing/malformed `## Proof contract`, UI-slice-without-mockup — a mockup ref means a real image ref or plannedRef token, never bare prose). All advisory/fail-open: the exit code flips on HIGH findings only; the convention advisories are low/info by construction.
- `rig scope audit <mission> [--json]` — checks C1headers and the one-SPEC convention (frontmatter `intent:` or a readable legacy Intent section, Mini-requirements, Proof contract, and current NOTES). It never requires a second PRD. All convention findings are advisory/fail-open.
Notes:
- Surface source-verified against `packages/cli/src/commands/scope.ts` at `51554eee` (v0.4.0 post-slice-35); SDLC control-plane verbs source-verified against `scope.ts` / `proof.ts` / `scope-audit.ts` in OPR.0.4.4.23.
@@ -1381,6 +1852,7 @@ One top-level command first shipped in v0.4.0 (slice 10 — skill / knowledge li
Usage: `rig skill <subcommand>`
Subcommands:
- `loadout --runtime <claude-code|codex> [--cwd <path>] [--project-root <path>] [--topology <ids>] [--apply] [--json]` — inspect the deterministic managed loadout selected by `catalog.yaml` system selectors, topology/profile selectors, and `project.yaml install.skills`. Reports selector reasons, catalog Git revision/content digest, target, and current/missing/shadowed/conflicting status. `--apply` writes exact bytes plus an ownership manifest, is idempotent, and refuses to overwrite or remove locally modified/unowned content.
- `audit [--json] [--include-cache] [--severity <level>] [--rig <name>]` — read-only audit of the skill cascade. Detects provenance + freshness issues across the canonical → product mirror → hub cwd → installed plugin chain.
Audit categories surfaced:
@@ -1403,16 +1875,21 @@ Composes with the existing `scripts/mirror-skills.mjs` guardrails — the audit
## Operator Context-Mode Bindings (v0.3.2)
One top-level command first shipped in v0.3.2 (`rigPolicyCommand`,
defined in `packages/cli/src/commands/rig-policy.ts`; release-0.3.2
slice 09). Pairs with the daemon's typed-primitive store at
`packages/daemon/src/db/migrations/041_rig_policy.ts`. Operates the
names keep their shipped forms). Operates the operator context-mode
binding surface (sleep / desk / mobile / away / focus / debug) used by
mode-aware agent posture.
### `rig policy`
### `rig mode`
Usage: `rig policy <subcommand>`
Usage: `rig mode <subcommand>`
Subcommands:
- `set <mode> [--scope <scope>] [--qualifier <id>] [--<field> ...] [--evidence <citation>] [--confirm] [--bearer <token>] [--json]` — propose a binding. Without `--confirm` the CLI echoes the proposed binding and exits with `exit 2` so scripts cannot accidentally apply; `--confirm` is the explicit operator action. `<scope>` is one of `global_host | rig | workstream | qitem` (defaults to the per-mode recommendation). `--qualifier <id>` is required for `rig | workstream | qitem` scopes and rejected for `global_host`. Per-field tuning flags: `--autonomy-scope`, `--heartbeat-cadence`, `--inspection-depth`, `--update-detail`, `--escalation-threshold`, `--concurrency-limit`, `--permission-prompt-posture` (one of `normal | batch_for_human | do_not_prompt_unless_blocked`; `auto_accept` is FORBIDDEN by convention), `--expiry-or-stale-rule`. `--evidence` carries a free-text operator citation (message id, file pointer, chatroom topic, etc.).
@@ -1427,7 +1904,25 @@ Notes:
- Restate-and-confirm posture (HG-4): `set` is restate-only until `--confirm` is passed. This prevents accidental script application.
- `--qualifier` strict reject for `global_host` (HG-7 guard finding): operators who type `--scope global_host --qualifier <id>` get an error and the daemon is never contacted. The CLI does NOT silently drop the qualifier.
- Operator-edit mutations (`set --confirm`, `unset`) require an operator bearer token (`--bearer` or `OPENRIG_AUTH_BEARER_TOKEN` env).
- Surface source-verified against `packages/cli/src/commands/rig-policy.ts` at `53794fbe` (v0.3.3).
- Surface source-verified against `packages/cli/src/commands/rig-mode.ts` at `b13a8e4c7` (0.5.2 rename).
### `rig policy`
Usage: `rig policy <subcommand>` — the top-level PERMISSION-POLICY verb, introduced in 0.5.2 after
the context-mode verb moved to `rig mode` (RULING-rig-mode-rig-policy-naming). OpenRig bakes NO
allow/ask/deny permission policy — the harness-native permissions are the control surface. This verb
TEACHES and RECORDS into RigSpec (`permission_policy: builtin:<name> | none`); it never enforces at
runtime.
Subcommands:
- `list [--json]` — the built-in templates (`locked | standard | open | yolo`) plus the reserved
deliberate-`none` choice, each with its ref form.
- `show <name> [--json]` — one choice: its ref form and what recording it means.
- `current --spec <path> [--json]` — the `permission_policy` value recorded in a rig spec and how it
Unknown runtime resource types are still copied to the runtime extension directory for agent-visible context.
#### Writing a `codex_config_fragment`
**Begin the fragment with a table header.** A fragment must be a valid TOML
document on its own, and every key it sets must sit under a table it declares.
A fragment that puts keys ahead of its first table header is refused at
projection time, and nothing is written.
The reason is TOML's grammar rather than a policy choice. The managed block is
appended to the end of the user's `config.toml`, and TOML has no syntax for
returning to document root once a table has been opened. So if the user's file
ends inside any table, an appended root-level key does not land at root — it
silently becomes a member of *their* table. Refusal is deterministic and never
inspects the user's file: a fragment author cannot see user state, and a rule
that passed or failed depending on it would be impossible to reproduce.
```toml
# refused — `model` would bind into whatever table the user's file ends inside
model = "gpt-5"
[mcp_servers.exa]
url = "https://mcp.exa.ai/mcp"
```
```toml
# accepted — every key sits under a table this fragment declares
[mcp_servers.exa]
url = "https://mcp.exa.ai/mcp"
```
Where a fragment's table is one the user already declares, the user's table
wins: the managed table is dropped, their values are never merged, rewritten or
overwritten, and the rest of the fragment still applies.
### Resource Path Rules
- All resource paths must be safe relative paths (no `..` traversal, no absolute paths)
@@ -350,6 +383,12 @@ The startup block follows the same format as in the RigSpec (see `docs/reference
Agent-level startup is applied to all profiles. Profile-level startup is applied only when that profile is active. Both merge additively via the startup layering model.
An additional orientation challenge requires an explicit `startup_proof` action
with `value: authenticated` and `idempotent: true`. Omission adds no exercise;
a later applicable `value: none` overrides an earlier selection. See
[startup proof selection](rig-spec.md#startup-proof-selection) for precedence,
restore behavior, and the distinction between readiness and verified proof.
### Delivery Hint Quick Reference
| Hint | When Delivered | Mechanism |
@@ -375,7 +414,7 @@ Agent-level startup is applied to all profiles. Profile-level startup is applied
10. Resource IDs must be unique within their category.
12. Lifecycle `execution_mode` must be `interactive_resident`.
13. Lifecycle `compaction_strategy` must be `harness_native` or`pod_continuity`.
13. Lifecycle `compaction_strategy` must be one of `default-compaction`, `managed-compaction`, `handover`, `apprentice-handover` — or a deprecated alias (`harness_native`,`pod_continuity`), which validates with a deprecation advisory and normalizes to its canonical value (OPR.0.5.6.20).
14. Lifecycle `restore_policy` must be `resume_if_possible`, `relaunch_fresh`, or `checkpoint_only`.
15. Startup files and actions follow the same validation rules as in RigSpec.
@@ -413,6 +452,6 @@ The only required file is `agent.yaml`. Everything else is referenced by paths i
@@ -225,7 +225,7 @@ Commands sent to the agent's terminal after it's ready. Can install MCPs, run se
| `send_text` delivery after ready | **Supported** | Reliable. Requires harness to be ready. |
| Hook projection | **Experimental** | Files are copied but execution/integration varies by runtime. |
| Runtime resource projection | **Supported for recognized fragments** | `claude_settings_fragment`, `claude_mcp_fragment`, and `codex_config_fragment` are applied to provider config. Unknown types are copied to runtime extension directories. |
| Permission allowlisting | **Supported via harness config** | Core OpenRig boot adds the minimal Claude `Bash(rig:*)` baseline. Additional policy should be supplied through spec-selected runtime resource fragments or native Codex config profiles, not adapter hard-code. |
| Permission configuration | **Native settings plus managed launch flags** | OpenRig launches Claude with `acceptEdits` and Codex with `workspace-write` unless an explicit supported selection changes them. It does not add a global Claude `Bash(rig:*)` allowance. Use [the first-user permission guide](getting-started.md#opt-in-permissive-operation) for opt-in and custom choices. |
| MCP installation | **Experimental** | Claude Code: `/mcp` interactive command or `claude mcp add` from CLI. Can also be described in startup files for agent self-configuration. Reliability depends on runtime TUI state. |
| System dependency installation | **Not deterministic** | Describe in startup files; agent handles via shell commands. |
| Recurring tasks / wake timers | **Runtime-dependent** | Claude Code supports recurring tasks via the `/loop` command. Codex does not have a confirmed equivalent. Orchestrators should include `/loop` instructions in startup for Claude Code agents. |
@@ -246,7 +246,8 @@ This way, when deterministic support becomes fully reliable, the agent will boot
OpenRig performs best-effort deterministic runtime configuration for managed sessions. Core bootstrap stays minimal; user/custom policy belongs in spec-selected runtime resources. These writes are intentionally invasive and should be disclosed plainly:
- Claude global config: `~/.claude/settings.json`
Purpose: allow core `rig` commands without repeated Claude permission prompts.
OpenRig no longer writes a core `Bash(rig:*)` permission allowance here. Older
installations may retain one; existing user settings are not removed.
- Claude global state: `~/.claude.json`
Purpose: pre-trust managed workspaces and mark onboarding complete for fresh managed sessions.
- Claude project-local config: `.claude/settings.local.json`
@@ -260,6 +261,11 @@ Two important caveats:
- these writes are best-effort and should still be paired with startup guidance so the local agent can verify and repair them if needed
- already-running adopted sessions may need restart before they pick up newly written config
Permission mode and runtime resource projection are separate. A selected fragment
can affect native configuration, but OpenRig's launch flags can override those
values. Recording a config-surface `permission_policy` is not proof that its
rules were translated or applied. See [permission precedence and limits](getting-started.md#custom-settings-and-precedence).
### Runtime Differences
**Claude Code:**
@@ -274,7 +280,9 @@ Two important caveats:
- Reads from `AGENTS.md` and `.agents/` directory
- Recurring task support is limited — no confirmed equivalent of Claude Code's `/loop` command
- MCP configuration mechanism differs from Claude Code
- Approval mode controlled via launch flags (`-a`, `-s`, `--full-auto`)
- Approval policy and sandbox access are separate controls. OpenRig's default
`-s workspace-write` selects the sandbox; it does not force `-a`. A member's
`codex_config_profile` selects native `-p`, distinct from the AgentSpec `profile`.
- Can self-install dependencies from instructions but timer/recurring behavior is not reliably available
When authoring startup content, note which instructions are runtime-specific. For example, an orchestrator that needs a monitoring loop should include instructions like: "If running Claude Code, use `/loop 3m` to periodically check rig health. If running Codex, check rig health at the start of each task cycle instead."
your agents to run OpenRig commands without repeated permission prompts?”
**Yes — recommended** / **No — keep prompts**. An existing explicit choice is
reused; No or no answer leaves settings unchanged. Broader access is separate.
> Everything below reports **what is currently true**, never a guarantee that
> downstream work will succeed. "Daemon up" does not mean every agent is healthy;
> "kernel ready" does not mean every kernel agent is healthy; a workspace root
> being *live* does not mean it is the *right* one for your project.
## The ordered path
## Prepare and launch
1. **`rig setup`** - install and verify the local runtime (tmux, cmux, Claude
Code / Codex, runtime config). Run once per machine. `rig setup` does NOT boot
the kernel (see "Kernel framing" below).
2. **`rig up <rig-spec>`** - launch a rig. This **auto-starts the daemon** if it
is not already running (the four daemon-dependent verbs below do not auto-start
it - they tell you to run `rig up` or `rig daemon start`).
3. **`rig status`** - see what is currently true: daemon up/down + port, kernel
readiness (distinct from daemon health), and your effective workspace root
(default vs override).
4. **`rig workspace doctor`** - check that your workspace is ready; it reports
state + fix-hints. The effective workspace root defaults to
`~/.openrig/workspace/` unless you override `workspace.root` (env or config).
5. **`rig workflow instantiate <name>`** - start a workflow runtime instance by
its **discovered name** (e.g. `conveyor`, `basic-loop`) - no hidden file path
needed. `rig workflow specs`shows the seeded built-ins. Instantiating creates a
workflow instance and its first **entry qitem**.
6. **`rig scope ...`** - browse and manage the durable mission/slice artifacts the
workflow coordinates (see the scope <-> workflow bridge below).
Type `rig` in an ordinary terminal to open the startup and work TUI. It shows
the daemon address; **d** expands the selected instance path and diagnostics.
If the daemon is stopped, press Enter
to start that daemon, then choose the rigs and seats you want. Kernel is
recommended first; selecting its operator does not start every kernel seat.
The same view is available with **S** from ordinary TUI work.
**?** opens Help even while connection checks are pending. **w** skips startup;
**Esc** goes back, or leaves startup from its first page. These choices do not
start a daemon or a seat. **L** opens local reading before or after connecting:
choose configured Specs, project intent, projects, or missions and slices, then
select a directory or file. **r** reads the selected source again; **Esc** returns.
Local reading uses this machine's configured workspace paths and file allowlist,
including when the selected daemon address is remote. It shows disk provenance,
missing or denied sources, binary files and the 1 MiB text truncation boundary.
These disk snapshots may change after reading and do not supply live queue,
execution or topology state. Live views load after a confirmed connection and
deliberate entry; a stalled live read does not prevent Help or local reading.
When terminal transport is unavailable, **t** starts the empty terminal service
so recovery choices can be inspected. It launches no seats.
`rig setup` prints this ordered sequence as its next-steps output; `rig status`
points back here. This doc is the durable reference.
For a previously occupied seat, Enter attempts its previous conversation.
If history is unavailable, read thereason. **f** opens a separate fresh-start
decision for that named seat; **Esc** declines without launching it. A confirmed
fresh conversation receives the configured context and retains the old history,
but does not resume that history. Authentication or runtime failures require
repair of that prerequisite. **o** opens the existing native terminal here; detach
to return (tmux defaults to Ctrl-b, then d). Decide native trust/auth prompts
there. If a fresh start paused before context delivery, **c** finishes that
delivery to the same occupant. **r** reads actual state again; **d** expands details.
### Choose your providers
Ask: **“Which working account do you want this team to use: Claude Code, Codex,
or both?”** Reuse an explicit choice already made. Recommend the account the
user already has working; a second subscription is not a prerequisite.
| Choice | Starter name | Owner / checker runtime and model |
| --- | --- | --- |
| Two Codex agents (existing route) | `first-project` | Both `codex`, pinned `gpt-6-astra` |
| Two Claude agents | `first-project-claude` | Both `claude-code`, configured native default model |
| One of each | `first-project-mixed` | Claude owner (native default); Codex checker (`gpt-6-astra`) |
Claude uses the same unpinned model convention as the existing Claude kernel and
implementation-pair recipes: OpenRig does not pass a model override. Read the
selected harness's configured model and show it alongside the recipe and launch
command before proceeding. Confirm account access to any pin; if unavailable,
ask for a supported model choice rather than silently substituting a model or
provider. After launch, confirm the actual native model before assigning work.
Install OpenRig and check `tmux -V`. Check **only the selected providers**:
- Claude Code: `claude --version` and `claude auth status`. If sign-in is missing,
ask once: “Please run `claude auth login` in your launch environment.”
- Codex: `codex --version` and `codex login status`. If sign-in is missing,
ask once: “Please run `codex login` in your launch environment.”
Install a missing selected CLI using its provider's installation instructions.
The other provider's CLI/login and Herdr/cmux are optional. Do not copy credentials
or start repeated sign-in attempts. Recheck the selected login after the user
completes it. `rig setup --dry-run` previews the broader setup; applying
`rig setup` installs/checks **both** harnesses and cmux, so it is optional for
this selected-provider path, not a requirement to fix an unused provider.
### Kernel startup stays automatic
On a fresh instance, ordinary daemon startup chooses the kernel variant from
successful native auth probes: Claude alone, Codex alone, or both. Thus an absent
unused provider is fine. The project recipe does **not** constrain that probe:
if both accounts are authenticated, automatic kernel startup uses both, even
when the two project agents use just one provider. No manual kernel setup is
needed for this journey. The starter choice is not an instance-wide provider
restriction; a request to use only one provider for everything is separate.
Preserve any existing managed kernel and working rigs.
### Launch the two project seats
Set `starter` to the chosen name from the table; the existing Codex route remains:
```sh
cd <your-repository>
starter=first-project # or first-project-claude or first-project-mixed
rig specs preview "$starter" --kind rig
rig up "$starter" --cwd . --plan
rig up "$starter" --cwd .
rig status
rig ps --nodes --rig "$starter"
```
Preview the selected seats, models and resources. Plan checks resolution and
preflight for the working directory. Launch starts the daemon if needed, with
the kernel behavior above. Read readiness for the project seats, not only daemon
health. Resolve a named authentication, trust or permission prompt before giving
that seat work. No new team is needed when returning to an existing project.
When a seat pauses, open its existing terminal with **o** in the startup view.
Read the proposed command, working directory and target instance. For an intended
local `rig` call, choose the native prompt's one-time approval if that is the scope
you want; a saved command-prefix allowance also affects future matching calls.
Decline an unexpected operation and tell the same agent what to do instead.
Approval controls whether an action may run; the sandbox controls its filesystem
and network access. Turning approvals off does not grant network access.
After answering, watch for the command's result and the agent continuing. Read
the corresponding queue row and transition from your ordinary terminal. If an
operation timed out, read its result before asking for another attempt: it may
already have taken effect. A delivered message or disappearing prompt alone is
not progress. If startup is still waiting for context delivery, use **c** for the
same occupant, then **r** to refresh. Do not start another seat to clear a prompt.
These starters are deliberately small starting points, not universal teams.
For a different installed runtime or team shape, inspect `rig specs ls --kind
rig` and `rig specs preview <name>` before selecting it. A seven-seat showcase
is optional and consumes more concurrent capacity.
## Give the owner an outcome
For example, in a project that imports CSV files:
```sh
rig send "dev-owner@$starter" 'Improve the CSV import error when a required column is missing: name the column and leave the existing data unchanged. Add a regression check, ask dev-check for an independent check of the exact candidate, and record the result and how I can try it. Keep the change local; do not publish.'
```
Replace the example with a real problem in your repository. Include what the
user should observe, a boundary and how success can be checked. The owner
creates and claims a durable task, implements it, and routes the selected
independent check. You should not have to relay the review between terminals.
`rig send` is the initial conversation; the queue and repository artifacts
retain the work. An unbound shell does not need to impersonate a queue owner.
Use the chosen rig name in its seat addresses (for example,
`dev-owner@first-project-claude`). From an actual project seat, follow the work with
`rig queue list --limit 1000`: its default scope is the caller's current rig.
`queue list` has no `--rig` option. From an observer shell or another rig, use
`rig queue list --destination "dev-owner@$starter" --limit 1000` and the same
command for `"dev-check@$starter"`, after verifying those live addresses.
These show each destination's obligations, not a whole-rig view. An unbound shell
must not pretend to be a seat to change scope; use `--all-rigs` only when that
broader view is intended. Then read `rig queue show <id> --full` and
`rig queue transitions <id>`. A delivered message is not a reviewed result.
Read the artifact, exercise its behavior, and check the candidate reviewed.
## Share the dashboard and return to it
```sh
rig tui --shared
```
A fresh kernel runs the ordinary TUI in its existing `operator-human` terminal.
This command attaches another client to that terminal. **Ctrl-b, then d**
detaches without quitting the TUI; return with the same command and the view
stays where it was. Another authorized agent can capture or operate that same
pane. It should tell you before changing your view. The terminal is not a
human inbox and does not prove anyone is watching it.
Plain `rig tui` remains an independent local view. If an older kernel or a TUI
you quit shows a shell, run `rig tui` in that shell once. `--shared` does not
start or replace a terminal, so a missing binding is reported with recovery
guidance rather than creating a second kernel.
Herdr users follow the same launch and task path. To place the managed team in
Herdr, use `rig terminal open "$starter" --provider herdr`; for the shared
dashboard, use `rig terminal open kernel --provider herdr`. The equivalent
cmux provider is also available. Read the opened/absent/degraded result: a
partial terminal view is not a healthy team. Repeated terminal-open calls can
create another provider workspace; return to the one already open when you
want to preserve it. This is terminal integration, not native plugin enrollment.
## Continue real project work
Return to the same owner with the next outcome, citing the earlier result.
The seat address and durable queue survive closing your viewing terminal.
Keep intent, acceptance and evidence in the repository's existing project,
mission and slice artifacts; the starter reads those before inventing a path.
If the project has no work tree, start with `rig workspace doctor` and
`rig scope mission create --help`, then `rig scope slice create --help`. Set
the actual intended outcome before creating work. `rig scope` retains what is
being built. When repeated coordination warrants a workflow, discover with
`rig workflow specs`, inspect its owners and inputs, and instantiate the
selected name with `rig workflow instantiate --help`. A workflow is not needed
merely to make the first local change.
For a continuing team, [OpenRig Software Factory](../../packages/daemon/specs/agents/shared/skills/core/openrig-software-factory/SKILL.md)
offers manual/team work, queue-supported orchestration without Workflow, and an
optional explicit Workflow path, with wake defaults, token costs and permission
choices visible. Give its short request to your existing agent. After
installation, discover the compatible bundled recipe with `rig context show
skills/core/openrig-software-factory --json`; retain a missing/version-mismatch
result rather than silently using newer instructions. Its growth section keeps
three choices clear: stay with the pair, add one or two seats to the running rig
with `rig grow` and no YAML, or optionally author a custom rig. It covers
new-seat context/work ownership, concurrency costs and saving the expanded spec.
This guide remains the short first-use path.
## Incomplete setup and restart
| Observation | Next action |
| --- | --- |
| Tool missing or login fails | Use the specific setup/auth hint; recheck that executable in the launch shell. Do not send work to an unready seat. |
| Daemon is healthy, kernel is still starting | Read `rig status` and `rig ps --nodes --rig kernel`; kernel readiness is separate. |
| Shared terminal is absent | Inspect the existing kernel binding and recovery state; use standalone `rig tui` while resolving it. |
| Viewing terminal was closed | Reattach with `rig tui --shared`; do not relaunch the team. |
| Daemon restarted but tmux survived | Re-read `rig status` and the existing queue; a daemon restart is not a fresh project. |
| Host reboot lost tmux sessions | Open `rig`, start the daemon if needed, and select the existing rig and seats. Resume is the default; a fresh conversation needs a separate decision. |
| Launch reports no usable snapshot | Inspect the existing rig and retained project files, then follow the same-seat recovery below. |
| Work is waiting on a prompt or decision | Read the row, transition and named prompt; preserve the obligation until the missing decision arrives. |
If a snapshot is unavailable, the startup view checks the selected seat's
retained startup source and authoritative occupant relation. It reports a
missing or ambiguous source instead of selecting an arbitrary historical row.
Repair the named source, retry, or leave the seat stopped. Check the retained
queue, project notes and observed result before continuing work.
`rig setup` prints the short form of this path; `rig status` points back here.
## Kernel framing (what `rig setup` does and does not do)
The kernel rig **auto-boots on daemon-start** - it is daemon-start behavior, NOT
a `rig setup` step and NOT something you boot by hand. So:
Explicit CLI daemonstartup retains its automatic kernel behavior. The TUI
starts the daemon with kernel auto-boot disabled so the user can select seats:
- `rig setup` installs/verifies the runtime; it does not start the daemon or the
kernel.
- Starting the daemon (`rig daemon start`, or implicitly via `rig up`) is what
boots the kernel rig in the background.
- Starting it from bare `rig` prepares no agents automatically. The TUI offers
kernel setup and individual seat selection after connecting.
- **Kernel readiness is a distinct signal from daemon health.** The daemon's HTTP
health binds early; the kernel can still be booting or a kernel agent can be
unhealthy while the daemon is up. `rig status` surfaces kernel readiness
@@ -60,10 +274,233 @@ Two related primitives, often confused by new operators:
an **entry qitem** that routes the first step to an owner. This is the live
coordination of *who does the next step*.
How they relate: a scope slice is the durable description of a unit of work; a
workflow instance + its qitems are the live machinery that moves that work
through owners (hot-potato handoffs). They are not auto-bridged - there is no
auto-instantiate-from-scope - you instantiate a workflow by name when you want to
run one, and you reference your scope artifacts as the work it coordinates. A
typical loop: author/track the unit in `rig scope`, then
`rig workflow instantiate <name>` to start the runtime that drives it.
Scope files retain what the work is; workflow instances and their queue packets
retain who acts next. Creating or editing a mission does not start work. You can
instantiate a named workflow with `rig workflow instantiate <name>`, or explicitly
inspect an authored lifecycle with `rig workflow compile` and create its runtime
with `rig workflow instantiate-lifecycle`. Compilation alone does not start work.
and missing selections' files remain unavailable. No links are followed
recursively and no successor mission is guessed. The reader accepts at most 32
selected addresses, each source file at most 64 KiB, with 128 KiB of selected
content in total; exceeded limits are visible as unavailable references. A hash
for a section covers exactly the returned section bytes. Bare library refs and
remote URLs are not fetched by this local reader.
Authority and current usefulness are separate questions. An incomplete global
outcome census cannot justify retaining a particular restriction whose premise
was disproved. Read current corrections before interpreting historical
dispositions, preserve unrelated valid boundaries such as publication authority,
and distinguish automatic wake/receipt bookkeeping from useful owner action.
Normal interactive planning is not itself pathology. State the relevance and
cost of the interruption in the assessment; do not turn the signal into a
recurring self-audit or require a universal outside reviewer.
Checkpoints are audited under `health/checkpoints/history/`; replaying identical
bytes writes nothing. Later censuses advance observation time. High-to-high
observations retain episode identity; a clearing checkpoint and later recurrence
produce a cleared episode and a new ID. Reads never update checkpoint state.
Stale, unavailable, contradictory, clipped, or missing evidence cannot admit a
diagnosis. No findings is not a healthy assertion. The current source bounds are
200 lineages, 10,000 transitions and 1,000 product outcomes per checkpoint, and
1 MiB per input. Unsupported or invalid sources fail visibly.
## Investigate and record a disposition
Read the exact evidence and the current authority. Trace where the pattern
began, test whether your own actions amplified it, and distinguish another seat
or stale guidance. A second opinion is optional. The packet is a starting point,
not a closed evidence set.
```json
{
"verdict": "insufficient evidence",
"causalStart": null,
"steering": "Inspect the missing outcome evidence; apply separately established corrections within current authority.",
"uncertainty": "The cited artifact does not yet establish the denominator.",
"evidenceRefs": ["<inspected-evidence-path>"]
}
```
```sh
rig health diagnosis record <qitem-id> --file disposition.json
rig health diagnosis show <qitem-id>
```
Verdicts are `false positive`, `early real condition`, `established pathology`,
`insufficient evidence`, or `resolved`. The disposition is retained on the
diagnostic qitem's transitions and visible in CLI output and the queue. Recording
it does not close or alter the underlying product work. Changed dispositions
retain earlier testimony; exact replay is a no-op. A detector clearing is
recorded separately from an agent declaring the problem resolved.
An optional `correction` keeps the causal judgment, proposed or taken action,
and later behavioral effect distinct. It does not require a complete `progress`
census. For example, a retained-case assessment can record:
```json
{
"applicability": "The retired emergency restriction no longer applies; publication still needs its separate decision.",
"causalJudgment": "The retained trace attributes persistence of the restriction to a disproved premise.",
"action": {"state": "taken", "summary": "The authorized restriction was retired in the retained case.", "evidenceRefs": ["evidence/correction-action.md"]},
"effect": {"state": "unobserved", "summary": "No later natural opportunity has been observed.", "evidenceRefs": []}
}
```
Put this object beside `verdict`, `causalStart`, `steering`, `uncertainty` and
`evidenceRefs` in the existing disposition. Action states are `proposed` or
`taken`; effect states are `unobserved` or `observed`. Every referenced artifact
must be available locally; `taken` and `observed` each require evidence. These
checks establish availability, not causal truth. The receipt retains evidence
hashes and `assessment` identifies the actual queue actor, time and transition.
`behavioralEffect` is the latest owner's reported effect, defaulting to
`unobserved` for legacy dispositions. It is not an independent certification.
A later owner submission preserves previous testimony. Record an observed
effect only from a real later opportunity, with its next decision, useful work,
recurrence and interruption burden; a scripted replay proves mechanics only.
Closing a row, changing a prompt or clearing a numerical signal never supplies
that evidence automatically. No opportunity means unobserved, with the release
claim left to its decision owner. This guidance neither re-enables diagnosis nor
assigns corrective work.
## Human delivery
An assigned agent may explicitly request human escalation:
```sh
rig health diagnosis notify <qitem-id>
```
It requires a registered `human.address` and an admitted `human.conditions`
entry (`critical`, `established pathology`, or `confirmed ceremony`). The connector must be enabled
and pass live readiness checks. The current connector implementation verifies
Slack scopes and channel membership; the diagnosis service itself uses a
transport-neutral readiness port. The existing gateway owns delivery policy
and posting. One human request is retained per episode, and its actual delivery
outcome comes from queue receipts. `pending` is never presented as `posted`.
Inspect the returned qitem's transitions for the connector receipt. No periodic
health check performs remediation. With `human.conditions=["confirmed ceremony"]`,
the enabled diagnosis loop automatically requests one notification for an active,
confirmed passive ceremony episode. Provisional, indeterminate, cleared, and stale
episodes never notify. The existing readiness/gateway path owns the effect; an
unready connector leaves a visible, deduplicated readiness receipt. Later checks
may retry readiness but never create a second request for an existing episode.
Read-only list/explain/preview commands never send.
## Read-only consumers and calibration
A consumer such as a later Herder plugin reads `GET /api/health` and
`GET /api/health/:findingId`, or the identical `rig health --json` and
`rig health explain <finding-id> --json` records. The record schema is
`openrig.health/v0alpha1`; list metadata is `openrig.health-list/v0alpha1`.
Preserve ID, status, policy version, time window, freshness, evidence and
threshold together. There is no health score or implicit remediation authority.
The default list excludes cleared records; explicit cleared queries and exact
ID reads retain them while the source can still project them. A recurrence after
clear receives a new episode ID. This is an on-demand projection, not a history
store; a detail 404 does not prove resolution.
Lists default to 100 and cap at 200, with `total` and `truncated` explicit.
Ceremony findings take precedence before the cap so context pressure cannot
hide the primary signal. Narrow a truncated query; do not certify the unseen
remainder. Empty is not healthy, unavailable is not empty, and an indeterminate
finding cannot authorize a diagnostic occurrence except the explicit, fresh
`needs-diagnosis` passive candidate described above. An existing occurrence may
receive one status-observation receipt when its source becomes indeterminate;
that does not create another obligation or wake.
Live sources supply context pressure, passive ceremony candidates and attributed
assessments, plus optional ceremony checkpoints.
Behavioral and epistemic categories intentionally have no detector. Review
carousel, redundant wakes, stale directives and scope admission have typed
evaluators and replay controls, but the live source does not infer their missing
candidate-change, rescue, directive-conflict or admission-authority facts.
Untagged work and undeclared relationships remain outside passive ceremony
coverage; normal tagged work no longer requires a special outcome census. A small lineage below the configured threshold may still deserve an
agent's inspection; the threshold is a conservative admission rule, not a
definition of good process.
`packages/daemon/scripts/probe-health-calibration.mjs` accepts a sealed replay
export and output directory after daemon/CLI/TUI builds. It drives the compiled
checkpoint/list/explain/diagnosis commands, renders the TUI, retains exact
records and screens, compares database and filesystem effects around reads,
and measures query cost against declared budgets. Every write uses a disposable
home and database. Historical expected states must be grounded independently
of the detector formula; incomplete outcome evidence is an indeterminate case,
not a denominator of one. A calibration report describes its selected corpus
and remaining blind spots, not a fleet-wide false-positive rate.
Resolve `topology.root` from configuration; never hardcode the root. The owning
seat is the sole direct author. It writes as the durable position, not as a
particular occupant or predecessor.
The filename describes the situation or trigger and never contains a stage,
date, occupant generation, or version. Its address is therefore stable:
```text
seat:lore/<stable-slug>.md
seat:lore/<stable-slug>.md#situation
```
A stage change edits frontmatter and `date`; it never renames the file. Existing
citations must continue to resolve after the change.
## Required metadata
Every lore item carries these fields from birth:
| field | meaning |
|---|---|
| `taxonomy` | Exactly `lore`. This is the machine-readable privacy class. |
| `stage` | Current epistemic maturity. Legal values come only from [`knowledge-maturity.md`, “Two encodings, one ladder”](knowledge-maturity.md#two-encodings-one-ladder); do not copy or extend that vocabulary here. |
| `method` | How the owning position could know the claim: the observation, comparison, incident, or source method. |
| `date` | UTC date-time of the current stage decision or substantive revision. Update it when `stage` changes. |
| `position` | Canonical owning seat address, `<seat>@<rig>`. It names the position, never an occupant. |
`taxonomy: lore` is required both in each item's frontmatter and on any
manifest-bearing lore pack that serves it. A misspelling is not equivalent and
must fail any machine pin that extracts the class.
## Item template
Start a situation-shaped item with this template. Replace every placeholder;
`wip` is the starting label, while the authoritative vocabulary remains the
citation above.
```markdown
---
taxonomy: lore
stage: wip
method: "<how this position could know>"
date: "YYYY-MM-DDTHH:MM:SSZ"
position: "<seat>@<rig>"
---
# <Short situation or trigger>
## Situation
**Position:** `<seat>@<rig>`
**Moment.** <What is happening when this knowledge becomes useful?>
**Reflex.** <What tempting response is likely to be wrong?>
**Instead.** <What should the reader do?>
**Because.** <What evidence or failure made the difference?>
```
Facts may use `## Fact` instead when a fact genuinely is the transferable unit.
Any H2 intended as a section-level attachment repeats the canonical `Position:`
line so attribution remains visible when the resolver returns only that section.
## Addressing and composition
Lore uses the existing `seat:` grammar and an explicit rig-and-seat read grant.
The address says which file or section to read; the grant says whose seat tree
the address is relative to. No grant means no read.
For an install, compose the whole item address (`seat:lore/<slug>.md`) so its
frontmatter, taxonomy, stage, method, date, and position travel with the content.
A composed piece must visibly retain:
- the `seat` source label supplied by the resolver;
- the owning `position` from the item's bytes; and
- the `taxonomy: lore` class from both item and lore-pack metadata.
Serving one seat's lore into another seat's install is allowed only when the
caller deliberately grants the owning seat tree. The resulting piece remains
attributed to that owner; the receiving seat does not inherit its identity.
An attachment point is only a pointer paired with the owning position:
```yaml
lore:
address: "seat:lore/<stable-slug>.md#situation"
position: "<seat>@<rig>"
```
An attachment never embeds or copies the lore bytes. The reader resolves it
with the named position's explicit grant when the deeper context is relevant.
## Stage changes
To change maturity:
1. Edit only `stage` and `date` unless the claim itself also changed.
2. Re-resolve the same `seat:lore/<stable-slug>.md` address.
3. Confirm the address is unchanged and the new stage and date are visible.
If a stage change would break the address, stop: that storage shape violates
this convention. `superseded` and `retired` follow the semantics at the cited
maturity home; no lore-specific alternatives are invented here.
## The outward gate
A lore artifact never crosses from rig-local position knowledge into a shared
or shippable audience by copy, export, serving, or packaging. Grant-gated
read-side composition inside the rig is not graduation and keeps the source
address and position attached. Its content may inform a new outward artifact
only through authored re-homing:
1. The owning seat authors the destination artifact, or records an explicit
delegation to its author.
2. The destination is written for its new audience in its own words; the lore
file itself remains byte-unchanged.
3. The destination records the graduation event:
```yaml
graduation:
source_address: "seat:lore/<stable-slug>.md"
source_stage: "<stage at graduation>"
date: "YYYY-MM-DDTHH:MM:SSZ"
owner: "<seat>@<rig>"
delegation: "none | <who delegated to whom>"
warrant: "<why the content earned the new audience>"
```
4. If the destination is shippable, it must pass the public-substance admission
gate. Renaming, rewrapping, or declaring copied lore as another pack class is
laundering, not graduation.
The source address, source stage, owner/delegation, date, and warrant make the
move auditable from the destination. The source lore item is never edited merely
to announce that another artifact graduated from it.
## Privacy boundary
Lore is rig-local position knowledge. Shippable projection and bundle paths must
refuse `taxonomy: lore` structurally, independently of token deny-lists or content
scanners. Existing containment protections for seat files remain in force.
This convention deliberately does not create:
- automatic transcript or memory mining;
- a lore librarian, central curation queue, or review ceremony;
- cross-rig lore sharing;
- a new address grammar, resolver kind, or public export form.
Growth is owner-authored and organic. A seat writes an item when lived work makes
the situation worth preserving; consumers attach or compose it by address when
Last validated against code: 2026-09-29 (install paths and `--target` only; other sections last checked 2026-04-11)
Source of truth: `packages/daemon/src/domain/bundle-types.ts`, `packages/daemon/src/domain/bundle-archive.ts`, `packages/daemon/src/domain/pod-bundle-assembler.ts`
A `.rigbundle` is a self-contained distributable archive that packages a rig spec, all referenced agent specs, their resources (skills, guidance, startup files), culture file, documentation, and an integrity manifest into a single file. The recipient can install and launch the rig without needing the original source tree.
@@ -180,6 +180,13 @@ If any check fails, extraction is aborted and an error is thrown.
## CLI Surface
### Paths
`rig bundle create`, `inspect` and `install` resolve every path you give them (`<spec-path>`, `-o`, `--rig-root`,
`<bundle-path>`, `--target`) to an absolute path against **your** current directory before sending the request. The
daemon then reads and writes those paths on **its own host**: nothing is uploaded, so the files must already exist
where the daemon runs.
### Create a bundle
```bash
@@ -211,7 +218,7 @@ The create command:
rig bundle inspect <bundle-path> [--json]
```
Shows the manifest, digest validity, and integrity verification result. Inspect extracts the archive into a temporary directory for safe validation, then cleans that directory up. It does not install the bundle.
Shows the manifest, digest validity, and integrity verification result. Inspect extracts the archive into a temporary directory for safe validation, then cleans that directory up. It does not install or launch anything.
| `<bundle-path>` | yes | — | Path to the `.rigbundle` file. |
| `--plan` | no | `false` | Preview the bootstrap plan without installing or launching. |
| `--plan` | no | `false` | Preview without installing or launching. Not side-effect free: it runs the preflight, which executes the members' runtime `--version` probes, and it records a bootstrap run. It writes nothing to the target. |
| `--yes` | no | `false` | Auto-approve trusted actions during apply mode. |
| `--target <root>` | yes in apply mode | — | Target root directory for package installation. Required unless `--plan` is used. |
| `--target <root>` | yes in apply mode | — | Directory the bundle is installed into and launched from. Required unless `--plan` is used. |
Extracts the bundle, validates integrity, and bootstraps the rig. In apply mode, the daemon requires `targetRoot`, so `rig bundle install` must be given `--target <root>` unless you are running with `--plan`.
Install **launches the rig**; it does not just unpack it. It extracts the bundle to a temporary directory, validates integrity, and bootstraps the rig. In apply mode, the daemon requires `targetRoot`, so `rig bundle install` must be given `--target <root>` unless you are running with `--plan`.
For a pod-aware (schema version 2) bundle, apply copies the extracted contents (`bundle.yaml`, `rig.yaml`, `agents/`, culture and docs files) into the target and launches from there, then removes the temporary extraction. So:
- the target becomes the rig root: `agent_ref` paths resolve inside it, and a member with `cwd: "."` (or no `cwd`) starts in the target;
- an absolute member `cwd` stays as authored, and `rig up --cwd <dir>` still overrides every member's cwd;
- if the target already has a file with **different** content at any bundle path (for example its own `rig.yaml`), install refuses with `target_conflict` and writes nothing. Identical files are accepted, so reinstalling the same bundle into the same target works. Use an empty or dedicated directory as the target.
Legacy (schema version 1) bundles keep their old behavior: `--target` is only where packages are installed.
`rig up` auto-detects `.rigbundle` files and routes them through the bundle bootstrap path.
- `--target <root>`controls where packaged files are installed
- if `--target` is omitted for a `.rigbundle`, the CLI defaults the install target to the current working directory
- `--target <root>`is the install target described above (for a schema-version-2 bundle, the directory the bundle is copied into and launched from)
- if `--target` is omitted for a `.rigbundle`, the CLI defaults the install target to the current working directory, so the bundle's files are written there
- `rig up` resolves a relative `--target` against your current directory before sending it, like `rig bundle install`; with `--host`, `--target` is sent as given and must be a path that exists on that host
- `--cwd <dir>` does **not** change the install target; it only overrides the launched members' working directory for that run
---
@@ -364,4 +380,4 @@ Equivalent explicit form:
rig up /path/to/my-team.rigbundle --target ~/projects/my-project
```
The bundle is extracted to a temporary directory, integrity is verified, packaged files are installed into the target root, and the rig is bootstrapped with all agents and resources from the bundle. If you also want agents to launch with a different working directory for that run, pass `--cwd <dir>` separately.
The bundle is extracted to a temporary directory and its integrity is verified. A schema-version-2 bundle is then copied into the target root (refused if the target holds different files at the same paths) and the rig is launched from there, with all agents and resources from the bundle. If you also want agents to launch with a different working directory for that run, pass `--cwd <dir>` separately.
| `name` | string | yes | — | Rig name. Used in session naming (`{pod}-{member}@{name}`), snapshot identification, and spec library lookup. |
| `summary` | string | no | — | Human-readable description. Shown in spec library, review surfaces, and `rig specs show`. |
| `culture_file` | string | no | — | Relative path to a rig-wide culture/constitution file. Must be a safe relative path (no `..`, no absolute). |
| `permission_policy` | string | no | — | Permission policy attached to the rig. Either a built-in (`builtin:locked`, `builtin:standard`, `builtin:open`, `builtin:operator`) or a safe relative path to a custom policy file (resolved from this spec's directory; no `..`, no absolute). Absent leaves the default floor. A member may set its own `permission_policy`, which takes precedence over the rig-level one. See "Attaching a permission policy" below. |
| `permission_policy` | string | no | — | Permission policy attached to the rig. Either a built-in (`builtin:locked`, `builtin:standard`, `builtin:open`, `builtin:yolo`) or a safe relative path to a custom policy file (resolved from this spec's directory; no `..`, no absolute). Absent leaves the default floor. A member may set its own `permission_policy`, which takes precedence over the rig-level one. See "Attaching a permission policy" below. |
| `managed_blocks` | map | no | `CLAUDE.md` | File that receives OpenRig's managed instruction blocks for Claude Code members. Only the `claude-code` key is accepted, with `CLAUDE.md` or `CLAUDE.local.md`. Codex members always use `AGENTS.md`. See "Choosing the Claude instruction file" below. |
| `docs` | Doc[] | no | — | Documentation files that should travel with the rig. Included in rig bundles. Each entry has a `path` field (safe relative path). The engine does not consume these — they are for humans and agents setting up the environment before launch. |
| `startup` | StartupBlock | no | — | Rig-level startup files and actions. Applied to all members via the startup layering model. |
| `services` | ServicesBlock | no | — | Optional managed services (Docker Compose). When present, services boot before any agent launches. |
OpenRig writes its instructions for Claude Code members into managed blocks in
the member's working directory. By default the file is `CLAUDE.md`. If your
repository tracks `CLAUDE.md`, write the blocks to `CLAUDE.local.md` instead:
```yaml
managed_blocks:
claude-code: CLAUDE.local.md
```
Claude Code loads `CLAUDE.local.md` from the working directory as well. By
convention the file is kept out of git, for example with a `.gitignore` entry.
- Accepted values are `CLAUDE.md` and `CLAUDE.local.md`. Any other value or
runtime key is rejected before a member launches.
- The setting applies to launch, restore, relaunch, handover, adding members,
and export. `rig down` removes OpenRig's blocks from the selected file only.
- OpenRig never edits, moves or deletes blocks in the other file.
A rig that already wrote blocks into `CLAUDE.md` keeps them there after you
switch. Until you remove them, `CLAUDE.md` stays modified and Claude Code loads
both copies. Delete each `<!-- BEGIN OpenRig MANAGED BLOCK: … -->` …
`<!-- END OpenRig MANAGED BLOCK: … -->` section by hand and keep the rest of the
file. If `CLAUDE.md` has no other uncommitted edits you need to keep, you can
instead run `git restore CLAUDE.md`; that command discards every unstaged change
to the file, not only OpenRig's blocks. Running `rig down` on a rig that still uses
the default is not a substitute: it strips every OpenRig block from that
directory's `CLAUDE.md`, including blocks written by other rigs.
---
## Pod
@@ -229,6 +271,7 @@ into your project and edit it to taste.
| `id` | string | yes | — | Member identifier. Must not contain dots. Must be unique within the pod. |
| `agent_ref` | string | yes | — | Reference to an AgentSpec. Must start with `local:` (relative) or `path:` (absolute). Exception: `builtin:terminal` for infrastructure nodes. |
| `profile` | string | yes | — | Profile name from the referenced AgentSpec. Use `default` for the default profile. Exception: `none` for terminal nodes. |
| `codex_config_profile` | string | no | — | Codex-only native profile passed as `-p <name>`; letters, numbers, `_`, `.`, `-`. Separate from the AgentSpec `profile`. With the normal launch mode, this replaces OpenRig's explicit workspace-write sandbox flag. A full-bypass policy instead emits danger-full-access and omits this profile argument. |
| `cwd` | string | yes | — | Working directory for the agent. Resolved relative to the rig root (the directory containing the rig spec). Use `"."` for the rig root itself. Can be overridden at launch time with `rig up --cwd`. |
| `label` | string | no | — | Human-readable member name. Shown in UI when present. |
@@ -343,12 +386,51 @@ Startup blocks can appear at three levels: rig, pod, and member. They are merged
| Field | Type | Required | Default | Description |
| `type` | string | yes | — | Action type. One of: `slash_command`, `send_text`. Note: `shell` is explicitly NOT supported in v1. |
| `value` | string | yes | — | The command or text to send. |
| `phase` | string | no | `after_files` | When to execute. One of: `after_files` (after startup files are delivered), `after_ready` (after harness readiness check passes). |
| `type` | string | yes | — | Action type. One of: `slash_command`, `send_text`, `startup_proof`. Note: `shell` is explicitly NOT supported in v1. |
| `value` | string | yes | — | Command/text to send, or `authenticated` / `none` for `startup_proof`. |
| `phase` | string | no | `after_files` | When to execute text/commands. One of: `after_files` (after startup files are delivered), `after_ready` (after harness readiness check passes). Proof selection is resolved before projection regardless of phase. |
| `idempotent` | boolean | yes | — | Whether this action is safe to replay on restore. **Required field.** Non-idempotent actions must NOT include `restore` in `applies_on`. |
| `applies_on` | string[] | no | `[fresh_start, restore]` | When this action runs. Subset of: `fresh_start`, `restore`. |
### Startup proof selection
Startup adds no orientation exercise by default. To select the authenticated
startup challenge, declare an action in an agent, profile, rig, pod, member, or
operator startup block:
```yaml
startup:
actions:
- type: startup_proof
value: authenticated
idempotent: true
```
Use `value: none` in a later layer to select lean startup explicitly. The last
applicable declaration wins in agent → profile → rig → pod → member → operator
order. Culture contributes files, not a proof selection. With no applicable
declaration, the result is `none`; the number of startup files never selects
proof. Invalid values and non-idempotent proof declarations fail validation,
including declarations overridden later. These actions declare policy and are
never typed into a terminal.
An authenticated selection challenges only a fresh or fresh-fallback managed
agent launch. Resumed, forked, rebuilt, and adopted sessions receive no new
challenge; terminal nodes never receive one. `applies_on` follows the requested
startup context, so a fresh fallback during restore uses `restore` selections.
Keep the default `[fresh_start, restore]` to cover both fresh launch paths.
Identity delivery, projection, readiness, and ordinary startup actions still
run. `startup_status: ready` means startup completed, while `oriented: missing`
means a selected proof awaits authenticated submission. Omission/`none` yields
`oriented: n-a` on a new fresh launch and retires an older challenge without
deleting its audit history. Retirement follows successful harness launch,
before readiness checks, so attention, timeout, or a readiness exception cannot
retain the preceding proof. A replacement that fails to launch does not retire
the current proof; resume/adoption also preserves existing proof history.
The effective selection is recorded on `node.startup_pending`; actions are
persisted in startup context for restore and fresh relaunch.
---
## Services Block
@@ -477,7 +559,7 @@ These rules are enforced by the validator. A spec that violates any of these wil
19. Each wait target must define exactly one of: `service`, `url`, `tcp`.
20. `condition` is only valid on `service` targets and must be `healthy`.
21. Startup file `delivery_hint` must be one of: `auto`, `guidance_merge`, `skill_install`, `send_text`.
22. Startup action `type` must be one of: `slash_command`, `send_text`. (`shell` is explicitly rejected.)
22. Startup action `type` must be one of: `slash_command`, `send_text`, `startup_proof`. (`shell` is explicitly rejected.) Proof selection requires `value: authenticated` or `none` and `idempotent: true`.
23. Startup action `phase` must be one of: `after_files`, `after_ready`.
24. Startup action `idempotent` is a required boolean.
25. Non-idempotent actions must not include `restore` in `applies_on`.
@@ -491,8 +573,8 @@ These are the built-in specs shipped with OpenRig. Read them as worked examples.
rig mode set delegated --scope mission --qualifier my-project/release-1 --evidence "Operator delegated this outcome"
# The proposal above exits 2 without writing. Apply the deliberate choice:
rig mode set delegated --scope mission --qualifier my-project/release-1 --evidence "Operator delegated this outcome" --confirm
rig mode set human-led --scope mission --qualifier my-project/release-1 --evidence "Return to interactive planning" --confirm
rig mode unset mission my-project/release-1
```
Unsetting reveals the next applicable explicit choice, or the visible human-led
product default. A new, resolved rig with no binding reports `human-led`,
`source: product-default`, and `binding: null`. Missing scope identity, conflicting
linkage, unavailable storage or malformed authored context reports `unknown` with
a reason. Calling `effective` without a scope does not select a rig implicitly.
The existing operator bearer requirement still applies. `set` requires `--confirm`;
`unset` is an explicit deletion command and applies immediately.
## One scope and phase contract
`GET /api/rig-mode/effective` accepts `rig`, `project`, `mission`, `workstream`, and
`qitem`. Its additive `operatingPosture` object is also attached to health records
and diagnosis findings. CLI explanation and TUI health detail display the same data.
Consumers of posture, phase, and health should use this object rather than invent
another preference store or infer delegation from workflow existence or activity.
| Field | Meaning |
| --- | --- |
| `posture` | `human-led`, `delegated`, or `unknown` |
| `source` | `product-default`, `binding`, or `unknown` |
| `context` | Resolved rig/project/mission/workstream/qitem IDs, source addresses, canonical authored paths and phase; null if scope resolution failed |
| `context.phase` | `{value, source}` from an explicit workflow packet step, otherwise slice stage/status, otherwise mission release phase/status; null fields when unavailable |
| `binding` | Winning binding ID, scope, timestamp and evidence citation; null for default/unknown |
| `reason` | Explanation of the result or the missing/conflicting fact |
| `grantsAuthority` | Always `false` |
| `members` | For queue-backed findings, each member's qitem, posture, source and binding ID; differing or unknown member postures yield unknown aggregate posture |
The existing SQLite mode-binding table remains the only preference store. Matching
posture bindings resolve in this order: qitem, workstream, mission, project, rig,
global host. The qualifiers are respectively a qitem ID, `project/mission/slice-id`,
`project/mission`, project ID, canonical rig ID, and null. Rig names resolve to IDs.
The workstream is an existing authored slice, found by its directory or SPEC ID;
the returned identity uses its SPEC ID and project/mission qualification.
> grounded against openrig main `c27f2aff`, 2026-07-05/06). **Once shipped,
> THIS repo document is the living SSOT for the SDLC conventions going
> forward**; the corrective spec is the historical design record. Scaffold
> templates, the advisory scope audit, the `mission-slice-sop` skill, and the
> shipped bootstrap overlay all point HERE — they must not restate this
> document in full (restatement is how drift is born).
> **Source of truth:** this repo document is the living SSOT for the SDLC
> conventions. Scaffold templates, the advisory scope audit, the
> `mission-slice-sop` skill, and the shipped bootstrap overlay all point HERE —
> they must not restate this document in full (restatement is how drift is born).
OpenRig is a software factory the human steers from a high altitude. The human
records intent; agents turn intent into a plan, the plan into a build, the
build into proof. The Living Notes UI is a **plain projection of well-formed
markdown on disk** — agents change the files, the UI re-projects. These
conventions define "well-formed." Everything here is **advisory / fail-open
for agents**: nothing below blocks a write; the audit records and advises.
## WHERE YOU ARE DECIDES WHICH LOOP YOU RUN — the VM inner loop vs the host outer loop
The parent host's SDLC was being applied wholesale to the VM. **They are different worlds with
different responsibilities**, and conflating them is what turned this document into friction.
| | **INNER LOOP — the VM** | **OUTER LOOP — the parent host** |
|---|---|---|
| **question it answers** | *does it actually work?* | *does what was delivered match what was intended?* |
| **method** | ground → fix → upgrade in place → **run it and look** → iterate | intent-vs-delivered comparison, commit-message review, privacy/leak scrub, release management, publish |
| **definition of done** | to the best of our knowledge the code is **completely functional and works**, and needs **no heavy refactoring** to be release-ready | the release is shippable and honest |
| **ceremony** | **Part A only.** Part B is not the default here. | **Part B applies.** The release itself earns it. |
**WHY the ceremony belongs there and not here — the rule that makes this self-enforcing:** the
proof ceremony is a **protocol for transmitting confidence to someone who was not there.**
Plan-lock, proof contracts, C1 drops and intent-vs-delivered all exist so a reader who did NOT
watch the work happen can verify that it did. On the host that is exactly the situation, so the
artifacts must carry the confidence. **On the VM the builder IS there** — they ran it and looked at
it. Running the ceremony there re-encodes, at high cost, something already known, for an audience
standing in the room.
**WHERE A FIX GOES — go where the validation is POSSIBLE, not where the ceremony lives:**
- **Needs a live daemon to verify → the VM.** That is the only place it CAN be verified. (Worked
example: proving the ACTIVITY telemetry was really fixed required cutting the runtime over and
reading real values; no amount of host-side review could establish it.)
- **Verifiable by reading — docs, comment scrubs, message hygiene → the host.**
**The VM's north star for a release:** hand the host code that, to the best of our knowledge, is
completely functional and works. Not a proof pack. **Effect-verified functionality.**
build into proof. The TUI projects authored work on disk and, where an owning
proof policy is selected, derives readiness from recorded judgments. These
authoring conventions are **advisory / fail-open for agents**: the audit
records and advises; the selected proof policy separately governs who may judge.
## HOW TO READ THIS DOCUMENT — two parts, and only one of them is the default
@@ -54,11 +18,43 @@ completely functional and works. Not a proof pack. **Effect-verified functionali
| | applies | contents |
|---|---|---|
| **PART A — THE SIMPLE SDLC** | **ALWAYS. This is the default.** | artifact conventions, proportionality, the advisory audit, the honesty rails |
| **PART B — THE RIGOROUS OVERLAY** | **ONLY when the founder, or an orchestrator relaying the founder, ASSIGNS the heavy path to a NAMED piece of work** | the proof-contract format, the two locks, C1 proof drops, the locked role contracts |
| **PART B — THE RIGOROUS OVERLAY** | **ONLY when the human operator, or an orchestrator relaying that decision, ASSIGNS the heavy path to a NAMED piece of work** | the proof-contract format, the two locks, C1 proof drops, the locked role contracts |
**You may not select Part B for yourself.** If you believe something earns it, say so in ONE
sentence and continue on Part A until told otherwise. Part A corresponds to the `mission-slice-sop`
skill; Part B to `mission-slice-intent-proof-sop`.
skill; Part B is defined below and applies only when assigned.
## THE COMPONENT MENU — this document is a menu, not a pipeline
The SDLC is assembled from **components chosen per mission**, not a single mandated flow.
This document describes the components and carries recommendations; a mission (or the team
running it) picks per phase. Multiple valid options can exist for the same phase — choosing
one does not deprecate the others. Nothing below deletes or overrides an existing component.
| phase | components available today |
|---|---|
| **PRODUCT / MISSION PREPARATION** | **The product-management pass** — the layer above planning, run by the advisor/PM seat before planners touch a mission. The build is a lossy compression pipeline (PM intent → planner spec → builder code); nothing leaks downward by proximity, so this pass grounds in the raw sources, synthesizes ONE scope of record, presents owner decisions decision-ready, mints slices at plan time, deposits the full design contract INLINE in each design-bearing spec (owner words verbatim, settled decisions, evidence, shape of done, anti-goals), routes the complete reading list and internet-research assignments from the spec itself, frames each slice's planning-dial rung with its reason, and rereads every spec as the narrow downstream reader. The measuring stick: SPEC.md alone, plus what it explicitly routes to, reproduces the intent in a reader with none of the author's context. Full reference: `product-management-pass.md`. |
| **PLAN** | **The planning dial** — planning rigor is a spectrum chosen per piece: P0 mini-requirements and pointers (simple, reversible work) · P1 an authored spec with a proof contract (the default) · P2 plus a research round before the spec freezes, run BEFORE build dispatch · P3 plus an adversarial pass by a non-author, judged against product goals, landing amendments with proof-contract teeth · P4 plus a blind from-scratch design diffed against priors before proceeding. Dial up by what the piece is, exactly as build care is priced below. Full reference: `planning-dial.md`. |
| **BUILD** | **Part A, the simple flow** (below — the default) · **product-journey components** — a mission-selectable menu for greenfield work, drift recovery, public journey probes, one evidence-gated look at the layer below, minimal repair, independent QA/review, integration, and installed smoke; compose only what the work needs in mission/slice YAML, never as a new mode or mandatory pipeline. Full reference: `product-journey-sdlc.md` · **the wave model** — slices build in parallel in disjoint file territories, an integrator merges serially, and independent review fires once per wave (two reviewers with different vantages, never the writers; fix rounds re-earn verdicts at one final revision; per-slice discipline stays failing-test-first and verify-by-effect). The wave IS the care dial: what shares a wave decides how much review each piece gets · **Part B, the rigorous overlay** (below — only when assigned). These combine: a wave can carry one Part-B piece. Full wave reference: `wave-sdlc.md`. |
| **DISPATCH DATA** | **The execution-data conventions (S27)** — three tiny data disciplines that make execution state derivable instead of hand-tabulated, consumed by `rig view show execution`. **EC-1, edges in frontmatter:** every open slice carries its hard edges in `depends_on:` (an inline JSON array; an explicit `[]` is a statement, absence is INDETERMINATE); soft/serialization edges are one machine-readable Territory line, exactly `SOFT-AFTER: [ids] — reason`. **EC-2, wave-as-data:** wave composition + review model live on a durable queue row tagged `wave-map` + `format:wave-map-v1` whose body carries one fenced ```json block (`{format:"wave-map-v1", mission, waves:[{id, slices, serialized_order?, review_model?}]}`) — composition only, never status labels (status derives at read time); the planning dial recorded at lock surfaces as the frontmatter field `approved-spec-dial:` beside the approve stamp. **EC-3, dispatch names the worktree:** build batons carry the exact body line `worktree_path=<path>` from dispatch onward — it is Q1's join key; batons without it join by naming only and the view marks them `fragile_join`. Existing batons are never rewritten. |
| **RELEASE** | The release ceremony conventions (publication identity, honest release notes) and the CAPABILITY-DELTA LIFECYCLE LAW: each release's delta is authored at the fence and bound to the exact candidate, then reconciled to the published cut. Expiry requires both the canon header naming the absorbed delta and a distinct successor delta existing; a draft header alone is insufficient. Verify the actual boundary event and stop citing an expired delta. Full statement: `release-boundary.md` §6. |
| **BOUNDARY** | **The release boundary** — housekeeping run once per fence so the next release starts clean: seats reset or re-primed per their continuity needs, memory distilled (keep what changed a decision recently), every queue swept by destination and dispositioned, substrate torn down, boards frozen as records, a clean-box baseline captured. A checklist of judgment guides, not gates. Full reference: `release-boundary.md`. |
> Full reference documents for each component (graduated 2026-08-28): the
> product-management pass — `product-management-pass.md` · the
## Summary — A test system for the product, plus reliability fixes shipped through it
**v0.5.1 gives the product a test system.** Scenarios drive the real product through the same commands a person uses, so it can prove its own behaviour without a human checking by hand. The reliability fixes of this window ship through the test system — proving its value on its first cycle.
**v0.5.1 contains v0.5.0 in full.** One lineage, no divergence.
Migrations are additive only. Existing v0.5.0 databases upgrade by running `rig daemon start` on the new daemon. No CLI-command or flag removals or renames from v0.5.0.
## What Shipped
### Test system
- **Stub runtime for tests** — a fake agent runtime that scenarios can drive. Lets scenarios exercise the daemon end-to-end without needing a real Claude or Codex process.
- **Scenario format + runner** — a text-based scenario format and an executor that runs scenarios against the real product. Includes a real fix that surfaced from writing the runner: text-matching against a pane or transcript could never match its two intended surfaces; now it does.
- **Seed scenarios** — three seed scenarios that run green end-to-end against the real product. See Known Limits below for the seed-scenario one that could not be built.
### Container test bed — scaffolding
**Scaffolding for container mode** ships: argument builders, image-identity stamping, runbooks, and a unit suite. **The runner does not yet execute in containers** — this is scaffolding you will see in the tree but that no test currently drives. Container-mode execution is scoped forward.
### Reliability fixes (shipped through the test system)
- **Honest render for daemon transport failures** — the operator sees the actual failure rather than a silent no-op or generic error.
- **Queue-persistence** — two fixes ensuring queue records reflect what actually happened after they were written.
- **Cross-machine send regression** — found and fixed during verification. This is the test system doing its job: proving product behaviour across machines and catching a regression before release.
### Small features
- **Per-agent model from spec** — each agent's model comes from its spec rather than a fleet-wide default, so different agents can run on different providers or models.
- **Token usage tracked over time** — the daemon records token use so operators can see trends and predict when a seat is approaching a usage limit.
- **Machine-of-origin on messages** — messages carry the machine they came from, so multi-host coordination is legible.
### Other
- **UI timing hardening** — UI tests no longer fail on load-timing flakes.
- **Queue records tell the truth after they are written** — a class of wrote-it-and-it-did-not-stick issues closed.
- **Unreached-message visibility** — show messages that never reached an agent. On landing, this surfaced nine real handoffs that had been silently lost since 8 August.
## Known Limits
- **Container mode is scaffolded but not driven.** You will see the seam, argument builders, image stamping, runbooks, and a unit suite in the tree, but no test currently executes in a container. Scoped forward.
- **Seed scenarios: three of the intended set run green.** The tenth cannot be built without two capabilities that do not yet exist in the product (a scenario cannot mutate the running system's shape; the terminal interface exposes navigation state rather than fleet inventory). Both capabilities are scoped forward.
## Behavior + Compatibility
- **Migrations** — additive only. Existing v0.5.0 databases upgrade by running `rig daemon start` on the new daemon.
- **API surface preserved** — no CLI-command or flag removals or renames from v0.5.0.
- **v0.5.1 contains v0.5.0** — one lineage, no divergence.
## See Also
- `CHANGELOG.md` — the `[0.5.1]` entry.
- `docs/releases/v0.5.0.md` — the prior release (mission control TUI + context library + permission policies).
## Summary — Test system exercises for real; crash-cart conductor; reliability fixes
**v0.5.2 puts the v0.5.1 test system to work.** The reliability fixes in this release were found and validated by scenarios that drive the real product through the same commands a person uses. The headline lands: a crash-cart fleet-restore conductor that takes you from a dead daemon to a truthful cockpit and back to a live fleet with one keystroke.
**v0.5.2 contains v0.5.1 in full.** One lineage, no divergence.
No new migrations in this release. v0.5.1 already shipped migrations 068-071 (with 068 and 071 as a matched create / drop-if-exists pair, net no-op for any v0.5.0→v0.5.1 upgrade). v0.5.2 stays at migration 071. No CLI-command or flag removals or renames from v0.5.1.
## What Shipped
### Crash-cart fleet-restore conductor (headline)
Type bare `rig` at a dead daemon and reach a truthful cockpit — no cryptic error, no silent failure. One Enter restores the fleet kernel-first, adopts any surviving tmux panes (never clobbers them), and presents an exact per-seat remediation walkable in one keystroke. Cancel is honest. Destructive restore is only offered when adoption cannot succeed. Ten build rounds, four independent gates, non-author QA door test.
### Daemon event-loop no longer hangs under load
A class of daemon event-loop freeze under load is fixed and measured. If your daemon was going unresponsive under fleet-wide activity, that's this class.
### Unattended Claude seat handover
When a Claude seat needs to hand over (compaction, session boundary, explicit handover request), the outgoing seat now writes a recap and submits its packet automatically — the incoming seat picks up with the recap in hand rather than a cold start. Door-proven end-to-end.
Malformed or hostile policy input now produces honest structured errors rather than silent failure or unexpected behaviour. Three review rounds.
### Cross-host messages carry their machine of origin
Messages sent across hosts now include the machine they came from, so multi-host coordination reads correctly at the receiving end. Enforced end-to-end via door tests.
### Codex model-config drift detector
A new detector for a class of Codex model-config drift that would previously go unnoticed. Caught real cases on landing.
### Test-integrity work
- Test runner refuses to run against a dirty tree — a green run now means green source, not accidentally-uncommitted state.
- Hermetic test roots.
- Flaky fixtures isolated.
### Governance in code
Plan locks are explicit (chosen when you ask for one), not inherited from ambient state. Wake is opt-in.
### Container tar-file hang eliminated at the seam
A class of container tar-file hang is dead by construction — not worked around, but the seam that made it possible is closed.
## Known Limitations (5.3 backlog)
Ledgered honestly for the next release cycle. Named so you know what's not yet fixed:
- **Reply-hint self-qualify** — reply-hints on some cross-host messages address themselves via machine-ID rather than registered host name; using the reply-hint directly may fail with "no registered host X". Substitute the registered host name from `rig host list`. Fix in 5.3.
- **`rig queue handoff --summary`** warns yet the field is unsettable on its own output. Fix in 5.3.
- **`rig send --verify` does not detect staged-unsent** for multi-line sends toward Claude seats. Multi-line sends can stage silently.
- **Claude transcripts thin under fullscreen upsell** — upstream fullscreen upsell writes `"tui":"fullscreen"`; one accepted prompt can re-flip a fleet. Fix + pin + prevention in 5.3.
- **Test runners can silently skip on unbuilt trees** — a final-gate rule (build CLI + assert zero skips) is adopted for the release ceremony; the product-side fix lands in 5.3.
- **`seat-handover` model-fidelity real-Codex end-to-end** fails identically on main (environment-vs-product distinction unrooted). Isolation fix in 5.3.
Plus a handful of internal debt items ledgered for maintenance.
## Behavior + Compatibility
- **Migrations** — none in this release. v0.5.1 shipped 068-071 (068 CREATE + 071 DROP-IF-EXISTS pair, net no-op for v0.5.0→v0.5.1 upgraders); v0.5.2 stays at 071.
- **API surface preserved** — no CLI-command or flag removals or renames from v0.5.1.
- **v0.5.2 contains v0.5.1** — one lineage, no divergence.
## See Also
- `CHANGELOG.md` — the `[0.5.2]` entry.
- `docs/releases/v0.5.1.md` — the prior release (a test system for the product + reliability fixes).
**v0.5.3 puts context on rails.** Pull exact sections of shipped skills and library entries by address instead of reading whole files. Discover context by asking, not by guessing paths. Refocus resolves refs automatically. Handovers write a durable, superseded-chain seat recap that lands in every restore packet. Composition is by reference, so there is no second copy to drift.
**v0.5.3 contains v0.5.2 in full.** One lineage, no divergence.
No new migrations. Migration head stays at 071 (matches v0.5.2). Nothing to apply on a v0.5.2→v0.5.3 upgrade. No CLI-command or flag removals or renames from v0.5.2.
## What Shipped
### Address grammar for context: `rig context get` (headline)
Pull the exact bytes of one section:
```
rig context get <pack-ref>/<file>#<H2-slug>[/<H3-slug>]
```
The `/<file>` component is required even in a one-file pack. Refs are canonical full paths (e.g. `skills/<namespace>/<name>`). A bogus slug fails loud and lists the addressable sections. `rig context list` enumerates every shipped entry's ref and name; `rig context show` inspects one. Cross-source access (`seat:` / `mission:` atoms) is an **authoring** affordance — declared in pack manifests and granted via `rig context profile --rig/--seat/--mission` during composition — not an ad-hoc argument to `get`.
### `openrig-skills` router — ask → ref → load
The router teaches the three-step: ask your question, get a ref, load by ref. Use it when you don't know which skill or context applies, or nothing is projected after a cold boot.
Handover time, compaction boundary, or any moment your successor would otherwise re-derive your state from scrollback: `rig context recap-write` maintains a seat-scoped `RECAP.md` beside `LEARNED` with a collision-safe superseded chain. Restore packets now carry an authored-recap pointer.
### Refocus resolves refs automatically
The refocus hook resolves library refs automatically (ref beats file), fails loud but non-blocking, and delivers pointers. This is the shipped channel for pointing running seats at new content.
### Bounded expertise packs on demand
`rig context get` serves expertise packs by path-like ref, e.g. `skills/process/context-engineering`. The old `context-pack:` colon form was removed and the CLI says so.
**Caveat that pack:** it is titled "Traditional Context Engineering — 2024-2025 Snapshot" for a reason — provisional, historical, non-normative by founder ruling. Current OpenRig skills, explicit user rulings, and measured practice outrank it on any conflict.
## STOP doing (was correct under 0.5.2, is wrong now)
1. **STOP copying library content into seat or mission files to compose context.** Compose by ref: addressing means there is no second copy to drift. Nothing catches the copy automatically; it is still a defect by rule.
2. **STOP treating a `MODEL DIVERGENCE` proclamation as alias noise.** The canonicalize-then-compare fix ended the pinned=fable / effective=claude-fable-5 false-positive class, and the monitor now isolates per-seat throws. A proclamation on this runtime is a real escalation — act on it.
3. **STOP reading "founder-gated" in tool errors as an authorization wall.** The overstated wording is retired from the scripts; those guards demand explicit paths (e.g. `OPENRIG_SKILL_CANON_ROOT`), not permission. The real founder boundary is exactly: pushes and PRs. Local commits and ordinary apply work never need founder word.
4. **STOP hand-placing evidence or trusting a placeholder PRD as the proof contract.**`rig proof add` pairs evidence to the contract, and a pristine scaffold PRD can never silently become that contract: the authored SPEC serves with a named advisory (`contractSource` in the echo tells you which source bound).
## Landed but not yet drivable
Eval CASES ship (14 selection/loading cases), but no runnable grading pass exists today. `--provider rig` is the live proof-contract door and is NOT YET DRIVEN — it says so itself and redirects to fake (the wiring is the convergence-test driver work in flight). `--provider fake` replays only transcripts supplied via `--transcripts <map.json>` and errors every case without one. When the live door is driven, the next release promotes this to a capability.
## Behavior + Compatibility
- **Migrations** — none in this release. Migration head stays at 071 (matches v0.5.2). Nothing to apply on a v0.5.2→v0.5.3 upgrade.
- **API surface preserved** — no CLI-command or flag removals or renames from v0.5.2.
- **v0.5.3 contains v0.5.2** — one lineage, no divergence.
## Known Limitations (5.4 backlog)
- **Reply-hint self-qualify** — carried from 5.2; reply-hints on some cross-host messages address themselves via machine-ID rather than registered host name. Substitute the registered host name from `rig host list`. Fix in 5.4.
- **Eval grading pass** — CASES ship; no runnable grading pass yet. Driven `--provider rig` lands in 5.4.
- **`rig send --verify` does not detect staged-unsent** for multi-line sends toward Claude seats — carried from 5.2 backlog.
- **Claude transcripts thin under fullscreen upsell** — carried from 5.2 backlog.
- Plus internal debt items ledgered for maintenance.
## See Also
- `CHANGELOG.md` — the `[0.5.3]` entry.
- `docs/releases/v0.5.2.md` — the prior release (test system in earnest + crash-cart conductor).
**v0.5.4 makes command output describe the effect that actually happened.** Transport resolution distinguishes missing, found, and indeterminate sessions. Delivery verification distinguishes staged input from an agent-consumed turn. Queue, refocus, lifecycle, import, and preview surfaces preserve real state and refusal semantics instead of collapsing them into plausible success.
**v0.5.4 contains v0.5.3 in full.** The VM cut was reconciled onto the published line by an ordered 32-of-32 stable patch-id match.
No new migrations. Migration head stays at 071 (matching v0.5.3). Node engine requirements are unchanged. Existing CLI commands remain; this release adds `rig seat set-model`, `rig seat stop`, and `rig seat clean`.
## What Shipped
### One classified session-resolution path
Send, capture, nudge, and walk consume one probe result: found, absent, or indeterminate. A tmux or transport blip no longer becomes a false `Session not found`, and recovery output says what was checked.
### Delivery verification by effect
`rig send --verify` checks whether input remained staged or was consumed as a turn. Single-target, fan-out, and JSON output use the same classification, so "typed" and "delivered" are no longer synonyms.
### Honest unknown-sender delivery
Messages without attributable sender identity are delivered with an unknown-sender label instead of refused. Where possible, the sender receives a notice to follow up with attribution.
### Supported single-seat lifecycle controls
- `rig seat set-model` changes the canonical model binding with an audited reason.
- `rig seat stop` stops one live seat without disturbing siblings.
- `rig seat clean` removes dead-session residue so the logical seat can launch again.
Safety is whole-class: every session a lifecycle action could mutate is probed, and a live or indeterminate result refuses the action. The model change was exercised through a real managed successor.
### State-preserving queue and refocus behavior
Queue notes preserve the row's state. Owned listing without identity refuses instead of silently widening or changing scope. Refocus keeps due state until delivery becomes visible. Preview and handoff output say when content is bounded or optional.
### Running-name guard across create and import
A running rig name cannot be recreated through another instantiator or import route. Every path preserves the same `409 rig_name_running` code and teaching message through the CLI boundary.
### Hermetic startup and plugin authority
Scratch/test startup boundaries are hermetic. The bundled `openrig-core` plugin advances under explicit version authority, preventing an older runtime from silently rolling installed guidance backward. Stale lab refocus registrations are retired.
## Behavior and Compatibility
- **Migrations** - none; schema head remains 071.
- **API compatibility** - existing commands remain available; three seat lifecycle verbs are additive.
- **Lineage** - the 0.5.4 product delta is reconciled onto the published 0.5.3 line by patch identity.
- **Release boundary** - tag, npm publish, host install, and daemon cutover remain separate authorized acts; this document does not imply that they occurred.
## Known Limitations (0.5.5 backlog)
- **Writer-layer disk cap** - cleanup is an interim control; the permanent write-layer cap is unfinished.
- **Repair/approve frontmatter rewrite** - the known rewrite defect remains open.
- **Observation pipeline performance** - the larger pane/process polling redesign remains measurement-gated and is not claimed by this release.
## See Also
- `CHANGELOG.md` - the `[0.5.4]` entry.
- `docs/releases/v0.5.3.md` - the prior context release.
- GitHub Release: https://github.com/mvschwarz/openrig/releases/tag/v0.5.4 (created only after the authorized publish ceremony).
**v0.5.5 makes the fleet notice, retry, escalate, diagnose, and onboard on its own.** Stop hand-babysitting the work in flight: `rig parked` diagnoses silent-stuck with confidence and teaching; the standing stuck-sweep runs without you; baton wakes retry, aggregate, and escalate on their own; parks carry their own wake and read HEALTHY; `rig ps` grows a typed `ACTIVITY` column from one oracle; the Slack human layer is live end-to-end; fresh installs onboard themselves.
**v0.5.5 contains v0.5.4 in full.** One reconciled lineage.
**Migrations**: TWO new. Head advances `071` → `073`:
Activity × open obligations, with per-input confidence and remedy taught inline. Reach for it when a seat looks idle and you suspect dropped work — before any `claimedAt` arithmetic or pane capture. The row face used to hide the difference between park and strand; this verb ends that.
### Self-retrying wake ladder + escalations view
Failed baton wakes retry, aggregate, and escalate on their own. Transitions ARE the ladder state (restart-safe); per-destination aggregation; rungs deliver-and-advance; unconfirmed-with-no-pickup escalates without re-send. Watch `rig view show escalations` for the aggregates. Hand off the row and stop chasing nudges.
### Standing stuck-sweep runs without you
Overdue + undelivered become routed findings with derived evidence inline. Stop hand-running `queue overdue` / `undelivered` on a timer — consume the sweep's findings instead.
**Caveat (5.6 backlog):** cross-host successor visibility is a proven false-positive class — 39 active findings at census (20 proven FP, 19 unverified). Contained fleet-wide one-per-condition; treat cross-host-lineage findings as unverified until the 5.6 detector fix. The delta itself teaches the caveat.
### `queue block --on <blocker>` — park-with-wake
A park can carry its wake and read HEALTHY. `queue block --on <blocker-qitem>` records the wake; held-with-live-wake is not flagged; auto-unparked owners get an honest wake. For imminent-but-blocked work only; not-imminent work belongs in the mission workspace — the queue is a conveyor.
### `rig ps` gains a true `ACTIVITY` column from one oracle
Typed taxonomy: working / idle-at-prompt / needs-input-as-count+reason / unknown. Evidence ladder with self-report on top and visible rung degradation. Seat-keyed across swaps. The TUI reads the same truth. For any "is it thinking or stuck" question — the oracle beats capture.
`fork:` carries live context; `rebuild` primes from the durable chain and names its priming artifacts. Mid-swap failures record honestly. When replacing an occupant — no more dry-run-only planning surface.
### Slack human layer live end-to-end
Gateway running, thread-per-seat, exactly-once inbound reconciliation, escalation loudness (mention) distinct from routine. Humans are addressable members; `rig gateway human` has full fragment-lifecycle verbs. Any agent can escalate directly; escalation-class sends arrive loud on the destination's phone.
### Fresh installs onboard themselves
Fresh installs get focused onboarding packs by default (config off; existing rigs untouched). When standing up a new rig or seat — stop hand-walking the eight pieces.
### Refocus fires only when it should
At a context threshold or on demand — NEVER at session start. Post-compaction both runtimes; usage threshold Claude-only. A fresh seat getting a refocus is now a bug to file, not noise to ignore.
### One-file scope convention
`scope mission create` → `NOTES.md` + intent-bearing `SPEC`; `slice create` → `SPEC`-only (no `IMPLEMENTATION-PRD`) + acceptance `PROGRESS`; release missions get a capability-delta scaffold. `repair` is ADDITIVE — stamps append, never rewrite author frontmatter. Stamp verification is a mechanical strip again.
### Daemon bind intent has provenance
An inherited `OPENRIG_HOST` never selects single-bind; adoption gates test ALL required listeners. Daemon maintenance from any managed environment — no more `env -u` ceremony.
## STOP doing (each was correct under 0.5.4 and is wrong now)
1. **STOP the per-lock author-reconstruction protocol.** Stamps APPEND now; strip-reconstruction is mechanical.
2. **STOP diagnosing parks by `claimedAt` arithmetic + capture.**`rig parked` derives the diagnosis with confidence and teaching; capture is a fallback glance.
3. **STOP storing deferred work as queue rows.** The queue is a conveyor for imminent sequential work; deferred work lives in the mission workspace.
4. **STOP authoring `IMPLEMENTATION-PRD.md` and `MISSION_NOTES.md` on new work.**`SPEC.md` is the one spec file; `NOTES.md` is the chain name.
5. **STOP routing contact through an orchestrator by default.** Any agent escalates directly.
## Landed, not yet drivable
| Landed surface | Missing live door | Honest current action |
|---|---|---|
| A–D delivery preferences + availability (stored, validated) | rules engine (0.5.6 slice 01) | set availability via `rig gateway human` verbs; expect no routing effect yet |
| S01 operator rung | human-layer connector delivery leg (0.5.6 wave S11 territory) | escalation view + daemon health carry the floor |
| `@external` addresses on the direct send path | routing falls through to tmux (0.5.6 wave-2 fix) | reach humans via the gateway path (queue/escalation), not raw `rig send` |
| Multi-human topology | single-human ships (honesty marker); multi-human is 0.5.7 | one registered human; design assumptions accordingly |
## Behavior + Compatibility
- **Migrations** — two new: `072_thread_seat_map.ts`, `073_queue_transition_wakes.ts`. Head advances `071` → `073`.
- **API compatibility** — existing commands remain available; new verbs are additive.
- **Lineage** — the 0.5.5 product delta is reconciled onto the published 0.5.4 line by aggregate patch identity.
- **Release boundary** — tag, npm publish, host install, and daemon cutover remain separate authorized acts.
## Known Limitations (0.5.6 backlog)
- **Cross-host stuck-sweep false positives** — 39 active findings at census (20 proven FP, 19 unverified); contained fleet-wide; detector fix lands in 0.5.6 wave 1.
"import {createRequire} from 'node:module'; const require=createRequire(process.argv[1]); const D=require('better-sqlite3'); new D(':memory:').close();",
@@ -112,7 +112,7 @@ export function renderDaemonTransportError(
fact: e.message,
consequence:"The command's outcome is UNKNOWN — the daemon did not answer in time.",
action:
"The daemon is slow or unresponsive (check load); retry once conditions ease. This is a slow response, not a stopped daemon.",
"Check the command's effect before any retry, then inspect daemon load with 'rig status' and the daemon logs if it remains slow. This is a slow response, not a stopped daemon.",
return"# missions\n\nProject missions live here. Each mission folder maps to one Project mission in the UI and owns a `slices/` child folder.\n\nExpected shape:\n\n```text\nmissions/<mission-name>/README.md\nmissions/<mission-name>/PROGRESS.md\nmissions/<mission-name>/slices/<slice-name>/README.md\n```\n\nEvery mission and slice README carries a stable dot-ID in its frontmatter (`id: OPR.<ver>[.<n>]`) per the scope-and-versioning convention. Use `rig scope mission create <name>` and `rig scope slice create <mission> <slug>` to mint conformant artifacts; the CLI handles the dot-ID + auto-numbering for you. The folder name is the operator-facing slug; the `id:` is the stable handle that survives renames.\n";
case"artifacts":
return"# artifacts\n\nWork products live here: plans, drafts, generated outputs, and other files that a slice may reference before closure.\n";
case"evidence":
return"# evidence\n\nHuman-readable verification notes live here. Use this for compact proof summaries that are not tied to a screenshot or video packet.\n";
case"progress":
return"# progress\n\nPROGRESS.md tree. OpenRig's Progress browse view scans this directory recursively for PROGRESS.md files and renders them as a hierarchical tree.\n";
case"field-notes":
return"# field-notes\n\nOperator field notes. Free-form markdown notes from your daily work. OpenRig surfaces these alongside missions and slices for context.\n";
return"# dogfood-evidence\n\nProof packets live here. Each proof packet folder is matched to a Project slice by folder-name tokens and may contain markdown, screenshots, videos, traces, and other verification artifacts.\n";
-\`artifacts/\` is for work products that a slice needs to keep.
-\`evidence/\` is for proof notes and verification summaries.
-\`dogfood-evidence/<proof-packet-id>\` becomes Tests proof when the packet id contains the slice id tokens.
Usestablekebab-casenamesformissionandslicefolders.Everymission/sliceREADMEalsocarriesastabledot-IDinitsfrontmatter(\`id: OPR.<ver>[.<n>]\`) per the \`scope-and-versioning\` convention; mint conformant artifacts via \`rig scope mission create <name>\` and \`rig scope slice create <mission> <slug>\`. Folder names are operator-facing slugs; the \`id:\` is the rename-proof handle.
`;
constSTEERING_PLACEHOLDER=`---
title: PriorityStack
status: placeholder
---
#OpenRigPriorityStack
Thisfileisaplaceholdercreatedby\`rig config init-workspace\`. Edit it
constFIRST_CONVEYOR_RUN_TIMELINE=`# Story — First Conveyor Run
##2026-04-1509:00—Slicedeclared
Operatorrequest:"Build a CLI tool that lints todo lists."Slicedeclaredin\`getting-started/slices/first-conveyor-run/\`. Acceptance: \`tdl lint <file>\` exits non-zero on malformed entries.
##09:02—Orchestratorroutes
\`orch-lead@getting-started\` picks up the slice. Routes to \`driver@getting-started\` via \`rig queue handoff\`. Driver receives nudge; opens IMPL-PRD; reads acceptance.
TheStorytabshowsthiskindofnarrativeforanyrealslice.Driver-authored\`timeline.md\` files live in each slice folder; this is yours to read + your operator's to update as work moves.
Thetabrenderstheslice's\`PROGRESS.md\` markdown file. Driver updates it as work progresses. Operator scans it during walks. Founder reviews at slice closure.
OperatorclicksArtifacts.Sees4commits,5files,1proofpacket,2screenshots.Openscommit\`feat(lint): parse + validate todo entries\`. Reads the diff.
##11:18—Readtests
OperatoropensTeststab.12/12passing.Openstheproofpacket—screenshotshowsCLIoutput\`Error: malformed entry at line 3\`. Visually verifies the acceptance was met.
>ConventionsSSOT:\`docs/reference/sdlc-conventions.md (installed: $OPENRIG_HOME/reference/sdlc-conventions.md)\` — section names, proof-contract format, the two locks, C1 headers. For a small slice the mini-requirements may BE the whole PRD.
thrownewError("Run Git source selection/inspection/update on the daemon host through its loopback URL; these commands use local Git and filesystem paths.");
thrownewError(`A context pack named '${installName}' already exists at ${targetDir}. Remove it first or use --name to install under a different name.`);
}
constfile=entryasRecord<string,unknown>;
constrelPath=file["path"];
if(typeofrelPath!=="string"||relPath.length===0){
thrownewError(`manifest at ${manifestPath} files[${i}] missing 'path' (string)`);
thrownewError(`manifest at ${manifestPath} files[${i}].path '${relPath}' has an unsupported suffix; allowed: ${Array.from(ALLOWED_CONTEXT_PACK_SUFFIXES).join(", ")}`);
// OPR.0.5.3.5 mini-req 7 — the outgoing occupant's boundary write (the Q2
// requirement the store alone does not satisfy). Daemon-independent like
// trace: the seat dir resolves from topology.root CONFIG; the write flows
// through the ONE store (supersession + addressability gate); advisory
// contract findings ride stderr while the write still lands — the boundary
// is never blocked on prose shape.
cmd.command("recap-write")
.description("Write this seat's authored RECAP (decisions-with-rationale) at the handover boundary; supersedes the previous recap into the seat's chain")
.requiredOption("--rig <rig>","Rig name (the rigs/<rig> altitude)")
.requiredOption("--seat <seat>","Seat id (the seats/<seat> altitude)")
.requiredOption("--file <path>","Markdown file containing the recap content")
thrownewError(`A context pack named '${installName}' already exists at ${targetDir}. Remove it first or use --name to install under a different name.`);
}
cpSync(source,targetDir,{recursive: true});
}
if(targetExists){
thrownewError(`A context pack named '${installName}' already exists at ${targetDir}. Remove it first or use --name to install under a different name.`);
}
cpSync(sourceDir,targetDir,{recursive: true});
// Sync the daemon library so the new pack appears immediately.
thrownewError(`Git selection retained at ${targetDir}, but this daemon does not serve it. Check context.root and workspace ref precedence before using it.`);
console.log(`Installed at ${targetDir}. Library now has ${syncRes.data.count} context pack(s).`);
if(gitSelection)console.log(`Git ${gitSelection.revision} from ${gitSelection.checkout}; inspect/update with rig context source.`);
}
}catch(err){
console.error((errasError).message);
@@ -463,6 +872,41 @@ Examples:
}
});
constsource=cmd.command("source")
.description("Inspect Git checkout vs served context, or explicitly fetch/merge and select an update")
.addHelpText("after","\nStart: rig context add <repository-path-or-URL> --git [--pack path]\nEdit/commit in the reported checkout with ordinary Git. Updates never push or reset.\nConflicts retain the old selection; resolve/commit or abort in the checkout before retrying.\nSelection does not prove agent consumption; use context get and check the actual consumer.\n");
if(resolve(entry.sourcePath)!==resolve(localRoot,entry.relativePath))thrownewError("This pack is not in the locally configured context library. Run Git source commands on its owning instance.");
if(entry.sourceType==="builtin")thrownewError("Builtin context is not a writable Git selection.");
if(sync.status!==200)thrownewError(`Selection updated, but library sync failed (HTTP ${sync.status}); run rig context sync.`);
if(!sync.data.entries.some((candidate)=>resolve(candidate.sourcePath)===resolve(entry.sourcePath)))thrownewError(`Selection retained, but the daemon cannot serve it: ${sync.data.errors?.map((error)=>error.error).join("; ")||"check context root and ref precedence"}`);
@@ -78,6 +113,7 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
cmd
.command("start")
.description("Start the daemon")
.addHelpText("after","\nStartup reserves this local instance before initialization and verifies the spawned child PID on every required listener.\nMissing/mismatched identity or child exit fails startup; failed publication withdraws only this launch's matching state. Use a matching CLI/daemon installation.\nA concurrent start fails without spawning another child. Inspect daemon-start.lock for launcher/child PIDs after an interrupted start;\nonly archive an abandoned reservation after proving both processes absent. A failed cleanup retains it and reports the child PID.\n")
.option("--port <port>","Port to listen on")
.option("--host <host>","Host to bind on")
.option("--db <path>","Database path")
@@ -108,9 +144,19 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
// multi-bind (loopback + tailscale auto-detect) when the operator
// S20 — bind intent comes ONLY from the dedicated surfaces: the --host flag,
// a FILE-sourced daemon.host, or OPENRIG_BIND_HOST. An env-sourced daemon.host
// is the overloaded routing channel (ENV_MAP maps it from OPENRIG_HOST — the
// exact injected state a managed environment carries) and never creates intent.
constintent=resolveBindIntent({
flagHost: opts.host,
envBindHost: process.env["OPENRIG_BIND_HOST"],
configSource: hostResolution.source,
configHost: config.daemon.host,
});
consthostUserExplicit=intent.explicit;
consteffectiveHost=intent.host??"127.0.0.1";
consthostForDaemon=intent.host;
// Run preflight before starting
constpreflight=newSystemPreflight({
@@ -140,6 +186,9 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
transcriptsEnabled: config.transcripts.enabled,
transcriptsPath: config.transcripts.path,
workspaceRoot: config.workspace.root,
contextRoot: config.context.root,
skillsRoot: config.skills.root,
topologyRoot: config.topology.root,
// V1 pre-release CLI/daemon Item 1 — project the
// ConfigStore-resolved rotation tunables into the daemon
// process env so file-stored values
@@ -190,11 +239,12 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
cmd
.command("stop")
.description("Stop the daemon")
.description("Stop the daemon (10s shutdown budget; 12s process wait; incomplete drain exits nonzero)")
.addHelpText("after","\nSends at most one SIGTERM to a live target and verifies the original PID and listener. Repeated signals join shutdown.\nFor a recorded target, missing/stale receipts and incomplete drains exit nonzero, including retries.\nTarget state is retained until a matching clean receipt; status reads preserve unverified state.\nNo target is a distinct no-op, never clean-drain certification; unbound incomplete evidence stays unverified.\nInspect OPENRIG_HOME/daemon-shutdown.json and daemon.log for the phase and outcome.\nThe bound covers asynchronous shutdown; an event-loop wedge still requires operator recovery.\n")
.action(async()=>{
try{
awaitstopDaemon(getDeps());
console.log("Daemon stopped");
constoutcome=awaitstopDaemon(getDeps());
console.log(outcome==="stopped"?"Daemon stopped":"No daemon target recorded; listener refused. Nothing to stop; prior drain not certified.");
console.error(`Service status is ${probeStatus==="stale"?"stale":"unverified"} (probe: ${probeStatus}); ${receipt?"showing the saved receipt":"no saved receipt is available"}.${typeofprobeError==="string"&&probeError?`${probeError}`:""}`);
}
if(!receipt){
console.log("Services configured but no receipt available yet.");
`refused: a human is already configured (${ids}) — 0.5.5 ships the SIMPLE SINGLE-HUMAN surface (amendment A1, founder R5), so \`rig gateway human add\` manages one human. `+
`If you truly need several, hand-author a fragment YAML under ${registry.humansDir()} (several fragments are displayed honestly, with an advisory); multi-human MANAGEMENT arrives in 0.5.7.`,
.description("Edit one field through the verb (same validation as add; re-projection immediate). Fields: display-name, delivery-class, away, binding.<n>")
.option("--reason <reason>","Reason recorded with connector binding changes","update human binding")
.option("--actor <actor>","Named operator when outside a managed seat")
.description("Remove a human: refuses while open conversations or non-terminal rows exist (--force archives and records what was orphaned; the fragment is archived, never deleted)")
.option("--force","Archive despite KNOWN in-flight items (each is recorded as orphaned)")
.description("Inspect read-only, explainable system health records")
.addOption(newOption("--self","Inspect the current seat (default)").conflicts(["seat","rig","instance"]))
.addOption(newOption("--seat <node-id>","Inspect one seat by stable node ID").conflicts(["self","rig","instance"]))
.addOption(newOption("--rig <rig-id>","Inspect findings whose canonical scope is this rig").conflicts(["self","seat","instance"]))
.addOption(newOption("--instance [instance-id]","Inspect the whole local instance, or one canonical instance scope by ID").conflicts(["self","seat","rig"]))
.addOption(newOption("--severity <severity>","Filter by severity").choices([...HEALTH_SEVERITIES]))
.addOption(newOption("--status <status>","Filter by finding status").choices([...HEALTH_STATUSES]))
console.log(` Later behavioral effect (owner report): ${correction?.effect.state??"unobserved"}${correction?" — "+correction.effect.summary:" — a disposition or clearance alone is not changed behavior"}`);
for(constrefofentry.authority)console.log(`${ref.role??"Authority"} (${ref.state}): ${ref.path}${ref.selectedBy?" selected by "+ref.selectedBy:""}${ref.reason?" — "+ref.reason:""}`);
if(occurrenceRead){
constview=diagnosisPreview(entry).readView;
console.log(` Evidence preview; full record ${view.fullJsonBytes} JSON bytes: ${view.fullCommand}`);
Loaded 100 of 1508 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.