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.
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.
v0.5.1 contains v0.5.0 in full. One lineage, no divergence, no dual
maintenance. Migrations are additive-only; existing v0.5.0 databases
upgrade by running rig daemon start on the new daemon.
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 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. The intended 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.
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.
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.
BEHAVIOR + COMPATIBILITY
- Migrations: additive-only.
- API surface preserved: no CLI-command or flag removals or renames
from v0.5.0.
- v0.5.1 contains v0.5.0 in full.
KNOWN LIMITS (carried into release notes verbatim)
- Container mode is scaffolded but not driven. Scoped forward.
- Seed scenarios: three of the intended set run green; the tenth
cannot be built without two capabilities that do not yet exist.
Scoped forward.
Formal CHANGELOG entry + docs/releases/v0.5.1.md follow in a small
docs commit on top of this release commit.
Reconciles origin/main forward-only into the 0.5.1 release branch so the
push is a fast-forward and does not drop the two 2026-08-07 docs commits
(CHANGELOG entries + docs/releases/v0.4.7.md, v0.4.8.md, v0.5.0.md).
Zero-conflict merge — the 362 scrubbed VM commits did not touch
CHANGELOG.md or docs/releases/*.md. Tree-identity of those 362 scrubbed
commits is preserved (first-parent reachable, SHAs unchanged).
Per §6a step 2 standing pattern: every release's public tip back-merges
into the working lineage promptly post-release. Doing it here at the
last honest moment; the release-ceremony discipline says next time this
happens right after each ship, not four days later.
5.1 RELEASE BLOCKER. The first real cross-host send 409'd at the SAME SHA on both
sides: 51-09 appended the origin host to the transport identity, so the header
carries a TRIPLE (member@rig@host-xxxx) while the body still carries the PAIR
(member@rig). Those values do not disagree — the triple is the same actor with its
origin added — but a literal string comparison read a less-specific-but-consistent
claim as a mismatch.
Deletion, not a smarter comparison: this refusal class was already RULED DELETED
(PM (A), 2026-08-11, require-sender-identity.ts:16-22) and both sibling helpers
already implement supersession. These two sites survived because the sweep ran
per-atom (P21 I4) rather than per-shape. At :223 the comment directly above the
guard already said the body value is IGNORED while the next four lines refused on
it — the code contradicted its own comment.
Provenance is unchanged: the wire decides the actor and the body never does. The
body is DISCARDED, not trusted. Refusing added nothing to that guarantee; it only
turned a deliverable message into a 409.
RED FIRST, against the L5 A3 assertion rather than an adjacent unit test: header
TRIPLE + body PAIR 409'd before the change and now delivers, with the outbox row
recording the WIRE identity and explicitly NOT the body pair — if the body value
had leaked into the recorded actor the fix would have been wrong.
Three existing tests pinned the deleted refusal; they are CONVERTED, not removed,
so the provenance coverage survives: each now asserts delivery plus a header-derived
sender that never contains the body claim, across /send and the broadcast fan-out.
Gates: daemon 551 files / 7388 passed / 0 failed; cli 166 files / 2275 passed;
daemon tsc 0, cli tsc 0. Out of scope and untouched: activity.ts:116 and the
parseSessionName first-at-sign question (PM carries it separately).
Co-Authored-By: Claude Fable 5 <[email protected]>
Slice 52, light path (founder-approved to land before 5.1). Eliminates the
load-sensitive false-fail in the host outer-loop ui suite by removing the
wall-clock race entirely rather than widening a timeout.
- app-shell.test.tsx: renderAt (+ the two SC-8 fixtures) mount AppShell via a
minimal createAppTestRouter (catch-all stub) instead of importing the whole
../src/routes.js tree. Chrome/surface is computed by AppShell from the router
pathname (surfaceForPath), so all SC-1/SC-2/SC-6/7/8 + surface-routing
assertions stay real; the heavy lazy-route import (the thing that raced the
5000ms waitFor) is gone.
- topology-view-mode.test.tsx: renderTopologyAt + the Class-B overlay fixture
mount HostScopePage/RigScopePage under AppShell via a minimal router; the
routes.tsx source-assertion (readFileSync) is unchanged.
- topology-perf-table-large-n.test.tsx: replace the flaky elapsedMs<2000
wall-clock assertion with a deterministic exactly-5-fetch invariant (1
summary + one /nodes per rig, each once, no dup/unknown); drop the
O(N^2)/Chrome-perf header claims. Mutation-check: a duplicated inventory
over-fetch drives it RED (nodeCalls 8 vs 4), asserted on count AND set-size.
Test-only: no production file changed. Full ui suite green uncontended
(197 files, 1723 passed / 1 skipped), ui tsc clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
dc0fb35cd (the conventions-doc commit) was authored from a stale working tree
and dropped 51-03 entirely: 2 files deleted, 5 reverted, 394 deletions. The
fold of dcddb8a6e remained an ancestor, so ancestry checks still passed while
the TREE no longer contained the work — the failure was only visible by RUNNING
the scenarios, which returned 2/3 with the pre-51-03 identity-normalizer output.
CAUSE, recorded so it is not repeated: main was folded with
`git update-ref refs/heads/main <sha>` WHILE HEAD WAS ON main. update-ref moves
the ref and leaves the working tree at the old content; the next commit then
captures that stale tree and reverts the fold. Fold-by-ref-update is only safe
when HEAD is NOT on the branch being moved — which was the whole reason the
technique was adopted (the primary worktree was usually occupied by a builder's
branch). It had silently stopped being true.
Caught by an effect check, not by review: `git merge-base --is-ancestor` said
YES, `git status` was clean, and only running the runner exposed it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Discovered by 51-03. Its intent was scoped "compose-only" — authoring ten
scenarios — while silently depending on two runtime capabilities that turned
out not to exist: the `mutate` verb has no shipped runtime binding, and the TUI
control socket exposes no daemon truth to compare against ps/queue.
That is a DEPENDENCY error, not scope creep by the builder and not
over-ambition by the author: the ambition was right, the availability
assumption was wrong. Building the missing capabilities inside the slice would
have been the actual scope creep.
The convention records the honest response: revise the ambition down to what
reality supports, mark the slice PARTIAL, and write ONE document giving a
downstream reader the accurate story — locked intent, what shipped and how it
was verified by effect, what did not ship with file+line proving the capability
is absent, what the delivery does not cover, and follow-on slices scoped from
what is known at discovery time.
The last point is the load-bearing one: the agents holding the context when the
gap is found are the most knowledgeable readers that problem will ever have.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Guard found and the desk reproduced two false greens on fc3f766e4: both sides
plucking a missing field normalized to [] and compared EQUAL (3/3 green on two
distinct non-empty surfaces), and a single-surface equals also passed. My own
falsification could not have caught either — I broke ONE side and got a correct
FAIL, which proves the comparison notices a DIFFERENCE but says nothing about
whether it notices EMPTINESS. A falsification only covers the axis it perturbs.
A. projectSurface now THROWS ProjectionError when a declared projection extracts
nothing from NON-EMPTY input. Empty-from-empty stays legal (a surface may
honestly hold nothing); empty-from-real-data means the declaration does not
match the surface, and it must never become equality evidence. Seeding a real
qitem cannot defend this — the PROJECTION is what empties the set.
B. Load-time refusals that teach, so a TypeError is never the first signal:
the declarative mapping is required (legacy list refused, naming the
declarative form), at least TWO surfaces are required (one-sided equality
passes regardless of the data), and path must be a string (it previously
passed validation and threw "path.split is not a function" at runtime).
C. Re-falsified on every axis, not just the original one: symmetric-empty now
ERRORs loud end-to-end with the field named, single-surface is refused at
load, and the one-sided difference case still FAILs. All three are unit
checks now.
Also isolated R4's pre-effect measurement to a private TMPDIR — it counted
scaffolds in the SHARED /tmp and flaked under the parallel matrix, the same
self-induced contention the D5 pin hit. Caught by an A/B run, not assumed.
3/3 scenarios PASS. Matrix 178/178 across 21 files, daemon tsc clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
The equals mode compared raw surface shapes through an identity placeholder, so
it could never hold: ps answers with rig records, queue with qitems whose
destinationSession is <pod>-<member>@<rig>. Comparing them requires DECLARING
which field of each carries the shared truth — that declaration is the
normalizer, and per A-N1 the declarative mapping is the only scenario-facing
form and LOWERS to the runner-internal seam. The seam signature is untouched;
this is additive.
equals:
ps: { pluck: name }
queue: { pluck: destinationSession, rig: true }
Projections are small and total: pluck a field per element, optionally reduce a
session name to its rig, then dedupe+sort so agreement on a SET is not defeated
by ordering or by N qitems naming one rig. Undeclared surfaces pass through
rather than silently emptying. Schema validates the mapping at load, so an
authoring error teaches instead of comparing nothing.
WHAT #10 NOW PROVES: ps and queue name the same rig set — a qitem addressed to a
rig ps does not know, or a rig whose queue work is invisible, fails the run.
FALSIFIED, not assumed: plucking rigId instead of name makes the sets disagree
(ULID vs scn-baton) and the scenario FAILS, so the comparison is value-sensitive
and both operands are non-empty. The scenario also seeds a real qitem — an
all-empty comparison would pass while proving nothing.
WHAT IT DOES NOT PROVE, measured at source and stated in the fixture: tui_socket
is asserted separately on its own coherence and deliberately NOT in the equality.
Both shipped OBSERVE verbs return UI/registry state (state -> {screen, drill,
viewTab}; commands -> the command registry), so the socket carries no daemon
truth; folding it in could only pass by comparing something trivially equal. The
gap is a TUI finding — the state query would need to expose the rig/queue view it
renders.
3/3 scenarios PASS against the real runtime. Matrix 170/170 across 21 files,
daemon tsc clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Two structural changes to the conventions SSOT, both aimed at one pathology:
every slice, however small, was loading the full ceremony, so a one-line
documentation change could draw a multi-round gate.
1. PART A / PART B. Part A — artifact conventions, proportionality, the
advisory audit, the honesty rails — applies ALWAYS and is the default.
Part B — proof-contract format, the two locks, C1 drops, locked role
contracts — applies ONLY when the founder (or an orchestrator relaying the
founder) assigns the heavy path to a NAMED piece of work, and may not be
self-selected. The two parts map to the two skills: mission-slice-sop and
mission-slice-intent-proof-sop.
2. THE INNER/OUTER LOOP SPLIT. The parent host's SDLC was being applied
wholesale to the VM. The VM's inner loop answers "does it actually work?"
and is verified by running it; the host's outer loop answers "does what was
delivered match what was intended?" and carries the release ceremony.
The rule that makes it self-enforcing: the proof ceremony is a protocol for
transmitting confidence to someone who was NOT THERE. On the host that is
the real situation. On the VM the builder is there and ran the thing, so
the ceremony re-encodes what is already known.
Placement rule: work goes where its validation is POSSIBLE — a fix needing
a live daemon to verify belongs on the VM; one verifiable by reading can
happen on the host.
The two honesty rails (public-repo surfaces, cited-hash coverage) moved into
Part A deliberately: they are general engineering rules and must not be gated
behind an assignment.
No content removed — the same material is reorganized and re-scoped.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
`git status --porcelain` counts untracked files repo-wide, so ordinary scratch
(.mcp.json, workspace/, scratch dirs) stamped every package build on a working
machine as dirty=true. A dirty stamp is unidentifiable, which defeats the point
of pinning a runtime to a commit — and this VM deliberately runs several pinned
runtimes side by side, so identifiability is load-bearing, not cosmetic.
Measured: HEAD e853a93db with a clean TRACKED tree still produced
"Build identity: 0.5.0 (e853a93db...) dirty=true", caused solely by two
untracked scratch entries that cannot affect the build output.
Untracked files are NOT blanket-ignored -- a new untracked .ts under
packages/*/src really would compile in, so `--untracked-files=no` would be the
wrong fix. The check is instead SCOPED to the build inputs: packages/ and
scripts/, tracked or untracked. scripts/ counts because build-package.sh is
itself the builder.
Verified both directions: clean tree with scratch -> dirty=false; an untracked
.ts under packages/daemon/src -> dirty=true.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
orch-advisor ratified the EVIDENCE and refused the READING. The file called
daemon-restart 'the faithful, v1-green reading of #2'; the locked format forbids
exactly that substitution (PRD line 20 — the seat-level restart verb and
daemon:{op} are distinct, neither routes through the other), and the arch
acceptance row names a SEAT restart plus a seat pane observable.
What the scenario proves is STRICTLY HARDER than the locked ask, which is why it
stands as built: a seat relaunch never touches daemon state, so the locked form
tests baton survival against a process that does not own the baton, while this
kills and re-spawns the process that owns the SQLite. What it does NOT prove —
that a relaunched seat re-attaches to its claimed baton, and the pane 'restored'
observable — is now stated as an untested, distinct failure mode rather than
implied by a 'faithful reading' label.
Also records WHY the other half is deferred: no snapshot-free per-seat
kill+relaunch verb ships, so the locked #2 is not expressible with the current
verb set. That falsifies the arch table's 'expressible today' mark for row 2 and
is routed to PM as a 0.5.1 finding.
Comment-only in a fixture; scenario-pipeline 10/10 unchanged. Made under the
desk ruling, deliberately OUTSIDE dev50-guard's pre-edit CLEAR (which scoped to
the src comment and excluded test changes) — separated so the boundary stays
legible rather than folded in silently.
Co-Authored-By: Claude Fable 5 <[email protected]>
The field comment promised that an absent outbox still wakes 'best-effort via
maybeNudge, exactly as pre-W1'. False for the two verbs that sentence names:
handoff and handoff-and-complete run assertTerminalClosureHasIntent as the last
statement of their transaction (:1282, :1435) and THROW
wake_intent_store_unavailable when a wake is intended with no store attached.
The maybeNudge fallback survives only on deliverWakeForSuccessor, which P34 made
real code rather than a promise in a comment.
A future editor trusting that sentence would reintroduce the executed-but-unwoken
item W1 exists to make unwritable — the claim outlived its warrant while the
warrant was replaced underneath it.
Comment-only: zero non-comment lines changed. Existing MF2 and P34 tests already
pin both sides. Gate: daemon tsc clean, daemon build ok, queue/W1 suites 82/82,
and a stale-claim grep showing no surviving pre-W1-fallback promise anywhere in
src. Closes the docs half of the W1 follow-up (dev50-guard's cold-parked row);
its second defect was already resolved by P34 in the opposite direction.
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard fresh gate at 6f53ede1 confirmed the original four closed, and found three
more. Two were mine to build; one was a real defect in my own pin.
(1) D8's approved PASS-twice secondary leg was absent. My own rev-2 contract
named it beside the stable-FAIL discriminator; only the FAIL leg existed. Added
through runScenarioFile under the same injected clock, asserting verdict and
normalized run-record stability plus clock arrival on both runs. The FAIL leg
proves the observed bytes are stable; this proves the everyday GREEN rerun is.
(2) The D5 integration pin WROTE MANAGED SEAT FILES INTO THE SOURCE TREE. It ran
real `up` on a topology whose seats declare cwd:. with no staged cwd and no
--cwd, so the daemon resolved seat cwd into packages/daemon and the seats wrote
AGENTS.md and .openrig/stub/** there. A pin that proves tmux-server isolation
while corrupting the owner tree is not a hermeticity pin — it asserted the
property in one dimension and violated it in another. Now stages per-seat
scaffold cwds with this slice's OWN staging helper, and adds an EFFECT PIN that
the source tree receives no managed files (asserted before AND during the live
run) while the seats' sidecar is proven present in the scaffold cwd. The live
two-direction sentinel check is unchanged.
(3) Root PROOF.md still bound contract rows to pre-restack captures. Repointed
every row to exact-SHA evidence; prior drops kept append-only and labelled
historical, so no row carries evidence across the restack.
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard NOT-CLEAR at dbbae216 named four claims whose evidence did not reach the
thing claimed. All four confirmed at source before fixing; all four were mine.
F1 transcript never reached JSON mode. The reader emitted `--tail --json`, but
`--tail <lines>` takes a REQUIRED value, so Commander consumed "--json" AS the
tail value — proven by discriminator: {"tail":"--json"}, json=false. The
containsMatch unit pin asserted on a hand-written payload and never crossed the
reader/CLI boundary, so "D11 covers transcript" was false. Reader now passes an
explicit tail count via an exported transcriptReadArgv, and a live-daemon test
drives the REAL CLI with the reader's own argv, asserting stdout PARSES as JSON
— with a negative control proving the old argv shape does not.
F2 D8's two runs never received the injected clock: runScenarioFile had no clock
option at all, so both used wall-clock defaults while the test named an
INJECTED_CLOCK it only used elsewhere. Threaded injectClockNow through the
pipeline into the scaffold, and pinned ARRIVAL at the caller boundary — the fake
rig records what the CHILD actually got. The fake daemon now derives readEnv
from the scaffold exactly as the real spawner does; a fake fabricating its own
env is how the dropped clock stayed invisible.
F3 D7's failure matrix was helper-only. Delayed readiness, early exit and
never-listens now each cross runScenarioFile, proving the distinct named reason
AND daemon teardown on every path (new tuiReadiness seam bounds them).
F4 the ambient-tmux test could not detect the contamination it excluded — and a
FALSIFICATION proved it: with the TMUX_TMPDIR replacement disabled it still
passed, because the scenario ends in `down` and cleans up after itself, so a
post-run name snapshot is unchanged either way. Rewritten to observe WHILE SEATS
ARE ALIVE and assert BOTH directions: seats are on the scaffold-owned server,
the inherited sentinel server gains nothing. Re-falsified: disabling the
replacement now fails the pin (at its first leg).
Matrix 161/161 across 20 files; daemon tsc clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
D7: the tui_socket surface reads a control socket that exists only inside a
running TUI, and readTuiSocket rejects on connect error while the expect poller
does not catch observation errors — so 'spawn then proceed' made #10 race-prone.
provisionTui waits boundedly for a real `state` ROUND-TRIP (not mere socket
existence — a listening socket answering non-JSON is not a working TUI), detects
EARLY EXIT and reports it as early_exit rather than waiting out the bound for
what is really a crash, and tears down on success and every failure path. Wired
through runScenarioFile behind env.tui, spawning into the scaffold's own tmux
server on a scaffold socket path. Pins: delayed readiness proceeds; early exit
and never-listens both fail named with teardown proven; opt-in only.
D8: guard rejected the first shape as vacuous — PASS rows carry only name and
verdict, so two passing runs prove nothing. The discriminator now runs a
deliberately STABLE-FAILING expect whose DIFF embeds the observed value, and
compares verdict/failedStep/diff/records across runs with the excluded
runner-local field NAMED (durationMs), not blanket-stripped. Second pin gives it
teeth: change ONE observed byte and the comparison fails.
D5 p3: a REAL scenario run leaves the ambient tmux dir UNCHANGED (not absent —
the fleet legitimately has a server). Also isolated D5 p1's pre-effect
measurement to a private TMPDIR: counting shared /tmp scaffolds was a
self-induced contention flake under the parallel matrix, caught by an A/B rerun
rather than assumed.
Full matrix 157/157 across 20 files (uncontended AND parallel); daemon tsc clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
D2: emit's UnboundActionError now states the MEASURED reason and the working
path. The stub runner has no input channel — no stdin reader, no socket, no
CLI/daemon route triggers a behavior; behaviors run only from the launch script
read at boot. So a step-time emit cannot be honestly bound without 51-01 source
work (routed), and simulating one would be a green that means nothing. The
message names env.stub_scripts as the v1 path instead of leaving an author
stuck at 'unbound'.
D3: guard required the COMPOSED pin, not just a helper argv spy — a helper test
passes while the pipeline silently drops the field. Drives runScenarioFile end
to end against a stand-in rig bin that records real argv, asserting
`scope audit --mission release-0.5.1 --json` arrives from the scenario's
env.scope_mission.
Pipeline 10/10, real-deps 10/10, daemon tsc clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
D1 closes mini-req 5 (scenario-resolved scripts delivered at up). The shipped
stub reads <cwd>/.openrig/stub/script.json, so distinct scripts need distinct
seat CWDs — and `rig up --cwd` cannot express that: resolveLaunchCwd makes the
override win for EVERY seat. So stage instead: copy the topology's SOURCE
DIRECTORY (guard rev-3 — a lone YAML rebases the spec root and orphans the
relative culture_file + local: agent_ref), author an absolute per-seat cwd in
the staged copy, deliver each mapped seat's script into its own cwd. Committed
fixtures are never written.
Key contract (loud, pre-effect): every env.stub_scripts key resolves to exactly
one runtime:stub member — unknown, ambiguous, duplicate-alias and non-stub all
throw StubScriptTargetError BEFORE the scaffold or any spawn, so a misspelled
seat can never silently run the default while its script lands in a dead dir.
Scripts validate through the SHIPPED parser. Container mode: scripted scenarios
refuse by name (routed to 51-04); unscripted keeps its stage path untouched.
Equality pinned SEMANTICALLY (parse/serialize cannot promise bytes).
D11, found BY the D1 e2e: containsMatch required a bare string, but both
shipped text reads return objects carrying text in `content` (capture:
{ok,sessionName,content,lines}; transcript: {session,lines,content,...}) — so
the contains mode could never match the two surfaces it exists for. Match the
named field only; a blind stringify would false-green on sessionName. Uncaught
because no landed scenario asserted on a real pane (#2's pane leg is deferred,
#10 uses equals) — the missing-tail class again.
Evidence: new e2e scenario-01-per-seat-scripts PASSES 1/1 against a real
3-stub-seat rig — R1 relative closure resolves after staging, R2 two seats run
only their own script, R3 the unmapped seat runs 51-01's built-in default.
Full matrix 144/144 (18 files); daemon tsc clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
A scenario `up` stands up REAL tmux seats. The helper documented 'set
TMUX_TMPDIR manually' — a loaded gun in a harness that calls up: seats land on
the operator's server, where a stray kill-server reaps the live fleet.
Own it like the daemon target and HOME. New hazard CATEGORY (accurate refusal
message, distinct from daemon-target and clock): TMUX_ATTACHMENT_ENV_VARS,
detectAmbientTmuxHazard, assertNoAmbientTmux, AmbientTmuxHazardError. A TMUX
attachment refuses BEFORE any filesystem effect; TMUX_TMPDIR is not a refusal
trigger (a dir, not an attachment) — the scaffold overwrites it with an owned,
cleanup-removed dir, kept short for the sun_path cap.
Pins (guard precision): refusal is pre-effect with no scaffold dir created;
inherited TMUX_TMPDIR proven ABSENT and REPLACED as independent assertions;
short-socket-path budget. The e2e ambient-dir-UNCHANGED pin rides the D7/D8
e2e increment.
run-scenarios.mjs drops the manual-TMUX_TMPDIR instruction (its basis moved
into the helper) and its normalizer note now cites A-N1.
Hermetic suites 21/21.
Co-Authored-By: Claude Fable 5 <[email protected]>
P21 I3 (081c1c930, 08-07) retired queue create --source: deprecated and
IGNORED, the sender derived from the transport header. The harness landed
08-06 00:15 still passing --source, so every precondition write broke the NEXT
DAY and stayed broken five days — the e2e was never re-run because the slice's
proof tail never happened.
applyQueuePreconditions now carries the fixture's DECLARED provenance as the
per-call env identity (OPENRIG_SESSION_NAME: creator=source, claimant=
destination) and drops the dead flag from argv — an ignored flag is a stale
claim in the harness. The daemon target in readEnv is preserved. Fixture
comments trued in the same diff (they documented the retired form).
Pin asserts the env identity per call AND the ABSENCE of --source.
scenario-02-baton now passes end-to-end for the first time: 1/1 under an
isolated TMUX_TMPDIR. Pipeline suite 9/9.
Co-Authored-By: Claude Fable 5 <[email protected]>
D4: proof leaves EXPECT_SURFACES for RESERVED_SURFACES; the validator rejects
surface:proof with a teaching error citing the PM ruling (a80735bc — no shipped
read verb; re-enters by un-reserving). Reader keeps a defense-in-depth unbound
leg for a runtime-smuggled value; stale FLAG-1 comments trued.
D1 (shape half): env.stub_scripts is a seat→path map, per-entry validated; the
key contract vs the topology binds at the pipeline (next increment).
D3 (schema half): a scope expect requires env.scope_mission — the shipped read
is `rig scope audit --mission <name> --json` (requiredOption); reader +
real-deps thread scopeMission through observe.
D7 (schema half): env.tui boolean opt-in; a tui_socket expect without it is a
load-time teaching error (converts the socket-race class into a named
validation failure). Both #10 scenario files true up with env.tui — including
the smallest ruled-format true-up to 51-03's corpus copy (named here per the
guard-cleared boundary).
Full matrix 122/122 green (16 files incl. twin-drift guard); daemon tsc clean.
Guard pre-edit CLEAR fe30bd50; lock at A-N1 pins 3c698b61/2e62eba4.
Co-Authored-By: Claude Fable 5 <[email protected]>
Ship-block ruling (qitem-20260811170941-5eadb968, promoted from my own DR-2 carry-forward note):
classifyNudgeFailure defaulted UNRECOGNIZED failures to `transient`, which asserts "a live seat
refused this attempt" for a string that only means "did not match the permanent pattern" — two
different claims. Honest labeling IS this slice's deliverable, and the product ruled the identical
question tonight (ACTIVITY hookless=unknown: the fix must not relabel the default).
Make all three classes POSITIVE matches: permanent-topology (not found), transient (interactive
prompt / timeout), else `unknown` — never a silent default. RED assertion added: an unrecognized
failure text is NEITHER known class (fires if anyone re-collapses the default later). queue-repository
+ queue-routes 124/124, tsc 0 both.
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
PM ruling (qitem-20260811163927-74493d76): the live corpus (9/10 strands to a seat on ANOTHER
daemon, "Session not found") is an ADDRESSING defect, not a retry problem — retrying is guaranteed-
permanent failure on a schedule. Fold the classifier into DR-1 (not a second slice) so the surfaced
count becomes ACTIONABLE: classifyNudgeFailure() labels each strand permanent-topology (destination
unresolvable on this daemon — route to the addressing family, NOT retry) vs transient (a live seat
refused this attempt — the only class a future DR-2, held n=1, would touch). GET /undelivered enriches
each item with deliveryFailureClass. READ-side labeling only; acts on nothing.
Also adds the `queue undelivered` cli contract tests (review-r1's non-blocking note). RED-first
(classifier unit + route deliveryFailureClass); queue-repository+queue-routes 124/124, slice15 16/16,
tsc 0 both. DR-2 retry stays HELD (n=1, named re-open conditions).
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
last_nudge_result is written (recordNudgeAttempt, from maybeNudge) but nothing ever
queries it, so a create whose nudge FAILED (failed:<msg>) is invisible: the sender records
a successful create while the destination was never woken. Surface that class via a named,
human/agent-visible read path — findUndelivered() + GET /undelivered + `rig queue undelivered`
(the /overdue analogue for pending-undelivered create-path work).
V1-ONLY, no false-positive mode: state='pending' AND last_nudge_result LIKE 'failed:%'. A
delivered row and a never-attempted (NULL) cold-park are both excluded; the never-attempted
class is FP-dominated (a legit cold-park is indistinguishable from a real never-reached strand
at the row) and stays DEFERRED behind a forward create-time intent bit — the exclusion reasoning
lives in the code + PRD, not chat. READ only: no retry, no unwind (DR-2 is PM-gated on this
surface's live measurement). RED-first; queue-repository + queue-routes 123/123; tsc 0 both pkgs.
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
An orchestrator diagnosing a silent seat could not see what model it was on, so a
Fable rate-limit read as a stall for three hours and produced a wrong-shaped
escalation (reroute-vs-reset) when the cause was neither.
node-inventory already carried both fields; the CLI's compact projection dropped
them. Restored there, plus MODEL on the --full human table where RUNTIME already
lives.
DELIBERATELY NOT in the compact human table: OPR.0.4.0.25 pins RUNTIME as a
full-only header for token economy (with CTX/RESTORE/STARTUP/TERMINAL/POD/MEMBER).
The first attempt added columns there, broke that pin, and truncated the session
cell. The pin is right; the JSON projection is the surface tooling reads anyway.
A blank model renders 'not-declared', never the em-dash every other empty cell
uses — 13 of 15 claude-code seats carry no declared model, and blank-as-em-dash
would read as 'this seat has no model'.
SCOPE, stated plainly: this is the SPEC-DECLARED model, NOT the running one, and
it does NOT close the founder's ask. The running model has no producer today —
that is the ACTIVITY-umbrella rider (running-model-per-seat), same shape as P40
hook-generation: a fact the seat knows and the daemon does not.
Document the daemon contract at the CLI surface: --blocked-on now accepts a typed non-qitem gate
(fold:/auth:/external:) alongside a live qitem id or a human seat (Atom 1a); --closure-reason notes
`superseded` on state=canceled (with --closure-target = the successor) records a supersession distinct
from an abandoned cancel (Atom 2a). Help-text only; no env facts in shipped strings (R7). cli tsc 0.
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Atom 2a added `superseded` to CLOSURE_REASONS (6 -> 7). The endpoint's validReasons test
pinned a magic length(6); assert against the canonical CLOSURE_REASONS set instead so it
self-updates with the contract rather than false-flagging a legitimate reason addition.
Test-only. The 5 scenario-daemon integration failures in the full-suite run were the retired
unbuilt-cli-dist caveat (build cli dist -> 9/9); not a code regression.
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A cancel-and-replace must not leave an unlinked orphan pair. Plain create() did not
expose handed_off_from (only handoff-and-complete set it), so a supersession successor
had no back-link to the row it replaced. Add handedOffFrom to QueueCreateInput and both
create INSERT variants: the successor records handed_off_from = the original.
Combined with 2a's forward link (original.closure_target = successor), a supersession is
traversable in BOTH directions — successor->original (handedOffFrom) and original->successor
(closureTarget) — and never reads as abandonment. Completes Atom 2 (metadata-correction honesty),
orch-lead 1e63c084's unblock. RED-first. 11 suites 227/227; tsc 0.
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A row corrected by cancel-and-replace could not record that it was SUPERSEDED: the
closure-coherence whitelist admits closure_reason/target only on done / park-record /
handoff-close, so state=canceled + a reason was refused (closure_fields_not_admitted).
Result: a superseded row (closure_reason=null) was indistinguishable from an abandoned one.
Add a FOURTH admitted form: state=canceled + closure_reason=superseded + closure_target=
<successor>. A plain cancel stays abandoned (closure_reason=null). superseded WITHOUT a
successor target is refused LOUD (missing_closure_target) BEFORE any write — never a silent
no-op leaving a stale row (the dead-signal class orch-advisor hit tonight).
RED-first (admitted+recorded / abandoned-null / loud-missing-target-no-mutation).
9 suites 212/212 (incl. hot-potato-enforcer / CLOSURE_REASONS); tsc 0.
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A park may be gated on a fold/auth/external condition that is not a qitem and not a
human seat. Admit `fold:` / `auth:` / `external:` as first-class blockers: compact-visible
(blocked_on is carried in the compact projection), settable at the PARK moment (which the
discovered-after-creation asymmetry demands), and downstream-classifiable — the ruling detail
rides a transition. A well-formed typed blocker is accepted; a bare prefix with no gate body
is refused loud (blocker_malformed) so a typo cannot masquerade as a gate.
Exports isTypedGateBlocker / typedGateBlockerPrefix for the downstream park detector.
Also consolidates the blocker "live" predicate: replaces the two inline terminal-state lists
(Atom 1b-ii/1b-iii) with one named, derived isBlockerLive (ACTIVE_QUEUE_STATES complement),
DISTINCT from the archiver's narrower TERMINAL_QUEUE_STATES per the name-terminal-sets-once
doctrine (OPR.0.4.6.FS-1). RED-first (malformed reject). queue-repository 52/52; tsc 0;
16 blocked_on+event+retention suites 300/300.
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
blocked_on promises "A waits until B completes"; that promise never fired on this
runtime, so rows sat blocked on done/canceled blockers for days. When a qitem reaches
a terminal state, auto-unpark every row parked on it (blocked_on=this, state=blocked)
to pending, clear the resolved blocker, log the transition, and emit queue.updated so
watchers/sweeps see the unblock.
Completes Atom 1b (blocked_on honesty): clear-on-exit + validate-at-park + propagate-
completion. RED-first. queue-repository 50/50; 15 blocked_on+event suites 284/284.
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A park on a QITEM-reference blocker must name a real, LIVE blocker. A nonexistent
blocker can never complete (the row can never unpark) and a terminal one is already
done — both are dead-blocker parks that read as patience and never self-clear (the
21-22 day strands; the desk sat on one for hours).
Scoped to qitem-refs ("qitem-…") so the existing corpus is untouched: human-seat parks
(isHumanPark) keep their own contract and bare gate-names are the Atom 1a typed-blocker
surface. Nonexistent → blocker_not_found; terminal → blocker_not_live. RED-first (both
cases). queue-repository 49/49; 12 blocked_on suites 239/239.
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Root cause of the dead-blocker strand: the update() SET wrote
`blocked_on = COALESCE(?, blocked_on)`, so any transition OUT of blocked
(input.blockedOn = null) PRESERVED the old blocker. A non-blocked row then
carried a stale blocker that nothing audits — rows sat urgent+blocked for
21-22 days on ghost/done blockers; the desk sat on one for hours.
Set blocked_on EXPLICITLY: keep it only while state=blocked (effectiveBlockedOn),
clear to NULL on every exit. RED-first (unpark leaves blocked_on = the blocker at
base; NULL after). Full queue-repository suite 47/47.
Atom 1 of slice 53 (queue post-create metadata & park honesty), base 6045ea62.
Own inner loop; full-breadth daemon gate + ship review at slice end. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A rig spec is consumed as authoritative by everything that RECREATES a rig and
is maintained by nothing that MUTATES one. `rig expand` adds a pod to a running
rig through the daemon; no code path writes the rigRoot spec back, so drift is
not an accident to be prevented — with no writer it is guaranteed.
That would be tolerable if it were visible. It is not. `bundle-assembler` reads
the spec from the FILESYSTEM and copies it VERBATIM into the bundle, never
consulting the live DB, so a rig that has been expanded exports as the smaller,
stale topology in silence. On this host: the spec declares 3 pods/8 seats, the
rig runs 5/14, and a bundle export today would drop dev50 and review50 — six
seats — with no error, because the spec is internally consistent and validates
clean. `rig validate` returns pass.
This is REPORTING ONLY. It mutates no spec and blocks no export.
- `spec-live-conformance.ts`: the comparator, plus extractors for the spec side
and the live side. Shared through a narrow `@openrig/daemon/spec-conformance`
surface so `rig doctor` and the export path answer with the SAME delta — two
surfaces disagreeing about the size of a rig is the defect one layer up from
the one being reported here.
- bundle export: warns on the response and the log, naming the actual delta
("pods running but ABSENT FROM THE SPEC: dev50, review50"). Silent when the
spec's rig is not running, since authoring a NEW rig is not drift, and it
never throws into the export path.
- `rig doctor --spec <path>`: pass / warn / skip. This is also how a later spec
write-back gets VERIFIED rather than eyeballed.
`--spec` is explicit because the path CANNOT be discovered: nothing persists a
running rig's spec location. The `rigs` table has no spec/rigRoot column,
`rig_services.rig_root` is empty, and `projection_manifest.source_spec` is
empty. The daemon does not remember which file described the rig it is running.
Absent the flag the check SKIPS WITH THAT REASON rather than passing — a check
that cannot find its subject and stays quiet is indistinguishable from one that
looked and found nothing wrong. The same rule governs an unreachable daemon.
The message names the real delta rather than a generic caution: a warning on
every export trains its reader to skim, and then the one export that matters
reads like the ninety-six before it. `message` is non-null EXACTLY when the
topologies disagree, pinned as an invariant.
RED-first, and the RED verified rather than assumed: the doctor tests were first
red only because of a broken fixture, so after fixing it I removed the wiring
line, confirmed 5/5 failed, restored it, and confirmed 5/5 passed.
Live proof on this rig: warns with the real delta (5/14 vs 3/8, naming dev50 and
review50); a generated matching spec PASSES silently as the negative control;
no --spec SKIPS with the reason.
Daemon suite 544 files / 7299 passed. CLI suite 165 files / 2265 passed
(hermetic env). tsc clean in both packages.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Desk re-gate found ONE survivor (client.ts:19, the header-chokepoint doc-block). Applying the desk's own
instrument — dumb-wide `refus|unattributable|identity_mismatch|401|409` + READ + classify, run rigorously
with a 401/409-in-comments sub-pass — found THREE MORE of the same class the desk's "one survivor" read
missed. Comment-only, zero production logic:
- cli/src/client.ts:19 (desk's find) — senderIdentityHeaders() doc-block, "refuses-unattributable LOUD"
→ DELIVERS + claimed:v1; no-actor → 400 actor_required.
- daemon/src/routes/review.ts:143 — resolveActorWithDeferral "409-on-mismatch" → wire supersedes (409
retired, ruling A). A 5th phrasing.
- cli/src/mcp-server.ts:386 — "a hardcoded 'mcp' would 409" → would be SUPERSEDED (transport:v1). 6th phrasing.
- cli/src/commands/chatroom.ts:63 — "a hardcoded 'cli' would 409 against the header" → SUPERSEDED. Same.
The recursion the desk predicted: four censuses, five+ phrasings, each instrument matching the FORM its
author pictured. Terminated here by reading the raw hits, not a cleverer pattern. Final passes CLEAN across
daemon+cli src: PASS-A (unattributable/identity_mismatch/refuse-loud/would-409), PASS-B (401/409-in-comments),
token-free (reject/deny/turn-away near sender) — only legit keepers remain (transport.ts boundary,
require-sender-identity.ts historical, other-subsystem identity_mismatch, unrelated 409-conflict routes,
token auth, Slack registration, my correct deliver-and-label prose).
daemon tsc 0, cli tsc 0. Zero overlap w/ main's ACTIVITY 5 files. Branch-only. NOT guard-accepted. Never pushed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Desk gate held the fold on record-truth: the class was not exhausted (the guard's census was diff-scoped;
a deletion atom invalidates prose source-wide, in files the deletion never touches). Comment/STRING-only,
zero production logic. Corrects every remaining site that teaches the retired 401/409 refusal as current:
SHIPPED user-facing string (worst):
- cli/src/commands/scope.ts:1677 consequence + :1673 comment — "daemon refuses 401 unattributable" →
400 actor_required (parameter completeness, not distrust).
Desk-named + in-footprint:
- daemon/src/routes/queue.ts:412,546 — "401 unattributable / 409 identity_mismatch" → deliver-and-label
(claimed:v1 header-absent; wire supersedes mismatch; 400 actor_required when no actor).
- daemon/src/routes/scope-approve.ts:32-33 — same.
- cli/src/commands/queue.ts:386,450,467,503,545,583,639,734,1116 — nine "daemon refuses-loud 401" →
400 actor_required. (545 was NOT in the desk's list — found by exhausting the class.)
Source-wide census caught 4 MORE beyond the desk's 14 (its census was source-wide yet still under-counted):
- daemon/src/routes/chat.ts:27,133 — chat uses requireSenderIdentity; "refuse-loud default" → deliver-and-label.
- daemon/src/routes/mission-control.ts:341 — asserted the RETIRED 409 as current → wire-supersedes.
- cli/src/commands/queue.ts:583 — a line-wrapped continuation my first phrase-match missed.
Re-census source-wide: CLEAN (only legit keepers remain: transport.ts = the --dangerously-interact
override boundary; require-sender-identity.ts = the explicit "former 409 RETIRED" historical note).
No test pins the changed shipped strings. Verify: daemon tsc 0, cli tsc 0, focused cli 208 + daemon 32 green.
Zero overlap with main's ACTIVITY 5 files (rebase-safe). Branch-only. NOT guard-accepted. Never pushed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
dev-guard NOT-CLEAR verdict on 6a6dbda2d (behaviorally green + accepted; comment/title-only fixes).
No behavior change. Corrects four statements that still taught the DELETED refusal contract as current:
1. require-sender-identity.ts (resolveActorWithDeferral doc): "409-on-mismatch" → wire-supersedes
(409 retired, ruling A); "refuse-loud default" → the two helpers now SHARE deliver-and-label,
this one's remaining distinction is surface scope, not refusal behavior.
2. send.test.ts: beforeEach comment (guard-refuses → deliver-and-label fixture rationale); line-194
test title ("the guard admits an attributable send" → "SENDS with its attributed identity").
3. cross-host-http.test.ts: beforeEach comment (attributable-only/guard-refuses → resolved-seat-renders-
real-sender; env-less would deliver-and-label).
4. cross-host-commands.test.ts: both comments — the SSH `--from` argv is the P23-D1 skew-compat shim for
pre-I4 remotes (PM ruled removal to P23-D1, qitem-20260811055900-f5920c0e), NOT a boundary guard;
env-less origin omits --from. Behavior/assertions unchanged.
Verify: daemon tsc 0, cli tsc 0, 7 focused CLI files 200/200 (env-scrubbed), daemon p18-thread 4/4.
Branch-only. NOT guard-accepted. Never pushed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
CLI half (REVERSES A1 6634b2852): requireAttributableSender refusal DELETED; the
`<unknown sender>` twin REBUILT as a SINGLE CLI origin (sender-identity.ts SENDER_FALLBACK,
imported by send.ts + broadcast.ts) so an env-less send/broadcast DELIVERS with the honest
marker + null actorSession — never refused, never forged. Literal now at EXACTLY the two named
twin sites: cli/src/sender-identity.ts + daemon/src/lib/pane-envelope.ts.
Guard rework (send.test.ts): the A1 "exactly ONE (because A1 deleted the CLI ones)" canonicity
guard reworked to EXACT-SET membership naming BOTH twins — a 3rd definition fails BY NAME (not a
count/upper bound); negative control RUN AND WATCHED RED (injected a 3rd def, observed the failure
name send.ts, removed it); guard comment rewritten; repoRoot made cwd-INDEPENDENT (fileURLToPath),
proven GREEN from packages/cli AND repo root. Sibling A4 grep-guard (client-headers.test.ts)
hardened: same cwd fix + a non-empty operand guard so clause 1's `.toEqual([])` can't vacuously pass.
Sweep (assume-more): flipped broadcast A1 refuse→deliver twin; RESTORED positive fallback tests in
send-header.test.ts + self-host-triple.test.ts (the deleted-refusal path re-created must be tested);
fixed stale A1 comments in cross-host-http.test.ts. Named-5 daemon guards checked (2 unrelated,
review-freeze already swept). ui = NO CHANGE (checkpoint's identity_mismatch was process_identity_mismatch,
a different subsystem — verified at source).
R7 subtraction (require-sender-identity.ts header): deleted this-environment rationale
(no-attacker / trust-domain-only-caller / incident-narrative / not-an-adversary-boundary); kept the
mechanism + the software-property distinction (cannot-verify ≠ illegitimate). Full-diff R7 grep clean
(pattern verified to have teeth pre-fix).
Branch-only. NOT guard-accepted. Never pushed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Flip the ~29 tests that asserted the retired identity-path refusals to the
swept behavior. Verified: 8 touched files 161/161 green in isolation.
- 401 unattributable_sender (header absent) → now delivers as claimed:v1
(body claim present), asserting the recorded provenance is claimed:v1 (R3);
inbox/drop → 400 actor_required (no body sender read).
- 409 identity_mismatch (header + differing body) → wire supersedes: delivers
under the wire identity at transport:v1, body claim ignored. Covers both
requireSenderIdentity sites AND resolveActorWithDeferral sites (files/write,
mission-control action, review/freeze) since both 409s were retired.
- scope-reapprove P21 I1: re-stamps need reApprove+reason; asserts on the
frontmatter provenance (unambiguous latest) rather than audit-row order.
Full daemon suite run pending. Never push. Base 9cecb5be6.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Daemon half of the P18 identity-refusal sweep. NOT yet dev-guard-accepted;
WIP commit to protect load-bearing work before the test-maintenance phase.
- 11 hardcoded identityProvenance:"transport:v1" sites now thread
resolveRecordedProvenance(c, identity): queue.ts x9 (create/claim/unclaim/
update/handoff/h-a-c/inbox-drop/inbox-absorb/outbox), scope-approve:58,
stream:85. Header-absent+body-claim now records claimed:v1, not laundered
to transport:v1. RED test p18-provenance-thread.test.ts GREEN (4/4).
- inbox/drop: inline 401 -> requireSenderIdentity; no body sender read, so
header-absent = 400 actor_required (disposition (a)). Behavior change 401->400.
- 409 identity_mismatch retired in BOTH requireSenderIdentity AND the sibling
resolveActorWithDeferral (ruling A + actor-to-label test): wire supersedes
body claim, deliver under transport identity, no persistence.
- daemon tsc 0. Daemon test-maintenance (~29 refusal-asserting tests) PENDING.
Never push. Base 9dc87e1db (dev50-driver checkpoint).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
P18 deletion atom (qitem-20260811040528-21d01c1a), founder-ruled deliver-and-label.
PARTIAL — checkpointed deliberately at a named capacity boundary, not finished.
DONE in this commit — the 401 half only:
- 401 unattributable_sender DELETED. A missing X-OpenRig-Session means an unmanaged
terminal, not an attacker. Header absent + a body-declared actor now DELIVERS and
records the weaker era-stamp claimed:v1.
- 400 actor_required RETAINED for neither-header-nor-body-actor: there is no actor to
attribute the row to. Parameter completeness, not distrust — the same shape PM ruled
a deliberate keep at queue.ts:215.
- requireSenderIdentity now returns { session, provenance } on the ok path, extending
the pattern resolveActorWithDeferral and resolveRecordedProvenance already implement
rather than inventing a mechanism.
409 identity_mismatch STANDS in this code, deliberately. PM ruled both refusals die,
ONE ATOM EACH: certified-but-body-disagreeing is noise to be superseded, not an attack
to be refused, and the incident transport matrix showed it firing on honest-but-skewed
clients during an outage. Split off because taking it here drags 12 test files including
the ui package into a light-path atom.
⚠ NOT SAFE TO SHIP AS-IS — see the sealed finding, sha256 7c85c24f…:
Eleven sites hardcode identityProvenance:"transport:v1" (queue.ts x9, scope-approve.ts:58,
stream.ts:85). They were CORRECT BY CONSTRUCTION while the helper refused — the header was
guaranteed present. This commit removes that warrant while the claim stays, so a headerless
caller is delivered as claimed:v1 and then stamped transport:v1: unverified laundered into
verified, forbidden in resolveRecordedProvenance's own words. tsc is 0 and every existing
test passes with the defect present, because deleting a refusal manufactures an execution
path no existing test could have covered. Threading resolveRecordedProvenance through those
11 sites is the next task and it is NOT in this commit.
Branch-only, never pushed. Base 60ee1e57b.
dev50-guard HOLD on 69d2a5e90, comment-only. Zero non-comment lines differ.
The previous wording justified the ACTIVITY/TERMINAL divergence by asserting
that "only one of these two surfaces makes an affirmative liveness claim that a
reader will act on". That is false, and verified false at source:
`tui/src/topology/glyphs.ts:45` computes
`activity === "running" || data.terminalActive === true`, and `tui/src/types.ts`
filters on `terminalActive === false`. TERMINAL drives shipped TUI and CLI
decisions directly.
Worse for the claim than a bare inaccuracy: because the TUI ORs the two, a stale
cached `true` still paints "working" there even with read-time aging in place.
The sentence did not merely overstate ACTIVITY's role, it implied a containment
that does not exist.
The comment now describes ACTIVITY's independent re-aging and warns against
reconciling the gap, without characterising TERMINAL at all. Ranking the two
surfaces was never needed to make the point.
No behavior change; the TUI's OR is pre-existing slice-15 behavior and is not
touched here.
Gate 15/15. tsc 0. Six seat-activity-adjacent packs, 113 passed.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
dev50-guard HOLD on 612495510, comment-only. Behavior unchanged: zero
non-comment lines differ.
The sessions-route comment added by this atom said the shared observation meant
ACTIVITY and TERMINAL "can no longer disagree about whether a seat is producing
output". That was true of the first candidate and FALSE the moment read-time
aging landed: ACTIVITY re-ages the raw `lastActivityAt` against the request
clock while TERMINAL projects the `isActiveWithinWindow` computed at poll time.
When a tmux read has been failing, `pollSeat` leaves the last record in place,
so the cached boolean can stay `true` after the pane went quiet — TERMINAL keeps
reporting that stale `true` and ACTIVITY refuses it.
That divergence is the stale-cache protection working. Only one of the two
surfaces makes an affirmative liveness claim a reader acts on, and it is the one
that re-checks. The comment now says so, so the next reader does not "reconcile"
the gap and delete the protection.
The graph-route comment is corrected on the same pass rather than left for a
third round: sharing the observation does not collapse the slice-15
non-inference contract (which is between `terminalActive` and `hasAssignedWork`
— ACTIVITY still reads no queue state), and it points at the sessions route for
why the two surfaces may differ.
This is the third time in this atom that behavior moved and adjacent prose kept
asserting the old property. A stale comment claiming a guarantee that no longer
holds is worse than no comment: it is a false claim the next reader will rely on,
and here it invited exactly the change that would undo the fix.
Gate 15/15. tsc 0. seat-activity, structural-signal, healthz-wedge,
attach-terminal-activity, ps-projection, startup-wiring: 6 files, 113 passed.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
dev50-guard HOLD on 3d0fda724 (2026-08-11T05:12:46Z), confirmed at source.
`SeatActivityService.pollSeat` returns null on a tmux error BEFORE it replaces
or deletes the cached record (seat-activity-service.ts:74), and
`pollAllRunningTmuxSeats` only evicts seats that are no longer running in the
DB. A seat still marked running whose tmux read keeps failing therefore keeps
its last observation forever — including `isActiveWithinWindow: true`.
The fold trusted that boolean and never aged the raw `lastActivityAt` against
the request clock, so an UNAVAILABLE observation became an affirmative liveness
claim: ACTIVITY would report `running` indefinitely after the instrument went
dark. Guard's discriminator fed an hour-old cached `true` and got
`state: running` out of the classifier.
That is this ladder's own no-fabrication rule broken in the direction nobody
was watching. Three guards already stopped motion inventing `idle` from
silence; none stopped a dead cache inventing `running`. A silent seat and a
seat we can no longer SEE are different states, and only the raw timestamp
tells them apart.
Freshness is now decided at read time from the raw `lastActivityAt` against the
request clock, using the seat's OWN `silenceWindowSeconds` — the same threshold
the service used at poll time, not a second one invented here. It fails CLOSED
when the fact is absent or unparseable, because an un-ageable affirmative is
indistinguishable from a long-dead one. A negative age still reads live,
preserving pollSeat's deliberate clock-skew tolerance.
Six RED-first controls added. The previous nine could not catch this: every one
of them varied the cached boolean and NONE varied observation age independently
of it, so the defect sat on an axis the gate never swept. A9/A10 (hour-old
`true` must not upgrade `unknown`, must not overturn positive `idle`), A11
(un-ageable fails closed), A13 (just inside / just outside the window) were
observed RED before this change; A12 and A14 pin the preserve side so a
correction that simply disabled motion cannot pass.
Gate 15/15. Full daemon suite 7283 passed / 0 failed. tsc 0. seat-activity,
structural-signal, healthz-wedge, attach-terminal-activity, ps-projection and
startup-wiring regressions all green.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
`rig ps` reported ACTIVITY=unknown for every seat on this fleet while the web
UI showed the same seats working. Two causes, one code path in
attachAgentActivity:
D1 (Codex -> unknown) = COVERAGE. A Codex seat has no runtime hook, so the
positive-hook early return never fires and the verdict falls to a
structural matcher that reasons over a fixed 8-line tail window. A tall
footer pushes the real work line above that window, so the matcher misses
and the seat reads unknown.
D2 (Claude -> idle) = PRECEDENCE. A seat that Stopped and then RESUMED still
carries the stale positive `idle` hook as its latest, and that hook was
returned as authoritative before anything else was consulted. Live motion
placed further down the ladder would still have lost to it.
The fix is one precedence rule applied at every exit of the ladder:
needs_input > running > MOTION > idle / unknown
Motion is tmux `#{window_activity}` — a fact about bytes on the pane, already
computed per seat at 1Hz by SeatActivityService and already surfaced in the
separate TERMINAL column. ACTIVITY was the only consumer that never read it.
Because it is a byte-level fact it is runtime-AGNOSTIC, so it covers Codex and
Claude identically instead of needing a matcher per provider reskin.
Motion only ever UPGRADES `idle`/`unknown` to `running`. It never overrides a
`needs_input` verdict (a seat waiting at a prompt keeps redrawing, and
relabelling it `running` would step over the one seat that is actually
blocked), and it never MANUFACTURES `idle` from silence (that would satisfy
"not unknown" by relabelling the default and rot the closed union). A silent
seat's prior verdict is returned exactly as it was.
The UI has folded this same signal under the name `terminal_activity` since
slice 15 (activity-visuals.ts). The daemon now reports under that existing
name rather than minting a second word for one fact — a second vocabulary for
one signal is how the two surfaces came to disagree in the first place.
Gate: packages/daemon/test/activity-motion-fold.test.ts, RED-first. A1 (idle
hook + motion), A2 (Codex coverage) and A8 (the demoted-hook shape this fleet
actually has) were observed failing by name before the fix; the six invariant
assertions passed before and after.
Live canary on the upgraded daemon, sampled DURING the turn rather than after:
a generating Codex seat read `running` for six consecutive 2s samples and
returned to `unknown` when it stopped, while a second Codex seat sitting still
read `unknown` in those same polls as a concurrent in-band negative control.
The structural matcher caught the middle of that turn and missed both EDGES —
motion covered exactly the boundaries the diagnosis predicted.
D2's precedence path is proven at the unit gate with an observed RED but is
UNTESTABLE-ON-THIS-FLEET live: every hook here demotes to unknown for
unresolvable generation, so no seat can produce the generation-RESOLVED
positive idle hook the precedence branch requires. Recorded as a third verdict,
not as a pass.
Full daemon suite 7277 passed / 0 failed, tsc clean.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Founder-ruled unbuild from the over-engineering audit (qitem-20260811020356-64ecb896),
DB disposition ruled at lock level (qitem-20260811020528-369f2e40).
1. W4 compaction-enforcement suite, FULL — commits 56858cbea, d789fbe51, 34fe36d55,
07879b10b, 89fd15bf5, d4236f5bd, df05193e4. Removes the compaction-control CLI, the 3
/api/compaction/control routes, enforcer-decision-store.ts, the human_hold skip reason,
authorize_ttl_minutes (config key + env + settings), and the generic beforeSend hook.
Grounds: a hold/authorize permission system for a control no human ever issued; the
founding incident was later ruled NO-FAULT.
2. W1 guard-tail trims — the MF5 caller-supplied wake-intent-* rejection on
POST /api/queue/outbox/record and its case-variant broadening (one block). W1's
transactional closure core, drain, indeterminate handling and crash-boundary tests stay.
3. Gate-verdict containment tail, PARTIAL — keeps the plain outside-the-worktree typo
guard; drops the tracked-destination, tracked-path-traversal, tracked-symlink-leaf and
.git administrative-path legs plus their fixtures.
4. W2b poison-retry — reverts bba1ad264. The envelope mechanism (d685eb713) stays.
Technique: each touched file was classified by whether its only commits since 56858cbea^
were the 7 named W4 commits. 12 W4-only files were restored wholesale to their pre-W4
content; 4 MIXED files got surgical edits. This matters because a68a156bb (ACTIVITY 5b)
also touches session-transport.ts, startup.ts, server.ts and test-app.ts — a blind
revert-to-pre-W4 would have silently deleted in-flight ACTIVITY work, including the
mapPaneState export. That work is preserved.
DB, per the lock-level ruling: migration 068 is APPLIED history and its file STAYS, inert.
New forward migration 071 drops enforcer_decisions. DROP TABLE IF EXISTS is load-bearing —
068 is excluded from the shared full-test DB fixture, so 071 runs there against a table
that was never created.
Two consequences named rather than buried:
- Removing the MF5 route guard restores a forgeable wake-intent path by design (the only
caller is the daemon's own localhost client). The exact-case drain selector is now the
ONLY thing keeping a recorded WAKE-INTENT- case variant out of the executable drain; that
W1 pin is kept and its now-false comment corrected.
- canonicalizeDestination in gate-lane.mjs is retained: it normalizes macOS /var ->
/private/var, so its justification needs no adversary, only an OS. Removing it made 6 of 7
gate-lane tests fail on honest paths.
Verified at base 1e4bfee83: daemon 7268 passed / 0 failed, cli 2260 passed env-less,
test:repo 164 pass / 0 fail, gate-lane binding 7/7, tsc clean in both packages. Isolated
daemon boots clean on a migrated copy of the live DB (/healthz ok, reconcile errors=0);
the 3 control routes 404 while /state and /trigger stay; rig compaction-control is an
unknown command. DB proof ran on a sqlite3 .backup copy — the live DB was never touched.
Not reviewed yet: handed to dev-guard as qitem-20260811033226-683d25ad for
accept-to-commit. Branch-only, not reachable from main, never pushed.
Guard round 1 (dev-guard NOT-CLEAR on bda641883, verdict sha256 8eeeabc0…c2cec) —
two bounded corrections applied, no scope change:
HIGH-1: startup.ts kept a W4-only `compactionSettingsStore` local, introduced by
56858cbea solely so W4 could share the settings store with EnforcerDecisionStore.
With that store deleted the local was single-use, so it left a second construction
shape for no behaviour and contradicted the FULL W4 unbuild claim. The construction
block is now byte-identical to 56858cbea^; all later non-W4 startup work retained.
HIGH-2: four records still asserted the deleted route reservation exists —
outbox-handler.ts:3-10 (prefix doc), outbox-handler.ts:281-290 (exact-case selector
rationale), queue-repository.ts:691-694 (missing-target refusal), and the
queue-transactional-closure describe label. Reworded to state the real effects:
the prefix is the executable drain namespace and NOT a route reservation, exact-case
selection is now the only thing keeping a recorded case variant out of the drain,
and the missing-target refusal stands on its own. No deleted guard was re-added and
no behaviour changed.
The cross-citation example in docs/reference/sdlc-conventions.md cited its
origin convention with the founder's personal absolute path and a seat
attribution — the exact leak this file's own public-hygiene rule (a public
artifact should carry no internal role/seat/path) forbids twenty lines above.
Cite the convention by name only; strip the seat attribution. Rule text and
citation structure unchanged; forward-only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The A2-1 model-fidelity fix's sibling, one field over. The codex adapter already
emits `-p <profile>` from binding.codexConfigProfile and the RESTORE path already
threads it; only HANDOVER dropped it — lookupNode SELECTed id/runtime/cwd/model
but not codex_config_profile, so a profile-pinned codex seat reverted to no-profile
at every handover.
Thread it the same way as model: NodeRow + lookupNode SELECT (seat-handover-service),
successor node object, SuccessorNode + transient binding (successor-session-launcher).
Adapter unchanged (already consumes binding.codexConfigProfile).
RED-first EFFECT proof: real lookupNode->createSuccessor->launchHarness path asserts
the successor binding carries the SPEC-pinned profile (mirrors the A2-1 money proof).
Enumeration guard extended to fence the profile field across the handover path.
Gate: daemon tsc 0, full daemon suite 7330 passed / 0 failed (hermetic, env-less).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
HIGH-2 (no-successor pairing). Workflow resume anchored assertTerminalClosureHasIntent
on firstClosedExceptionId against the redrive packet — pairing a close with a
successor that is not its own, which canonical transition 5764 forbids: 'the assert
runs only where a real source->successor pair exists.' That transaction has no such
pair. The exception closes have no successors; the redrive packet's predecessor was
closed in an EARLIER transaction. The assert is removed (the intent is still staged,
so the wake stays durable) and the dead anchor variable with it.
I had named this exact pairing as forbidden in my own correction to the planner, then
implemented it and wrote a comment claiming I had not. Doing it once rather than N
times made it no less vacuous.
HIGH-1 (guard granularity). The closed-set guard sanctioned FILENAMES and counted
FILES, so a call added inside an already-sanctioned file left the count unchanged and
stayed green — the guard could not fail for the case it exists to catch. It now
constrains CALL SITES: each entry declares how many sites the file may hold and why.
Comment lines are excluded, alias bindings are counted, the declaration is not a site.
Proven both ways, per the verdict's demand: the guard FIRES BY NAME
('count-mismatch:domain/workflow-projector.ts (sanctioned 2, found 3)') on a call
spliced into a sanctioned file, AND a negative control runs rev-1's file-granular
logic over that same corpus and observes it return violations=[] — the old guard
staying silent is what makes this evidence rather than assertion.
Rev 1's counts were also wrong: it matched comment lines, reporting 4/3/2 where the
code holds 4/2/2 and queue-repository holds ZERO (line 1711 is the declaration).
Separately, a first attempt at whole-file comment-stripping silently ATE a real call
(3 where 4 exist); caught by listing raw line numbers instead of trusting the count,
and replaced with line-oriented matching.
Guard suite 11/11. Full daemon run 1: 541 files / 7293 passed. Never push.
The set of in-transaction queue writers is asserted CLOSED. Every call site of
updateWithinTransaction in packages/daemon/src must appear in a sanctioned list
naming WHICH case it is; any unlisted call site fails.
It keys on the CALL SITE, not on the value passed. 'Callers that can pass a
terminal state' is a value-tracking property and is not locally decidable —
const s = cond ? 'done' : 'blocked' defeats it, as do assignment, loop and
initializer forms. That is the property review50-r2 falsified the W2b rev-1 guard
on, with four ordinary TypeScript shapes returning violations=[]. The synthetic
fourth writer here computes its state exactly that way and is still caught.
It over-approximates deliberately: park-only callers are listed too. Over-
approximation costs one explicit line when a writer is added; under-approximation
fails silently, which is the defect class this atom closes.
RED 3 fires on a synthetic fourth writer (demonstrated, not merely present) and
on a near-miss filename, proving the allowlist is per-path and not a pattern.
RED 5 is the known-negative: an empty corpus, and a corpus where nothing calls
the primitive, both report VACUOUS rather than a clean pass — a check that
verified nothing must not report the same green as one that verified everything.
The live run asserts NOT vacuous and an exact examined-count.
That exact-count assertion earned itself immediately: the sanctioned list was
first written from memory with four entries against three real call sites, and
the guard failed. queue-repository.ts DEFINES the primitive and reaches its own
write path privately — it is the definer, never a caller, and is now excluded
with that reason recorded.
Scope note: this is a CHECK-TIME guard, not runtime enforcement. better-sqlite3
exposes no commit hook and each writer owns its own db.transaction, so a fourth
writer can be fixed at runtime but never caught there. Never push.
RED 1 proves the wiring works; these prove it fails correctly, which is the half
that decides whether the atom is honest or merely green.
RED 4 (park): mission-control hold, and a human-gated workflow entry that parks
in-txn on a freshly created packet — the shape most easily mistaken for a closure
by an update-keyed guard. Neither stages an intent. This falls out of the
primitive (early return on a non-terminal source), not out of a call-site
condition, which is why no park site needed an edit.
RED 4b (the THIRD state): a terminal close with NO successor — approve closing
done/no-follow-on, and the resume path's exception closes. Legitimate, and needs
no intent because there is nothing to wake. A guard keyed on 'terminal close'
alone rather than 'terminal close + a create in the same txn' would fire here and
condemn correct code, one state over from the park case.
The 4b resume assertion is on the DELTA, not on absence: each exception item
ALREADY has an intent from when :729 created it as a successor. What must hold is
that closing it adds none. It asserts exactly one intent was added AND names which
id — asserting only the count would pass vacuously if the closes had each been
paired with the redrive packet.
NO-DOUBLE-SEND: after a wired close+create the intent row is finalized and a
recovery drain delivers nothing. This is the defect the guard caught in review —
maybeNudge sends without finalizing, leaving the row pending for the startup sweep
to deliver a second time.
FALLBACK: a writer with no intent store still nudges. Before P34 this was ZERO —
deliverWakeIntent returned 'skipped', and a skip is not an error, so the vanished
nudge surfaced nowhere. The control isolates the fallback's own send, since
create() nudges on its own and would otherwise carry the assertion.
11/11 green on 3/3 runs. Never push.
The W1 seam is fail-closed (MF2): a nudge-intended terminal close needs an
attached intent store to make its wake durable. Wiring the writers therefore
surfaces every harness that drives a terminal pair without one.
MEASURED, not grepped: two full daemon runs before the migration produced an
identical 17 files / 69 tests, every failure the same wake_intent_store_unavailable.
The grep population was 27 — ten of those never execute a nudge-intended terminal
pair, which is why the binding was to measure by running. Mission Control's
harnesses are absent for the same reason.
Each migrated harness attaches an OutboxHandler on the SAME connection (attachOutbox
rejects a split DB — a split DB cannot make the close and intent atomic). Nothing
relaxes fail-closed; the harnesses get a real store, exactly as W1's own callers did.
Also fixes a defect in the migration itself: three files hold MULTIPLE independent
harnesses with their own migrate([...]) lists, and only the first was given
outboxEntriesSchema. Attaching an outbox over a missing table made stageWakeIntent
throw a raw SqliteError, which escaped the workflow route's typed mapper as a 500 and
swallowed a real 409 (instance_version_conflict). Two further files carried the same
latent mismatch while still passing, because their secondary harnesses never stage.
All 7 blocks fixed; per-file parity now holds (3/3, 2/2, 5/5).
Full daemon suite GREEN twice, identical: 540 files / 7276 passed / 3 skipped /
5 todo. Typecheck clean. The occupant_tenures advisories are pre-existing and
unrelated — session-registry.ts is untouched by this atom. Never push.
Mission Control, workflow-projector and workflow-runtime close and create
OUTSIDE the queue's terminal verbs, so W1's guarantee held for one writer and
was merely detected for the rest. Each ruled site now stages its successor's
wake intent inside its own transaction and runs the W1 seam as the last
statement, making executed-but-unwoken unwritable at these writers too.
Sites wired (five, per the planner ruling + guard CLEAR):
mission-control-write-contract.ts close -> create (route/handoff)
workflow-projector.ts routes branch, and failed branch
workflow-runtime.ts route, and resume redrive
The two projector branches are MUTUALLY EXCLUSIVE (nextStatus=failed requires
routes=false), so each is asserted on its own rather than as two successors.
deliverWakeForSuccessor becomes the ONE shared post-commit delivery path and is
now public: it claims and finalizes the staged row, so the startup recovery
sweep cannot deliver the same wake twice — which maybeNudge (send without
finalize) would have allowed. Its documented no-outbox fallback was never
implemented (deliverWakeIntent returns "skipped" with no store attached, so the
nudge vanished silently); P34 routes writers here whose harnesses attach no
outbox, so the fallback is now real code and the stale docstring is corrected.
Parks are untouched by construction: the seam returns early on a non-terminal
source. Terminal closes with NO successor (the resume path's exception closes,
closureReason no-follow-on) are deliberately not paired — asserting them against
an unrelated successor would be a check that could only pass.
RED 1 (6ea8801f6) now passes at all five sites. Lock qitem-be056197; pre-edit
rev-2 2f4710b1; guard CLEAR qitem-634de805. Never push.
Terminal close + successor create through Mission Control, workflow-projector
and workflow-runtime stages ZERO wake intents today. Each test drives the REAL
writer with an intent store attached and asserts the successor's wake intent is
durable; all five fail at the intent assertion (never at setup), so the closes
and creates demonstrably happened and only the intent is missing.
Sites: mission-control-write-contract.ts:174, workflow-projector.ts:466 (routes)
and :729 (failed — exclusive with :466, nextStatus=failed requires routes=false
per :571-585), workflow-runtime.ts:992 (route) and :791 (resume redrive).
workflow-runtime.ts:831 is deliberately NOT a site: it closes N exception items
with no successor (the third state). Pairing it with the :791 packet would
satisfy the assert against an unrelated successor — a check that can only pass.
Lock qitem-20260809110825-be056197; pre-edit rev-2 2f4710b1; guard CLEAR
qitem-20260809180349-634de805. Test-only; no production change.
Default `rig ps --nodes` ACTIVITY was 100% hook-based: attachAgentActivity emitted
unknown/no_runtime_hook for hook-less/stale-hook seats, so live seats sat at empty prompts
with nothing in the control plane. Lift the EXISTING structural pane classifier
(session-transport classifyPaneActivity — spinner shapes / esc-to-interrupt / idle-prompt,
never a verb allowlist, so a "Drizzling"-style gerund reads as motion) into ACTIVITY:
- NEW SeatStructuralActivityService: captures + structurally classifies each running tmux
seat's pane on the 1Hz cadence and CACHES it per session. The request path stays
CAPTURE-FREE (reads the cache) — the OPR.0.4.3 healthz-wedge storm fix is preserved.
- attachAgentActivity precedence fold: fresh POSITIVE hook wins -> cached structural verdict
(evidenceSource pane_heuristic, sampledAt = the liveness observation time) -> honest
stale/unknown hook -> unknown/no_runtime_hook. Structural liveness OVERRIDES an absent or
stale hook (constraint 2 — liveness beats hook-arrival age).
- Constraint 1 (turn-boundary): idle != quiet; the finding-derivation (baton.dropped / idle-
gate) already requires held work, so the ACTIVITY column honestly shows idle/running.
Background-path safety (guard MF1/MF2):
- Freshness + invalidation: getStructuralActivity age-gates (evicts past a bounded window)
and pollSeat invalidates the row on a null/failed capture — a stale positive verdict never
survives a capture outage or same-name occupant transition.
- Single-flight, held until settle: sweeps never overlap (a stuck tmux can't accumulate
whole-fleet captures). Deliberately NOT time-out-to-release: execCommand has no abort, so a
wrapper timeout would release single-flight while the child process lives = a process-level
storm. A permanently-stuck tmux degrades to no-signal via age-expiry (honest). A kill-backed
capture primitive is a follow-on.
Wired production startup -> server ctx -> sessions/rigs routes (mirrors seatActivityService),
started post-bind in index.ts. Follow-on: the UI "N days quiet" label (activity-visuals.ts,
off eventAt) is the same arrival-age miscalibration on a separate UI surface.
Reviewed-by: dev-guard@v-openrig-build (accept-to-commit; 13-file tree on 560387c8e; MF1/MF2/HIGH3 close at source; 138/138 + tsc 0 + diff-check clean; CLEAR sha256 9034991f)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The universal outbound-request convergence. DaemonClient.fetch converges DaemonClient
traffic but front-door healthz + daemon-lifecycle kernel-status issue RAW reads that
bypass it, so a client-layer guard would pass review and leak two paths. Wrapping
globalThis.fetch (armed from setupFiles — unwritable by omission) catches all four live
provenances at once: env (M1, scrubbed), hardcoded :7433 literal (M2, 48 files), mocked
URL getter + un-mocked client (M3, ~24 files), and production default constants (M4,
grep-invisible) — every one ends in a real request to the canonical daemon.
ALLOWLIST BY SHAPE, not by registration (the third form — keeps fail-closed, needs no
65-file migration, is not a denylist): PERMIT loopback (127.0.0.1/::1/localhost) on an
EPHEMERAL high port (>= 32768 — exactly what server.listen(0) produces; verified 58158+
on darwin, Linux uses 32768+), which is every legitimate in-process fixture with ZERO
edits. REFUSE everything else — non-loopback of any kind (the machine-boundary arm:
hosts.yaml names a real remote at <redacted-remote-ip>) and loopback on a well-known/canonical
port including :7433. Default is refuse, so fail-closed survives. allowFetchTarget stays
as the escape hatch for a fixture that needs a fixed low port (by exception, not 65
files). State is globalThis-backed because vitest can split setupFiles vs test-file
module instances.
THREE-sided known-negative (a guard proven only refusing is a blanket denier that might
be broken in the safe direction): REFUSES the canonical daemon; PERMITS a
loopback-ephemeral fixture by shape; REFUSES a non-loopback target. RED-first against a
fixture, never live.
Gate: full cli suite 165 files / 2279 passed / 0 failed under the guard (blast radius
ZERO — the shape permits every fixture; escapes converge on :7433, refused). Shared
spawn/mock-server helper filed as its own follow-on atom, not this fix's gate.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Contains the broadcast-leak (broadcast.test.ts emitted REAL 'System maintenance'
broadcasts to 14 seats). A GUARD, not a one-file fix: only 19/164 cli files honored
the isolation convention; broadcast.test.ts leaked VISIBLY because it WRITES, the
other ~145 would touch a live daemon SILENTLY. The READ direction (results pollution)
was banked; the WRITE direction (emitting into the live topology) is strictly worse.
The hole: hermetic-env.setup scrubbed OPENRIG_URL/PORT but NOT the default STATE_FILE
discovery (OPENRIG_HOME/daemon.json). FORCE + ASSERT at the suite/setup layer, so the
guarantee lives where it cannot be skipped by omission (the W1 move):
- FORCE a fixture-scoped OPENRIG_HOME (a realistic `.openrig`-style temp home carrying
the `.openrig-fixture` marker) BEFORE openrig-compat captures the eager const, so
default discovery finds no live daemon. One stable home per worker (memoized) so the
env and the captured const cannot diverge.
- ASSERT `assertFixtureScopedHome` throws LOUD if the home is not fixture-scoped
(mkdtemp failure / import-order capture) — forcing alone silently fixes and proves
nothing. Known-negative in live-daemon-guard.test.ts drives it against a live home so
it is evidence, not hope (the requirement-4 law).
Also scopes broadcast.test.ts: every broadcast is explicitly bound to the in-process
fixture daemon, so an unscoped broadcast cannot reach live via any code path.
Blast radius measured by running the full cli suite CONTAINED: 3 files failed under
the raw fixture home; all resolved WITHOUT papering or file exemptions — 2
(host-registry, setup) by the realistic `.openrig` home + the stable-per-worker home
(which also deterministically FIXES the pre-existing OPENRIG_HOME flake), 1 (client
--version) was environmental (cli dist unbuilt). Full cli suite now 164/164, 2256/0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Migration 060's header said generation_uuid "is the STABLE occupant identity that
survives handover/swap/repair" — the inverse of the truth. A new occupant (handover/
swap/repair to a different native session) mints a NEW generation_uuid; it persists
only WITHIN one tenure (a same-native relaunch is a continuation, not a new generation).
node_id is the cross-handover key. Comment-only, no behavior/test change — the same
correction already applied to types.ts/startup.ts in the W2a-1 store half (1e41c0cea).
Reviewed-by: dev-guard@v-openrig-build (accept-to-commit; diff sha256 6610a183; base f1da823a2; tenure tests 50/50; tsc 0)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
getLatestForNode compares the recorded vs live occupant-tenure generation_uuid. The emitting
occupant's generation is CARRIED SOURCE-BOUND on the hook (the producer/relay supplies it at fire
time; the route ingests body.generation) — NEVER inferred from record-time state, which mis-attributes
a DELAYED prior-occupant hook to the successor occupant (a timing/boot_at inference is unsound at
SQLite 1-second precision + cross-host clock skew).
Read verdicts:
- carried != live -> unknown/generation_mismatch/stale, provenance resolved (REFUSED; dead-tenure
evidence; same-second-immune because nothing is timed).
- carried == live (incl. a same-native relaunch continuation) -> fresh, provenance resolved.
- null on EITHER side -> unknown + a distinct reason (generation_unresolvable = live null /
generation_unverifiable = carried none) + stale, generationProvenance='unresolved' ALONGSIDE
(never fresh; delivered-labelled).
- resolver THROWS -> unknown/generation_resolver_error (degrade; never crashes, never fresh).
- no resolver -> legacy, no label.
Scope: STORE HALF + route ingestion + a SessionTransport advisory blast-radius fix.
SessionTransport.diagnoseProducerLink now distinguishes the four generation reasons from clock-stale
BEFORE the clock-stale fallback (a fresh generation verdict is stale:true at age ~0; collapsing it to
"beyond the store window / seat quiet" mislabelled producer-not-landed / dead-tenure as a DARK seat).
INERT + OBSERVABLE: the relay/env producer-carry is a FILED PREREQUISITE
(qitem-20260808195950-73d8311f); until it lands every hook carries null -> generation_unverifiable
-> detection INERT (sound, never false-fresh). generation_unverifiable is the MECHANISM (carried
null), NOT a single cause: producer-not-wired OR the emitting occupant had no tenure at fire time
(mint-race / pre-060 upgrade). Reader rule: ANY resolved/generation_mismatch => producer wired
(live); ALL generation_unverifiable => likely-not-wired but not proof (a fleet in mint-race/upgrade
looks the same) — disambiguate by tenure. Consumer verify-demotion is the separate tap follow-on
(qitem-20260808183747-f7f04662).
A structural guard pins exactly one production construction site AND that it injects the resolver.
VM-only (mm2 has no occupant_tenures table until 0.5.1). Upgrade-transition: migration 060 at
startup + mint-at-register => already-running seats read unknown until re-register.
Restacked onto exact main 045dea26 (was 4 commits behind at c9e2120c0). (B) receipt: the 8 store
files' per-file patch-ids are IDENTICAL pre/post restack — store logic unchanged; only startup.ts
merged (my source-bound construction site + main's watchdog auto-registration, non-overlapping,
auto-merged; the construction-site guard re-earned at the tip).
Evidence (measured at the restacked tip): full daemon matrix 2-fail/7105-pass, tsc:0. The only 2
fails are the seat-handover-model-fidelity + seat-handover-cutover e2e, PROVEN NOT MINE by a fresh
A/B AT THIS BASE: they fail with IDENTICAL signatures (successor_create_failed /
graceful-handover-false) at exact main 045dea26 WITHOUT this change — a structured deterministic
handover-create failure, not a timeout; the definitive uncontended real-vs-flake confirmation is
QA/operator domain (the box must drain). Store 17/17 (incl. the same-second carried-A-vs-live-B
mismatch + resolver-exception degrade + real-SessionRegistry composition); provider-c4 30/30;
sessions-routes 41/41; send-prompt-guard advisory pin (fresh generation_unverifiable -> generation-
distinct advisory, never clock-stale).
Mechanism note (routed to PM by dev50-planner): source-bound beats the locked rev-2 88b9a3bc line
112 record-time resolution (the delayed-hook mislabel) — a build-discovered refinement toward
correctness.
Review status: dev50-guard round-3 IN FLIGHT — mechanism shape approved; the diagnoseProducerLink
blocker fixed at this SHA; clearance re-requested on the restacked SHA. dev50-planner + orch-advisor
prior gates cleared; orch-advisor merge-desk restack/receipt requirements addressed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Proof-only correction for dev50-guard re-seal (artifact GUARD-W1-HOLD-396C9E30.md
sha256 …). The product corrections were confirmed sound; the BLOCKING 1
regression overstated a close/reopen — it reused one still-open in-memory
Database.
Add a REAL crash-boundary pin: persist a claimed `sending` wake intent to a
file-backed db, CLOSE the connection (the crash), reopen the SAME file with fresh
OutboxHandler / QueueRepository, then reconcile + drain — proving
sending -> indeterminate, zero transport sends, and no re-drive. The before-send
and after-send/before-finalize windows share the same persisted `sending` state,
so one equivalence pin suffices. No production change; the stale `sending`
comment in outbox-handler.ts is updated to name the recovery-boundary reconcile.
Gate: queue-transactional-closure 24/24; daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Addresses dev50-guard re-seal HOLD on 7afed07f (artifact GUARD-W1-HOLD-7AFED07F.md
sha256 a1022520). Restacked onto current main 045dea26 (W1 outbox wiring +
watchdog auto-registration integrate cleanly in startup.ts).
BLOCKING 1 — a claim left `sending` by a crash now reconciles to `indeterminate`
at the recovery boundary WITHOUT re-sending: new QueueRepository
.reconcileAbandonedWakeIntents() (OutboxHandler.reconcileAbandonedSending),
called ONCE at startup before the drain. Kept SEPARATE from the concurrency-
exposed drain so an overlapping drain never reconciles another drain's in-flight
claim.
BLOCKING 2 — the executable drain selector is now EXACT-CASE (substr binary `=`
instead of case-insensitive SQLite LIKE), and the public /outbox/record
reservation rejects CASE VARIANTS (toLowerCase). A `WAKE-INTENT-…` variant is
therefore neither accepted by the route nor selected by the drain. Pinned through
the HTTP record route, the drain, and a real-qitem control; lowercase positive
retained.
Gate: full daemon suite 7126 pass / 0 fail (uncontended single-fork); daemon
tsc + build 0; cli build 0. Guard discriminator: variantSelectedByDrain now
false.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Addresses dev50-guard's HOLD on 74ab32a4f (artifact GUARD-W1-HOLD-74ab32a4.md
sha256 14a933a8). Claim NARROWED per dev50-planner: W1 makes an executed-but-
unclosed item unwritable via the queue's OWN terminal verbs (handoff /
handoff-and-complete) — NOT "the write path". Mission Control / Workflow
terminal writers are the named P34 follow-on (MF1), held for the gate.
MF2 — attachOutbox rejects an outbox on a different DB connection (split-DB
breaks the atomic seam); a nudge-intended terminal act with no intent store
FAILS CLOSED (the guard throws instead of silently closing without an intent).
MF3 — claim-before-send: a `sending` CAS claim (claimForDelivery/finalizeDelivery)
means overlapping drains send the external wake exactly ONCE, not just converge
to one state.
MF4 — the intent FREEZES its emitting envelope (generation resolved at stage
time); delivery replays it verbatim, so a crash-recovery after a tenure swap
never relabels the wake with the current occupant.
MF5 (security) — the executable wake-intent-* id namespace is reserved: the
public POST /api/queue/outbox/record rejects it, and the drain only delivers a
wake whose target qitem exists. Pinned through both the HTTP route and the drain.
MF6 — a timeout classifies as `indeterminate` (may have landed), not `failed`;
the retry prose is corrected (only `pending` is retried; failed/indeterminate
are terminal-visible; bounded retry is the named residue).
Proof gap — demo1 now drives a REAL handoff interrupted after commit / before
drain (deferred transport), not a hand-planted row.
Primitive — stageWakeIntent + assertTerminalClosureHasIntent are now PUBLIC
composable in-transaction methods (the createWithinTransaction precedent), so
wiring MC/Workflow in P34 is extension, not rework.
Gate: full daemon suite 7091 pass / 0 fail (uncontended single-fork); daemon
tsc + build 0; cli build 0. Blast radius (5 handoff harnesses) reconciled by
attaching an outbox, mirroring production startup.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Mission release-0.5.1 / 51-06 W1. A qitem terminal act (close + transition)
and its WAKE INTENT now commit as one act or none, so an executed-but-unwoken
close becomes impossible to WRITE rather than merely detectable.
W1-a — the durable intent row joins the terminal txn. handoff /
handoffAndComplete stage an outbox wake-intent row INSIDE their db.transaction
(reversed-never: the pane nudge stays a post-commit side effect; what joins the
txn is the durable intent). A fault anywhere in the act rolls back the close,
successor and intent together (NONE-side atomicity).
W1-b — the drain, with indeterminate-outcome discipline. A committed intent is
delivered by performWakeSend + CAS: verified -> delivered, ok-but-unverified ->
indeterminate (never silently delivered/failed), hard-fail -> failed (visible,
not lingering pending). The outbox CAS makes double-drain a no-op (drain twice,
delivered once). A startup-recovery sweep re-delivers crash-orphaned intents;
no periodic timer (out of scope) — the residue is a transient failure retried
on next start. indeterminate is a new closed-union value with a validated parse
replacing the unchecked cast; it is terminal-by-CAS.
W1-c — the seam guard. A runtime invariant, the last statement in the terminal
txn, throws if a close that intended a wake commits without its durable intent
— unwritable at the seam, not at review. Nudge-aware (nudge:false intends no
wake). Demonstrated firing on the real handoff chokepoint with staging neutered.
Proof (emitter demonstrations, each names its pin): demo1 -> W1-b idempotence,
demo2 -> W1-a NONE-side atomicity, demo3 -> W1-c guard firing. Full daemon
suite 7080 pass; daemon tsc + build 0; cli tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Commit messages, code comments and docs ship to readers with no access to how the
work was governed internally, and nothing in the conventions said so.
Adds two forward-only rules: no verbatim internal quotes (especially emotional
ones — quote the technical content of a decision, never the person or the mood),
and no governance framing (write what the change does and why it is correct on its
own terms, not who ruled it or which gate it passed).
States the test rather than a list of banned words: a reader outside the project
should be able to act on the text without knowing any internal role, seat, or
process name.
Not retroactive; existing history is not rewritten for these.
Co-Authored-By: Claude Fable 5 <[email protected]>
The PM-gate EFFECT proof: boot a codex seat PINNED to gpt-5.6-luna, HAND IT OVER
through the REAL SeatHandoverService + REAL CodexRuntimeAdapter in an isolated tmux
(D15), and read the successor's EFFECTIVE model off its live TUI footer. It runs on
the spec pin (gpt-5.6-luna), NOT the runtime default (gpt-5.6-sol). spec != default is
the discriminator — a footer showing the pin can only arise from -m threaded through
the successor binding (A2-1). result.ok=true (successor became a ready agent).
Reads the EFFECTIVE footer ("gpt-X <tier> · <cwd>"), never the banner echo
("model: X /model to change") — the banner shows the REQUESTED model even when the
runtime rejects it and falls back (the indicator-vs-effect trap; observed live as the
A3 specimen). Explicitly rejects any invalid-model/fallback text.
Harness notes learned the hard way: (1) codex silently drops -m to a fallback tier when
an --add-dir target (the queue-state root) is missing — created here; flagged for A3.
(2) repeated Enters over-navigate codex mid-resolution and flip the effective model —
trust gate is approved CONDITIONALLY (poll for the prompt, one Enter, stop). Stable 3/3.
Guarded (it.runIf): skips without tmux + codex + ~/.codex/auth.json. daemon build 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The census fence the slice exists to install — and it immediately caught what the
grounding map missed. 51-07 A1 threaded -m on the codex FRESH branch only; the
codex adapter's FORK (fork-instantiate, rigspec-instantiator) and RESUME (pod-aware
restore) branches computed modelArg but never emitted it, silently reverting a
spec-pinned codex seat. The claude adapter already emitted on all three branches, so
the map's "pod-aware PRESERVES" was true for claude and assumed for codex — the exact
increment-reasoning error this slice kills.
- codex-runtime-adapter: fork + resume branches now thread the model (fresh already did)
- repurposed the stale "does not pass a model when resuming Codex" test — it pinned the
bug with no reason; flipped to assert -m, with a record of the flip
- enumeration guard (model-fidelity-enumeration-guard.test.ts): every seat-launch
command template threads modelArg; buildCodexResumeCore accepts/emits -m; legacy
resume builders thread model; buildNativeResumeCommand's callers are enumerated ⊆
{node-inventory, resume-metadata} (classified NOT-A-SEAT-LAUNCH). A new launch branch
that drops the model fails here, loudly. Matchers proven non-vacuous.
- fix: model is a TRAILING param on claude/codex resume (a mid-insertion shifted the
posture arg into the model slot → `--model 'floor'`; caught by the full-suite gate,
permission-policy-lifecycle). attemptResume call sites reordered accordingly.
daemon build 0, cli build 0, full daemon suite green (7064).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
attemptResume already held node.model but dropped it into the Claude/Codex resume
adapters (only Pi threaded it), so a legacy restore relaunched the seat on the runtime
default — the same silent revert 0.4.6.PI1 fixed at the pod-aware site alone.
Thread the model through:
- ClaudeResumeAdapter.resume → emits --model (matches the fresh-launch adapter)
- CodexResumeAdapter.resume → buildCodexResumeCore → emits -m (top-level, before `resume`)
- restore-orchestrator attemptResume: pass node.model into both call sites
model is an optional param on buildCodexResumeCore; the non-launch callers (node
inventory, resume-metadata) omit it → byte-identical (regression pins hold).
RED-first per builder (exact byte pins). daemon build 0, 255/255 across resume +
restore-orchestrator + node-inventory + resume-metadata suites.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Handover silently reverted a spec-pinned seat to the runtime default: the
successor launched WITHOUT the seat's --model/-m. The gap was at the source of
the node, not the launcher — lookupNode() SELECTed only id/runtime/cwd, so the
model column never reached createSuccessor's binding.
Fix (spec-fidelity, not cost): SELECT model in lookupNode + carry it on NodeRow;
set model on the successor's transient binding (the adapter already emits it).
Money proof lives at the REAL path (seat-handover-service.test.ts): a model:X
seat's handover → launchHarness binding.model === X, driven end-to-end by
service.handover(). RED-first — the earlier launcher-level proof injected the
node literal and could not catch lookupNode dropping the column; this one does.
daemon build (tsc) 0, vitest 45/45 (handover + successor suites).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Follow-on to A3, planner-ruled IN scope (ledger c6c5b0249df64a07): the /broadcast fan-out is
a dispatch too, so a broadcast send must not accept-and-drop at the audit layer. The schema
decided the SHAPE — outbox_entries.destination_session is typed + indexed and the table carries
per-row delivery_state, a per-recipient design — so it is N rows, one per RESOLVED recipient,
never one row with a TargetSpec (which would poison the typed session column and could not
express a partial fan-out).
- routes/transport.ts (/broadcast handler): after the fan-out, record one outbox_entries row per
result.results entry — sender_session = the DERIVED actor, destination_session = each RESOLVED
session, identity_provenance = "transport:v1", and delivery_state per recipient (markDelivered
for ok, markFailed otherwise) so a partial fan-out records the truth per recipient. STRICTLY
DOWNSTREAM of the certified derivation (derivedActor); best-effort per row (dispatch committed).
- metadata-vs-identity: the outbox is sender-side audit the receiver never reads, so it scopes IN
(the same law that scoped the SSH read paths OUT).
Pins: broadcast→N rows(each resolved session, transport:v1, delivered); partial fan-out→per-row
delivered/failed; refused→0; headerless→0. RED 2-at-base + 2 controls. daemon tsc 0; transport
+ outbox + gateway-e2e suites green (46).
Based on A3's tip (7ac6bc2c0) so it includes A3; rebases onto the A3-inclusive main when A3 folds.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Identity/audit family atom 3 (design dc3a0719, A3 section). The outbox was explicit-verb-only
(`rig queue outbox record`), so a derived send was not recorded unless someone remembered —
the specimen-5 window, where a send's attribution survived only via the ghost's provider JSONL.
A3 auto-records at dispatch so a derived-but-unrecorded send cannot accept-and-drop at the audit
layer.
- routes/transport.ts (/send handler): after a SUCCESSFUL dispatch, record one outbox_entries row
with sender_session = the DERIVED actor (the x-openrig-session header, never a body claim) and
identity_provenance = "transport:v1". STRICTLY DOWNSTREAM of the certified header-derivation
(line 65 unchanged) — it only consumes the already-derived actor. Best-effort: the send is
already committed, so a rare audit-write failure is logged, never a false-negative on delivery.
- Records only a DERIVED send; a null-actor send has no sender to attribute (no fabricated row);
a REFUSED send returns before dispatch (no row); a cross-host send records the ORIGIN triple
(the derived header IS the origin after A2/A4), never the relay.
Implemented at the ROUTE level (not session-transport.ts) — derivedActor + outboxHandler are
route-scoped. Pins: derived→1 row(transport:v1), cross-host→origin triple, refused→0, headerless→0.
RED 2-at-base with 2 controls. daemon tsc 0; transport-routes + outbox-handler suites green (38).
DELTA REVIEW: the certified property (identity derives from the header, never the body) is
UNCHANGED and depended upon; A3 adds only a durable write downstream of it. Scope note for review:
/send only — whether /broadcast fan-out should also auto-record is a follow-on question, flagged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The added rule is that a cited hash states what it covers. Its own citation
was a bare path I could not resolve from this VM — a doc teaching citation
discipline failing its own standard at the first reader who tested it.
pm-openrig supplied the citation in the form their convention prescribes and
it is carried verbatim: path + section anchor + dated whole-file hash, with
coverage stated (whole file, bytes as stored, including the trailing newline,
0x0a verified, 229 lines, as-of 2026-08-08).
Records why the form has three parts rather than one: path + anchor is the
AUTHORITY, the hash DATES the citation. The cited artifact is living, so the
hash goes stale at the next legitimate append while the anchor does not —
the CURRENT-line convention applied to a citation.
Honest limit, carried rather than hidden: the bytes remain unverifiable from
this VM (substrate split). This is testimony with a reproducible definition,
and the first host-side reader can test it — which is precisely what the rule
requires of a citation.
Never push.
Co-Authored-By: Claude Fable 5 <[email protected]>
The shipped SDLC conventions carried the C1 drop-verb evidence guidance but no
hash-coverage requirement — so a hash could be cited as evidence without the
reader knowing which bytes it was taken over, which is what makes a digest
verifiable rather than merely impressive.
Adds the requirement in two admissible forms (whole-file with the
trailing-newline question answered, or file plus an exact span definition —
the span being part of the definition, not a hint), and states the reason
plainly: two honest people can hash the same artifact and get different
digests without either being wrong.
Cites the canonical workspace convention as the ONE origin
(openrig-work/conventions/artifact-cross-citation-path-plus-lineage.md,
span-is-part-of-the-definition, 2026-08-08) rather than restating its rule or
its specimen — a convention restated downstream forks, and the fork is what
gets followed.
Never push.
Co-Authored-By: Claude Fable 5 <[email protected]>
Identity/audit family atom 2 (design dc3a0719, A2 section). The SSH relay re-runs `rig`
in a non-login shell that would resolve ITS OWN seat, degrading the origin. Prefix
OPENRIG_SESSION_NAME=<origin triple> INSIDE the remote sh -lc line so the remote's
DaemonClient stamps the ORIGIN identity — the SSH parallel to A4's HTTP header triple.
- cross-host-executor.ts: RunCrossHostCommandOpts gains originTriple; when present, the
env assignment rides the SAME shellQuote layer, before the argv. Absent ⇒ byte-unchanged
from D13 (backward-compatible; only send --host passes it today).
- send.ts (runCrossHostSend): passes the origin triple it already composes (seat env +
selfHostId, upstream 3-part preserved verbatim). --from STAYS ADDITIVE on argv — a pre-I4
remote still reads origin from it; the argv removal is a separate P23-D1-gated increment.
- pins: executor byte-exact env prefix (via the real shellQuote) + additive --from +
backward-compat no-prefix; send-level passes-the-triple + 3-part-verbatim + --from-stays.
CLI src tsc clean; cross-host executor + commands suites green (72). Additive plumbing,
CLI-only (no daemon-route). NOTE for review: ps/whoami/capture --host SSH do NOT get the
env prefix (only send passes originTriple) — the A2 lock scopes to the send/From: path;
whether to extend uniform origin to the SSH read paths (as A4 did for HTTP) is a follow-on.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The full CLI gate caught a real regression the targeted subset missed (ps-http +
remote-http-ops, 26 tests): resolveOriginSelfHostId called getDaemonStatus OUTSIDE
fetchSelfHostId's own try/catch, so a throwing/unhealthy local probe aborted the
remote op before any request. A remote op must NEVER depend on local daemon health.
Wrap the entire resolution in try/catch ⇒ ANY failure returns undefined ⇒ the 2-part
header stamps (today's behavior), honoring pin 5 (no new failure mode). Adds a unit
pin: resolveOriginSelfHostId swallows a throwing local probe ⇒ undefined.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Completes A4 (design 9b5317c5). Threads THIS host's selfHostId (fail-open) through
every remote-targeting client so the stamped X-OpenRig-Session carries the ORIGIN
triple and the remote daemon renders the ORIGIN host, not its own (the receipt's bug).
- resolveOriginSelfHostId(lifecycleDeps): local /healthz selfHostId, C1 fail-open to
undefined (⇒ 2-part header, no new failure mode).
- 5 remote sites now route through remoteDaemonClient (no direct clientFactory(host.url)):
remote-host-ops:59 (chokepoint, serves 8 modules; resolves once) + ps:1382/1510 +
whoami:431/518 (fan-out resolves the id ONCE, not per-leg). RemoteHostDeps gains an
optional lifecycleDeps (callers already carry it; absent ⇒ fail-open 2-part).
- Pins: pin 1 MONEY (broadcast --host stamps the origin triple on the wire, real client
+ /healthz), pin 5 fail-open (unavailable ⇒ 2-part, still ships), pin 2 daemon
preserve-branch (an arriving 3-part sender rendered verbatim, never re-stamped),
grep-guard 2 clauses (zero direct remote constructions; originSelfHostId assigned in
exactly ONE place ⇒ pin 3 structurally true).
CLI src + daemon tsc clean; 169 cross-host/ps/whoami regression tests green (fail-open
in mocks). Ready for FULL r1 (extends the certified header-derivation property).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Identity/audit family atom 4 (design 9b5317c5, FIVE remote sites confirmed).
Count-independent core ONLY — no site constructs a remoteDaemonClient yet, so the
mechanism is dormant until the site-wiring commit. NOT foldable alone.
- senderIdentityHeaders(originSelfHostId?): a remote-targeting client composes the
ORIGIN triple (member@rig@selfHostId) at stamp time so the remote daemon renders
the ORIGIN host; identity stays DERIVED (env + this host's selfHostId), never
caller-supplied; an already-3-part origin is preserved verbatim; local unchanged.
- DaemonClient.originSelfHostId public field (assigned ONLY by remoteDaemonClient).
- remoteDaemonClient(clientFactory, url, originSelfHostId): the single origin-marker
construction point (shape b), preserves the injected-mock clientFactory test seam.
- unit pins (client-headers.test.ts): triple-compose, 3-part verbatim (pin 6),
local 2-part (pin 4), fail-open empty selfHostId (pin 5), no-session, and the
factory→wire integration (remote stamps triple, local stamps 2-part).
PENDING: thread fetchSelfHostId(local daemon) through the 5 sites via remoteDaemonClient
(remote-host-ops:48 + ps:1380/1505 + whoami:429/513); money pin (HTTP-relay broadcast
renders origin From:); daemon preserve-branch pin; grep-guard (2 clauses); FULL r1.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The inbound ignore log printed type/subtype/files — precisely the three fields
that have ALL PASSED when a message dies on bot_id or on missing-user/empty-text.
So a silent discard could not be explained by the one line whose job was to
explain it.
LIVE CONSEQUENCE, not hypothetical: three plain-message events were discarded
between 00:38 and 01:32 with no recoverable reason. Both attested conversation
coordinates were eliminated by direct read-only history (channel newest message
was our own alert; the founder DM's newest was seven hours earlier), so the
events came from a third conversation — and with channels:read UNGRANTED there
is no read that can even name it. Logging the channel is the only way to know.
ONE ORIGIN: ingestDecision() is now the single definition of the ingest branch
logic and shouldIngest() is a thin wrapper over it, so the log can never drift
from the behaviour it describes. A second copy of the branch conditions inside
the logger would have been the canonical-by-assertion shape this desk killed
three times today.
PRIVACY RAIL: channel id and a branch LABEL only. Never bodies, tokens, user
ids, or text content/length — pinned by a test that fails if the user id or the
message text appears in the log line.
RED before the fix, all three discrimination pins failing and the privacy pin
already green (it must stay green, and does):
- bot_id branch named, with channel
- missing-user branch named DISTINCTLY (not conflated with bot_id)
- empty-text branch named DISTINCTLY
- privacy rail: no user id, no text
Co-Authored-By: Claude Opus 5 <[email protected]>
The A5b image seam shipped UNWIRED AT BOTH ENDS: buildImageBlocks (renderer) and
slackDeliverFn (consumer, reads q.media) were built and unit-tested, but
slackDeliverFn had ZERO non-test callers and nothing anywhere populated `media`.
The shipped path — runOutboundOnce — passed no mediaRefs, so `rig slack outbound`
posted TEXT ONLY. Firing the M1 screenshot acceptance through it would have
delivered a founder alert saying delivery works end to end while the image path
never executed: a false pass baked into an acceptance.
WIRING: an alert's evidenceRef is, by definition, the durable artifact the human
judges — which is exactly what a screenshot in a founder alert is. The sweep now
forwards it as a Block Kit image block. No schema change, no new producer.
ONE GATE, NOT TWO: the https / secret-bearing predicate is NOT duplicated at the
call site. buildImageBlocks remains the single place that decides what is
forwardable, so there is one origin for the rule.
RED both directions before the fix:
- evidence-bearing alert emits exactly one image block carrying the URL (was 0)
- NEGATIVE CONTROL: a non-https evidenceRef emits NO image block (hygiene rail)
Co-Authored-By: Claude Opus 5 <[email protected]>
Identity/audit family atom 1 (design dc3a0719). A `rig send`/`broadcast` is
attributable-only: an unresolvable seat (no OPENRIG_SESSION_NAME/RIGGED_SESSION_NAME;
--from deprecated + ignored) REFUSES at the seat boundary — nothing rendered, nothing
dispatched — mirroring the shipped daemon 401 unattributable_sender (P21-I4).
Because the boundary refuses, the CLI can never render the <unknown sender> marker, so
the two CLI fallback definitions are DELETED (not centralized): the daemon's
pane-envelope.ts is now the SOLE definition in the tree (canonicity by structure, no
import). queue-repository.ts:553 is a comment reword only (the one non-refusable queue
nudge legitimately keeps the daemon-side fallback).
wrapSendBody now REQUIRES a resolved sender; it intentionally diverges from its
byte-identical twin wrapPaneEnvelope ONLY at the undefined input (the CLI can no longer
emit it). Lockstep comment updated.
- NEW packages/cli/src/sender-identity.ts: resolveSenderSession + requireAttributableSender
- send.ts/broadcast.ts: seat-boundary guard; seatSender threaded through every dispatch path
- tests: refuse RED->GREEN, resolvable-send, negative-control grep-guard (exactly ONE
literal definition in src); 3 tests that pinned the old marker render rewritten; seat
stubs added to the four dispatch-invoking test files
Co-Authored-By: Claude Opus 4.8 <[email protected]>
runCrossHostBroadcast REBUILDS the forwarded body ({text,force}+rig/pod) and
dropped the enveloped-fan-out marker, so the remote rendered a RAW body with no
From: — while the LOCAL broadcast path (same seat) wrapped. I4 made the LOCAL
broadcast From: derived + abolished the unattributable render, but never reached
this cross-host path. Adjacent hole made visible by I4, PM-ruled a P21-lane item.
FIX (CLI-only, no daemon change — the header path already works):
- ADD body.envelopeSender to the cross-host body via a single-origin helper
resolveEnvelopeMarker(), also called by the local path (extracted from its old
inline expression — this PREVENTS A FIFTH origin of the "<unknown sender>"
literal; the four-site consolidation belongs to the pooled refuse-loud item, NOT
here). The marker's PRESENCE is the enveloped signal; its VALUE is ignored by the
daemon /broadcast (transport.ts:210), which derives the From: from the transport.
- No extra-headers stamp: the DaemonClient already stamps X-OpenRig-Session=origin
env LAST on every request (client.ts:171, the P18 chokepoint), so the header is
auto-carried cross-host. An extra stamp would be a SECOND origin of it.
Negative control inherited for free: env unset ⇒ no header stamped ⇒ marker present
⇒ the daemon's existing I4 401 unattributable_sender fires (refuse-loud), not a
silent raw send. Pins: broadcast.test positive (marker carried, value=seat) +
session-less (single-origin "<unknown sender>", refuse-loud); cross-host-http twin
revised (the cross-host POST body now carries the marker, any-string value). CLI-
only (broadcast.ts + 2 test files); broadcast+cross-host-http 49/49 hermetic; cli tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A SMOKE run (OPENRIG_GATE_LANE_SMOKE=1) skipped every leg yet sealed a NORMAL
PASS indistinguishable from a real verdict in the JSON — hash-verifying proved
the file authentic, never that the gate RAN (pm re-checked 5 pins by run-window
duration to compensate). Now the artifact is self-describing:
- runGate({smoke, realExec, ...}) EXTRACTS the smoke→executor→verdict wiring
into ONE origin that gate-lane.mjs actually calls (not a mirror): the SAME
smoke picks the executor AND flows into the verdict, so it can't be set at one
site and forgotten at the other (the two-origins failure).
- buildVerdict records verdict.smoke from that passed value — NOT a second
process.env read that could disagree with what actually executed (intent vs
effect: runLegs takes the executor injected, buildVerdict never sees it).
- runLegs records per-leg durationMs — the INDEPENDENT observed cross-check a
whole-run total hides (mixed-mode robustness; no live hole today).
- Negative control (pm's gate condition) asserts through the SHIPPED runGate
with a spy executor: smoke:true↔ran-nothing, smoke:false↔ran-all-3, both
directions. Teeth-verified: dropping smoke from the wiring fails it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review50-r1 verdict: P21 security semantics ALL VERIFIED SOUND; two lane-introduced
gate breaks blocked the fold. Both fixed — TEST/FIXTURE ONLY, no production change
(the reviewed security code + the sealed named-gap doc are byte-unchanged).
BREAK 1 — 43 CLI hermetic REDs (header/env-cascade): the CLI reconcile made the queue
verbs derive source/actor from the seat env (X-OpenRig-Session), so an env-less harness
aborts pre-POST. The suites depended on the AMBIENT OPENRIG_SESSION_NAME (which masked
this in a managed runner — my own gate was invalid). Fix: each affected suite stubs a
deterministic seat in beforeEach + unstubs in afterEach so it never leaks across files
in singleFork (queue, queue-cross-host-cli, queue-update-metadata-reject, response-
integrity, d14). cross-host-commands: the "explicit --from wins" test is REVISED to the
reversed I4 contract (--from ignored → the derived seat origin wins, the forged --from
never rides the argv) — the same class as the send.test.ts --from revisions.
BREAK 2 — P24 migration-fixture-parity: 065/067 were neither listed nor declared-excluded
in migrationsForFullTestDb. Declared as EXCLUSIONS (067 ALTERs inbox/outbox — themselves
excluded from the core edge — so it can't ride the list; 065 extends the excluded
mission_control_actions), matching the 064/066 pattern; suites asserting provenance
migrate them inline. The twin rule: both halves declared in one increment.
Gates: daemon+cli tsc0; the 6 affected CLI files 152/152 hermetic (env-less);
migration-fixture-parity 5/5; daemon provenance clusters 138/138. (Pre-existing, NOT
P21: host-registry/setup fail under a STRICT multi-var hermetic env at BASE too — an
OPENRIG_HOME leaker in non-P21 suites — out of scope, flagged separately.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The I5 daemon chat route derives the sender from the X-OpenRig-Session transport
header (requireSenderIdentity, refuse-loud — chat is not founder-visible-breaking).
This is the CLI/MCP half: the chatroom + MCP chat verbs stop sending a forgeable
body sender and stop advertising the identity flag as authoritative.
- chatroom send + topic: --sender deprecated + ignored (its hardcoded 'cli' default
would 409 against the seat header); the body `sender` field is dropped (the daemon
derives it). The local send echo now reflects the DERIVED seat env (the header
source), not the retired 'cli' literal.
- MCP rig_chatroom_send: the `sender` param is deprecated + ignored; the hardcoded
'mcp' body value is dropped (the MCP server's DaemonClient stamps the seat env as
X-OpenRig-Session; the daemon derives it).
TARGET/filter --sender on read verbs (history/list) is untouched (it filters, it is
not an identity claim on a write). cli tsc0; chatroom + mcp-server 41/41; the send
test is revised from the 'cli' echo to the derived-seat contract.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Closes the review-actions half of the founder-visible deferral (d00c468d). The
browser UI fires approve/deny/etc via useMissionControlAction HEADERLESS (bearer
only + body actorSession) — my I2 requireSenderIdentity refuse-loud made the
founder's one-tap review 401, the exact founder-visible-break the addendum forbids.
- mission-control /action now uses resolveActorWithDeferral: CLI header ⇒ derive +
transport:v1 + 409-on-mismatch (forgery guard unchanged); UI headerless ⇒ the
body actor recorded as the DECLARED claimed:v1 variant — never refused, never null.
- resolveRecordedProvenance (require-sender-identity.ts) is the SOLE provenance
decider (rail 2 — no hardcoding; the local line + the remote both read it).
Direct ⇒ identity.provenance; relayed ⇒ NEVER transport:v1: a carried transport:v1
marker ⇒ relay:v1, everything else INCLUDING a missing marker ⇒ claimed:v1
(rail 1 degrade-down as the DEFAULT branch — uncertainty weakens, never strengthens).
- the cross-host forward CARRIES its resolved provenance in X-OpenRig-Provenance so a
claimed-era UI action is PRESERVED across the hop, never laundered into verified
(PM charter: no retroactive re-labeling / manufactured provenance).
Types: claimed:v1 is a first-class member of the closed IdentityProvenance union
(dev-driver's I5 shared type) — never null (null = pre-sweep-legacy ONLY, the era
boundary a legacy row vs a founder tap today).
Tests: the I2 headerless→401 test is REVISED (not deleted) to the claimed:v1 deferral
— the diff shows the deliberate contract change (rail 3). NEGATIVE CONTROL (rail 4):
a relayed claimed-era action records claimed:v1 not transport:v1; transport marker ⇒
relay:v1; missing marker ⇒ claimed:v1. Forward-send pins: CLI forward carries
transport:v1, UI forward carries claimed:v1. daemon tsc0; provenance-consumer sweep
192/192 (mission-control, files, review, chat, scope, queue, stream, migrations).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Per the PM-ratified era-boundary pin: claimed-era is a DECLARED typed variant
claimed:v1 in a CLOSED union (transport:v1|relay:v1|claimed:v1), never null — so
a pre-sweep-legacy NULL stays distinguishable from a founder-UI-tap-today.
resolveActorWithDeferral now returns claimed:v1 (not null) on the header-absent
deferral path; ActorWithDeferral.provenance retyped to the non-relay subset.
Adds the shared IdentityProvenance type (the closed alphabet every producer
speaks). I5 files-write + review-freeze deferral-path assertions flipped
null->claimed:v1. null now = pre-sweep-legacy/unstamped ONLY.
dev50 owns (after re-fold): resolveRecordedProvenance + the x-openrig-provenance
wire header + mission-control swap + the forward PRESERVING claimed:v1.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review freeze is the founder's hill-climb surface → resolveActorWithDeferral:
header present ⇒ derive + transport:v1 + 409-on-mismatch (CLI approve flow);
header absent (browser UI review-actions) ⇒ record the body actor CLAIMED-era
(identity_provenance null), never-break — the named deferral (owner=dev50).
Both freeze sinks era-stamped: freezeSliceExport→createAtomic + the FR-8
brief-write→writeAtomic. RED-first pins per path. Completes the I5 daemon set
(chat send/topic + files write + review freeze).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds resolveActorWithDeferral (beside requireSenderIdentity) per rail-addendum
d00c468d: header present => derive + transport:v1 + 409-on-mismatch; header
ABSENT (the browser-UI founder-visible path) => record the body actor CLAIMED-era
(identity_provenance null) instead of refuse-loud — never silent-accept (the null
era-stamp IS the visible gap), never silent-break the founder's flow. files write
uses it; the audit JSONL rows carry identity_provenance. dev50 owns the named-gap
semantics (owner + plumbing-path=daemon-issued per-seat identity) into review50-r1
and will verify this helper matches d00c468d at the fold. RED-first pins per path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
chat send + topic read `sender ?? "anonymous"` from the body (the worst-validated
silent default in the census). I5 derives the sender from the transport header
(requireSenderIdentity): absent → 401 unattributable_sender, differing body claim
→ 409 identity_mismatch, equal tolerated. Chat is not a founder-visible-flow-
breaking surface → refuse-loud is the default (no per-surface deferral).
RED-first pins per verb. Daemon-only; the CLI chatroom --sender (hardcoded "cli")
+ MCP "mcp" adopt-drops ride the I5 fold (coordinated with dev50, matching I3).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The I3 fold made the daemon queue routes derive the actor/sender from the
X-OpenRig-Session transport header (requireSenderIdentity). This is the CLI half:
the queue verbs stop sending forgeable body-identity claims and stop advertising
the identity flags as authoritative — matching the resolve/update/block
adopt-drops and the P21 charter (body identity REMOVED, not just tolerated).
Verbs reconciled (deprecate+ignore the flag, drop the body claim, add an env
pre-check that fails-loud locally when the seat env is unset — the header source):
- create --source → sourceSession dropped
- claim --destination → destinationSession dropped
- unclaim --destination → destinationSession dropped
- update --actor → actorSession dropped
- block --actor → actorSession dropped (writes via the same update route)
- handoff --from → fromSession dropped
- handoff-and-complete --from → fromSession dropped
- outbox-record --sender → senderSession dropped (--sender was still required+sent)
TARGET fields stay body fields (destinationSession/toSession on create/handoff —
the recipient, not the caller). The daemon tolerates an equal body claim in the
adopt-drop window, so no live breakage: the CLI flags defaulted from the seat env
== the header, header==claim==tolerated; the 409 only ever fired on a genuine
flag!=env mismatch (the forgery this closes).
Tests: 5 CLI unit tests revised from the pre-P21 "sends identity from env in the
body" contract to "sends NO body identity claim (daemon derives from the header);
an explicit/forged flag is dropped; the TARGET stays". cli tsc0; CLI queue suite
100/100; daemon queue cluster 130/130; daemon tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
/stream/emit read sourceSession from the body (allow-all); I3 derives it from
the transport header (requireSenderIdentity): absent → 401, differing claim →
409, equal tolerated. stream_items rows carry identity_provenance='transport:v1'
(§4); StreamStore gains the defensive column-detect. RED-first pins; the
missing-source 400 test becomes missing-body; header cascade extended to the
coordination stream/emit POSTs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The live allow-all body-supplied identity sites (specimen-5 family): inbox
absorb/deny read receiverSession from the body, outbox record read senderSession
from the body — all ungated. I3 derives each from the transport header
(requireSenderIdentity): absent → 401, differing claim → 409, equal tolerated.
absorb's absorbed qitem + outbox_entries carry identity_provenance='transport:v1'
(§4); OutboxHandler gains the same defensive column-detect. deny closes identity
without a new era-stamp (the entry keeps its drop-time sender provenance).
RED-first pins per verb; header cascade extended to the round-trip + chain tests.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The /inbox/drop sender is already P18 header-derived (transport chokepoint);
I3 adds the §4 era-stamp: inbox_entries.identity_provenance='transport:v1'.
InboxHandler gains the same defensive column-detect (once, ctor) so pre-067 /
curated-migration DBs degrade to the 8-col insert instead of throwing.
(Forensic correction: inbox drop is NOT an allow-all site — P18 already removed
the pluggable authenticate predicate; the live allow-all body-supplied sites
are inbox absorb/deny receiverSession + outbox record senderSession, next.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
claim + unclaim now derive the claimant from the transport header
(requireSenderIdentity), never body.destinationSession; absent → 401,
differing claim → 409, equal tolerated. The repo's assigned-destination
data-consistency check still composes on top (you can only (un)claim your
own item). Both transitions carry identity_provenance='transport:v1' (§4).
RED-first pins per verb; header cascade extended to the whoami-seed +
coordination claim POSTs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
handoff + handoff-and-complete now derive the actor from the transport header
(requireSenderIdentity), never body.fromSession; absent → 401, differing claim
→ 409, equal claim tolerated. Both the source-close AND new-item transitions
carry identity_provenance='transport:v1' (§4) via the sub-4 foundation.
RED-first pins per verb (401/409/derive+era-stamp); the missing-fromSession
400 test becomes missing-toSession (fromSession is header-derived now). Header
cascade extended to the cross-host-handoff + coordination handoff POSTs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
§4 CLAIMED-vs-DERIVED audit boundary for the queue spine. QueueTransitionLog
writes identity_provenance='transport:v1' when the actor came from the
transport chokepoint; absence = claimed-era (never backfilled/re-labeled).
Defensive column-detect degrades on pre-067 / curated-migration DBs (strict
no-op — 135 full-test-db suites unaffected). create + update routes pass
transport:v1; RED-first pins on both the created and update transitions.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
sub-2/sub-3 gated create+update on the transport header but were validated
only against queue-routes.test.ts; the cross-host + coordination + metadata
suites POST those routes header-less → 18 tests 401. Inject the header from
the body's identity field (header==claim tolerated; bodies untouched). No
production change — repairs the queue-routes-only-gate collateral.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The second spine verb. update's actor is now the transport-derived sender
(requireSenderIdentity on X-OpenRig-Session), never body.actorSession:
- absent → 401 unattributable_sender
- differing body actorSession → 409 identity_mismatch
- equal claim tolerated (adopt-drop window)
The derived actor flows to the update-transition (queue_transitions.actor_session).
RED-first: 401/409/derives pins (the derive asserts queue_transitions.actor_session ==
the header). Cascade: the 3 existing update route POSTs get X-OpenRig-Session = their
actorSession via the same auto-inject trick (header==claim==tolerated). queue-routes 41/41;
tsc/build 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The first spine verb. create's source is now the transport-derived sender
(requireSenderIdentity on X-OpenRig-Session), NEVER a body claim:
- absent header → 401 unattributable_sender
- body sourceSession that DIFFERS → 409 identity_mismatch
- equal body claim tolerated in the adopt-drop window
The derived identity flows to validateTargetRepo, the cross-host re-stamp, and the
create-transition/queue_items — closing the forgeable-source hole (CENSUS queue.ts
create sourceSession). imports dev50's shared requireSenderIdentity (never copied).
RED-first: 401/409/derives pins. The adopt-drop cascade (38 existing route creates that
POST without the header) is resolved by injecting X-OpenRig-Session = each block's
sourceSession — header==claim==tolerated, so bodies stay untouched; the body-sourceSession
field + CLI flag adopt-DROP rides the verb's completion fold, not the green pass. Full
queue-routes suite 38/38; daemon tsc/build 0.
Base: dev50 lane p21/body-identity-sweep. Next: the create-transition era-stamp
(transport:v1 → queue_transitions.identity_provenance, mig-067) + the remaining spine verbs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The I3 foundation: extend the claimed-era vs derived-era boundary (mig-065 pattern) to the
queue-spine identity stores — a NULLABLE identity_provenance column on queue_transitions,
inbox_entries, outbox_entries, stream_items. The I3 routes' transport chokepoint will write
transport:v1; absence = claimed-era (no backfill, per-row honest). Additive + nullable,
identical contract to dev50's 065 mission_control_actions column.
Registered in the prod ALL_MIGRATIONS. RED-first; the test seeds from the SHIPPED ALL_MIGRATIONS
(not the curated createFullTestDb subset, which omits the inbox/outbox base tables) — migration
fixture parity. daemon tsc/build 0.
Base: dev50's P21 lane p21/body-identity-sweep @ 24a71bc58 (I import their requireSenderIdentity;
migration 067 is mine). I3 verbs (requireSenderIdentity wiring + era-stamp writes + validateTargetRepo)
follow on this branch.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The transport surfaces were the live incident's forged surface (specimen 5: a
"From: pm-lead" rendered into a recipient's terminal with zero validation).
Daemon (routes/transport.ts):
- /send + /broadcast derive the --dangerously-interact override AUDIT actor from
the X-OpenRig-Session transport header, never body.actorSession; a body claim
that differs refuses-loud (409 identity_mismatch), an absent header on a
drive-the-prompt override refuses-loud (401 unattributable_sender).
- /broadcast envelopeSender (the From: rendered into EVERY recipient's terminal)
DERIVES from the header per orch ruling (a): a present body.envelopeSender is
only the enveloped MARKER; its value is ignored and the From: is the derived
actor. Unattributable enveloped send refuses-loud — never render an unverified
name (no silent fallback to the body value).
CLI (commands/send.ts): --from is DEPRECATED + IGNORED across all three send
paths (single, cross-host http, fan-out) + the ssh-relay origin-carry — the
rendered From:/actor derive from $OPENRIG_SESSION_NAME (the stamped header), so
a caller-supplied string can no longer forge the From:. Cross-host origin rides
the relay's authenticated context. (ssh-relay: the --from argv is now a
deprecated no-op pending the remote env re-stamp — flagged to orch, rail 2.)
Tests: 4 RED-first daemon pins (spoof kill, enveloped/interact no-header 401,
mismatch 409); the two ba41fea2 --from-provenance CLI tests are REVISED to the
reversed I4 contract (--from ignored, identity from the transport). daemon+cli
tsc0; transport-routes 35/35; send 72/72.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Mission-control action was the widest body-identity fan-out: body.actorSession flowed to FIVE sinks
(the mission_control_actions audit row, the created qitem's source_session, queue_transitions actor,
the owner nudge identity, and the Slack/desktop notification text), and the cross-host forward passed
the claim VERBATIM to the origin daemon — the census's #5/#17 forward sites.
Part A (local): the route derives the actor via the shared requireSenderIdentity chokepoint (401
unattributable / 409 identity_mismatch / adopt-drop transitional window) — the derived actor feeds all
five sinks. identityProvenance threads through MissionControlActionInput → the 3 actionLog.record()
calls, so the audit row carries the P21 era-stamp (transport:v1, absence = claimed-era). The CLI
`rig queue resolve` stops sending body actorSession; --actor deprecated + ignored.
Part B (cross-host re-stamp, the I2 charter): the forward now STRIPS the inbound identity claim (hostId
AND actorSession) and RE-STAMPS X-OpenRig-Session from THIS daemon's derived actor plus an
X-OpenRig-Relay marker — the origin derives the re-stamped actor, never the forwarded body claim.
remoteJsonRequest gains an optional headers field (merged after Content-Type/Authorization). A relayed
action records identityProvenance = relay:v1 (X-OpenRig-Relay present) vs transport:v1 for a direct
call — honest one-hop-derived provenance.
RED-first pin (route): absent header → 401; body claim ≠ header → 409 naming both; header-derivation →
recorded actor is the transport identity. Remote-action pins: the forward re-stamps the header + drops
actorSession (never rides the wire). mission-control routes 13/13 + phase-b 16/16 + write-contract
19/19 + remote-action + action-log 8/8 + audit-browse 11/11 (74/74) + cli queue 83/83; daemon+cli tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Completes I1 (scope-approve, the signing surface) on the 69de3385 core.
Era-stamp (the CLAIMED-era vs DERIVED-era audit boundary, plan §4): migration 065 adds a NULLABLE
identity_provenance column to mission_control_actions (shared by I1 scope-approve + I2 mission-control;
defensive column-detect so pre-065 harnesses degrade). The scope-approve route passes `transport:v1`
(it derived the actor from the transport chokepoint); the service writes it to the audit row AND a
`provenance:` frontmatter line beside approved-by. A DIRECT service call (no chokepoint) leaves it
NULL — absence IS the claimed-era marker, no backfill, no re-label (house absent-never-fabricated).
The audit-browse read path surfaces identityProvenance so a consumer renders a NULL row as
"recorded (pre-verification era)", never "verified". Per-ROW truth (surfaces flip at different folds,
each row stays individually honest).
Adopt-drop: the CLI `rig scope approve` stops sending body actorSession (the DaemonClient header
carries the identity); --actor is deprecated + ignored (non-breaking); the pre-check reframes to the
seat env (OPENRIG_SESSION_NAME — the header source), else the daemon refuses-loud 401.
Tests: scope-reapprove 13/13 (+ era-stamp derived=transport:v1 on audit row + frontmatter; claimed-era
direct call = NULL) + scope-approve 20/20 + append-only 4/4 + mission-control-action-log 8/8 +
audit-browse 11/11 + read-layer 8/8 (63/63, shared store degrades cleanly) + cli scope-reapprove 4/4;
daemon + cli tsc0. Migration 065 (main's highest is 064; 064 lands via the desk fold, 065 additive).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The signing surface (POST /api/scope/approve → on-disk approved-by frontmatter + mission_control_actions
audit row) took the approver from body.actorSession — a caller-supplied identity claim that any
buggy/stale/malicious caller could forge into the channel of record. This is the pervasive body-identity
class P18 fenced as P21; scope-approve is I1 (the ranked signing surface).
Introduces the ONE shared route chokepoint the whole sweep routes through — requireSenderIdentity(c),
generalizing P18's inline /inbox/drop form: the acting seat is DERIVED from the authenticated transport
header (X-OpenRig-Session, stamped once by the CLI DaemonClient from the seat env); absent ⇒ refuse-loud
401 unattributable_sender. Plus the adopt-and-drop transitional window: a body identity field is
tolerated ONLY when it equals the header; a DIFFERENT claim refuses-loud 409 identity_mismatch naming
BOTH values (never silently prefer either) until the field + its CLI flag retire.
scope-approve now derives actorSession from the header; onBehalfOf stays a body field recorded BESIDE
the transport-derived actor (delegation stays data — the reference pair the sweep generalizes).
RED-first pins (route-level): absent header → 401 unattributable_sender; body claim ≠ header → 409
identity_mismatch naming both; header-derivation → the recorded approver is the transport identity even
with no body actorSession. scope-reapprove 11/11 (route test threads the header) + scope-approve 20/20 +
append-only 4/4; daemon tsc0.
Follow-on in this increment: the identity_provenance era-stamp (transport:v1 on the audit row +
frontmatter; absence = claimed-era marker) + retire the --actor flag. Then I2 (mission-control) → I5.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The defect (operator receipt 1caca5be): the loader resolves a scenario's `topology` to a HOST
absolute path and buildRealDeps forwards it UNCHANGED to a CONTAINER daemon (`rig up <hostPath>`),
which reads it inside the container and reports a mystery "Source not found" — the same
host-vs-container path assumption behind the /root staging block and the readable-not-writable block.
Fix (the class, not the instance): a container-mode `stageTopology` seam on the ScenarioDaemon
contract that stages the topology's DIRECTORY into the container (the fixtures reference culture.md +
agents/ relatively, so siblings ride along) and returns the IN-CONTAINER path. The pipeline consumes
it before `up`; host-mode omits the seam (identity — the host path passes through unchanged). The
METHOD (tar-pipe plan + argv + expectFile fence) is imported from dev50's shared module — one method
for runbook and adapter, no re-derivation.
New scenario-container-stage.ts (makeContainerStageTopology) drives the plan's steps
(mkdir → extract-with-tar-stdin → fence) through the docker seam, extended to carry the extract
step's stdin. Loud+named on failure: any non-zero step (INCLUDING the fence, which now asserts the
FILE arrived via expectFile — not just the mkdir'd dir) throws ContainerStageError with the step
diagnostic PROPAGATED, so a masked tar failure (empty-but-writable stage) can never green through
to an EACCES/not-found three steps downstream. The seam contract requires the invoker to check BOTH
the tar and docker exits (a shell pipe masks the first).
Harness-class only (test/helpers + test/) — no pack/image touch, no repin #5. VM gate: staging
driver 3/3 (ordering + masked-tar-fail + fence-fail), adapter test green, parity fence unbroken,
pipeline 17/17 (host-mode unregressed), daemon tsc 0. node_modules per-worktree (readlink verified
inside the worktree, no primary-tree false-green). Docker L6 green is HOST-SIDE — handed off.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Caught while dev-planner was building against this helper: my runbook's fence
asserts a real file (shared.yaml) but the HELPER fenced only the directory —
which mkdir -p had just created. So a delivery that shipped NOTHING would pass
it: a shell pipeline reports only the LAST command's exit status, so a failing
tar piped into a succeeding docker exec yields an empty stage that is perfectly
readable and writable. The fence would go green and the real symptom would
surface as a file-not-found from rig up, three steps downstream.
stageFenceArgv now takes an optional expectFile and targets it (writability
still probed on the directory); stageTopologyPlan threads it through. The
weaker directory-only form remains available for callers with nothing specific
to expect, but the topology path now names its file.
Same class as the four before it: a check that passes while proving nothing.
Co-Authored-By: Claude Fable 5 <[email protected]>
dev-planner asked where the automated pipeline invokes the staging helpers.
The primitives alone leave the ORDER to each consumer, and the order is
load-bearing: the fence must run AFTER extraction (an empty stage passes a
naive read check) and BEFORE anything depends on it, or the first symptom is
an EACCES from rig up three steps downstream instead of a named failure at the
fence. stageTopologyPlan returns mkdir -> extract -> fence with the tar-side
argv attached, so the sequence cannot be re-derived wrongly by either consumer.
Co-Authored-By: Claude Fable 5 <[email protected]>
dev-planner needs the in-container staging convention for the L6 topology
path; without a shared helper we would have two staging methods that drift,
which is the same failure the published-daemon constants already fenced.
Exports the convention both consumers use: CONTAINER_STAGE_ROOT (the exec
user's home), containerStagePath (in-container paths only — the daemon reads
the topology INSIDE the container, so a host absolute path is always wrong),
the tar-pipe argv pair, and the stage fence.
Both live defects are unrepresentable through these helpers: staging under
/root (unreadable to USER openrig) and docker cp's root ownership (readable
but NOT writable, so rig up's pre-launch AGENTS.md write fails EACCES). The
extract argv carries no -u and no chown — extraction runs as the default exec
user, so ownership is correct by construction.
Pins: stage paths reject absolute and escaping names; the extract argv is
asserted to contain no -u/chown; the fence is asserted to probe by DOING
(test -r + touch/rm) and explicitly NOT by reading mode bits, which lie under
ownership, ACLs, and read-only mounts.
Co-Authored-By: Claude Fable 5 <[email protected]>
P25 was filed as build friction (a TS2688 @types/node failure through a
symlinked node_modules). Grounding first, per the dispatch: neither shortcut
reproduces TS2688 — a per-worktree install builds clean, and a symlinked
node_modules ALSO typechecks clean. The clean result from the symlink is the
actual defect.
npm install creates node_modules/@openrig/<pkg> as a RELATIVE symlink
(-> ../../packages/<pkg>). Under a symlinked node_modules those links resolve
against the PRIMARY tree, so a worktree builds and typechecks against source it
does not contain.
Verified by discriminator rather than inferred: with node_modules symlinked, a
deliberate type error added to the WORKTREE's packages/daemon is caught by that
package's own tsc and NOT caught by packages/cli's tsc, which exits 0 — it
checked the primary tree's daemon. A worktree can report green against code
that is not in it. That is a false-green generator, and it is worse than the
friction that was filed.
Documents the rule (install per worktree; never symlink), two adjacent traps
grounded while reproducing — npx tsc with no node_modules silently fetches an
unrelated 12-year-old "tsc" package, and cli tsc needs daemon dist built first
— and a one-line readlink self-check that proves a worktree resolves into
itself before anyone trusts its gates.
Co-Authored-By: Claude Fable 5 <[email protected]>
PM's tests-half ruling rested on send and broadcast rendering through one
composition chokepoint. Grounding showed the premise false where it matters:
broadcast's envelopeSender is authored CLIENT-SIDE from raw env and falls open
to the literal "<unknown sender>" (cli/src/commands/broadcast.ts:123-124) — a
provenance path differing IN KIND from send's, which is precisely the property
lock item 3 seals. A live send capture proves nothing about it.
The citation condition came up EMPTY independently: no hermetic test asserts the
broadcast sender triple. The broadcast cases in pane-envelope.test.ts and
send-header.test.ts assert the To:/scope line only; the triple suites
(self-host-envelope-triple.test.ts, -sweep.test.ts) drive the shared root
directly and name no broadcast case. Not-found-by-assumption is not coverage,
and a passing suite is not a citation — so both roads led to authoring the leg.
Three pins as ruled:
(i) the sender env is set EXPLICITLY on the exec, so the capture tests the
STAMP path; a container carrying no OPENRIG_SESSION_NAME would render the
fall-open and the leg would be testing the fixture, not the property. A
second assertion fails loudly if "<unknown sender>" appears at all.
(ii) the fall-open is recorded as a one-line P21 census observation (site #14,
body-identity class) — an observation, NOT a leg gate; that provenance
question belongs to P21's pooled sweep.
(iii) the PASS predicate quotes item 3 verbatim: the triple member@rig@host,
"unconditionally … with the host token in one fixed deterministic
position", naming the ORIGIN host.
Harness-class: no source, no image change; pin ce803b307 stands.
Co-Authored-By: Claude Fable 5 <[email protected]>
Operator receipt 1c144dd7: the ownership fix proved out (openrig-owned stages,
read+write probes pass, hashes match, all four rigs launched) and Leg A's
cross-host send DELIVERED with the envelope stamped
orch-main@rig-a@host-a0f3adb5 — the 51-09 property demonstrated live. The leg
then blocked: it drove rig send (transport, creates NO queue row) but asserted
a stored source_session on a forwarded qitem (queue). Zero rows, correctly.
GROUNDED IN THE LOCK, NOT IN CONVENIENCE. The delivery-locked proof contract
item 3 reads: "ALWAYS: send, broadcast, and queue sender surfaces all render
the sender triple member@rig@host unconditionally". It names THREE surfaces, so
the property is not "the identity appears somewhere" — it is the same triple
appearing on each surface independently. The answer to "envelope or stored row"
is therefore BOTH, and each must be driven by the verb that actually writes it.
LEG A1 — transport: rig send; assert the rendered envelope + the verbatim
reply round-trip. Already passing; unchanged. Asserts nothing about
the queue.
LEG A2 — queue: rig queue create --host with a unique body discriminator;
assert on the DURABLE row (stored sourceSession is the host-qualified
origin triple; the shipped from-host tag present and unchanged, lock
item 6). Asserts nothing about the pane.
Never infer a queue record from a rendered terminal envelope — action must match
assertion.
FLAGGED, NOT SILENTLY OMITTED: item 3 names a third surface, broadcast. This leg
proves send + queue live; whether broadcast needs a live capture here or is
covered by item 3's "tests" half is a gate call, not a runbook choice.
Harness-class: no source, no image change; pin ce803b307 stands.
Co-Authored-By: Claude Fable 5 <[email protected]>
Operator receipt ee8c4612: the fold-90 read fix proved out — fixtures readable,
byte-identity hashes MATCHED on their first reachable run, all three specs
validate. The next layer is the write half of the same class: docker cp
preserves ROOT ownership, so the stage was readable-but-not-writable to USER
openrig, and rig up's pre-launch delivery WRITES AGENTS.md into it — EACCES at
instantiate, before any seat exists.
DELIVERY: tar-pipe into a default docker exec, which extracts AS openrig, so
ownership is correct BY CONSTRUCTION. No chown, no root exec anywhere in the
procedure — the same principle already on record for the probe: the user the
product runs as is the user that does the work. (The operator's refusal to
chmod mid-proof is what kept this honest; a chmod would have greened the run
while leaving the procedure unable to deliver a usable stage unassisted.)
FENCE: the pre-flight now probes the FULL contract — read AND write — and it
probes by DOING (a real touch/rm), not by reading mode bits, which can lie
under ownership, ACLs, or a read-only mount. Each failure names the actual exec
user, the offending path, and the reason, so the next drift self-diagnoses.
Harness-class: no source, no image change; pin ce803b307 stands.
Co-Authored-By: Claude Fable 5 <[email protected]>
Kills the silent-omission tax (064/066/067 each had to be hand-added to a
curated list or tests failed confusingly with a missing column/table). The new
guard asserts migrationsForFullTestDb (the ~135-consumer shared fixture) is an
EXPLICIT subset of ALL_MIGRATIONS: every shipped migration is either listed or
declared in a co-located exclusions map with a RE-EVALUABLE reason. A future
migration added to ALL_MIGRATIONS but forgotten now FAILS LOUD, naming the
migration + the two legal fixes (add it, or declare it) — not a mystery. RED
enumerated the 24 current omissions (each declared with its subsystem reason);
negative-control tests prove the guard FIRES on a new undeclared migration + on
stale/redundant exclusions. Assert-not-derive (deriving would auto-include every
future migration and destroy the fixtures' deliberate minimalism).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Operator receipt 7b0f2533: L5.3 died on "sha256sum:
/root/topologies/shared.yaml: Permission denied". The runbook copied fixtures
to /root and then used a default docker exec — but the image declares USER
openrig (Dockerfile:61) with WORKDIR /home/openrig (:62), so the exec runs as
openrig and cannot traverse root's home. The runbook was written against a
root-exec assumption the image never made.
Fix follows the posture L3.2 already proved (its ~/work copy): stage under the
openrig user's own home. Deliberately NOT root-exec — the daemon itself runs as
openrig, so a root probe would exercise a user the product never uses and would
mask exactly the permission defects this testbed exists to surface.
The byte-identity assert STAYS (it is the collision premise; it has caught
nothing yet only because it could not run). Added to the pre-flight fence:
readability AS THE EXEC USER, checked before anything depends on it and failing
loud with the path, the actual exec user, and a directory listing — so a future
USER or permissions change self-diagnoses here instead of surfacing as a bare
"Permission denied" three steps downstream.
SIBLING SWEEP (the habit that paid at L3.2): every other runbook path is
already user-relative — L3 stages via ~/work, no other runbook or the container
adapter references /root. The defect was confined to this file's five
references, all fixed.
Harness-class: no source, no image change; pin ce803b307 stands.
Co-Authored-By: Claude Fable 5 <[email protected]>
dev-planner's adapter caught a real read-side asymmetry: the guarded routers
are gated by the TERMINAL token (server.ts:632 passes deps.terminalBearerToken
into transportRoutes), while publishedDaemonEnv sets the AUTH token. They
coincide under this procedure ONLY because index.ts:160 copies auth -> terminal
when the bind is not trusted — i.e. exactly the 0.0.0.0 bind we mandate. So a
direct curl with Authorization: Bearer works as-is, but a host-side rig read
needs the token under OPENRIG_TERMINAL_BEARER_TOKEN.
Adds TERMINAL_BEARER_ENV + rigReadEnv(token, baseUrl) so both consumers use one
helper instead of hand-assembling env, and documents the coupling where it can
be read.
WHY THE NEGATIVE CONTROL IS NOW PINNED: when the terminal token is null the
middleware passes everything through (auth-bearer-token.ts:98-101) — the route
is UNGUARDED. If the bind were ever softened back to loopback while keeping a
bearer, terminalBearerToken would stay null, the guarded probe would answer
without auth, and an auth-probe-only runbook would report a green proving
nothing. The negative control is the assertion that the guard is armed at all,
so the parity suite now fails if the runbook ever loses it.
Co-Authored-By: Claude Fable 5 <[email protected]>
The container adapter still embedded the original L3 mechanics that killed the A/B on BOTH
arms: ephemeral findFreePort() host port (Apple container 1.2.0 rejects ephemeral publish),
a 127.0.0.1-qualified publish (Apple resets on the loopback-qualified form), and a daemon
started with NO explicit bind and NO bearer (unreachable on the default loopback bind).
Imports the ONE published-daemon procedure from testbed-published-daemon.ts (dev50's shared
module — never re-derived; the parity fence catches drift):
- publishArg(hostPort, containerPort) → UNQUALIFIED `P:C` (no 127.0.0.1), throws on ephemeral 0
- hostPort defaults to the EXPLICIT L3_HOST_PORT (19433), never a free port
- publishedDaemonEnvFlags(token) on the `docker exec rig daemon start` → OPENRIG_HOST=0.0.0.0 +
OPENRIG_AUTH_BEARER_TOKEN (the bind demands the bearer; assertBindAuthInvariant — satisfied, not weakened)
- readEnv carries OPENRIG_TERMINAL_BEARER_TOKEN (the guarded /api/transport/* route checks the
TERMINAL token; the CLI sends it via resolveTerminalToken; the daemon uses the same token for both
on a non-trusted 0.0.0.0 bind, index.ts:160) so host-side `rig` reads authenticate to guarded routes
Test asserts the proven procedure (RED-first: the old loopback-publish assertions flip to unqualified),
plus new pins: env flags before the container id, explicit-port default, ephemeral-throws, terminal
bearer in readEnv. Harness-side only (test/helpers) — stays harness-class, no pack/image touch.
VM gate: scenario-container.test.ts 14/14, parity fence 4/4 (unbroken), daemon tsc 0.
Docker L6 green is HOST-SIDE (docker is host-only here) — handed off.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
dev-planner takes the L6 adapter (scenario-container.ts still embeds the
original loopback + ephemeral-publish mechanics) while I own the runbooks and
fixtures — two consumers of ONE procedure, which is precisely how two variants
drift and each proves a different setup.
This is the source of truth both consume: explicit bind (OPENRIG_HOST=0.0.0.0),
the bearer env the bind guard REQUIRES (satisfied, never weakened), an explicit
non-ephemeral host port, the unqualified publish form (Apple 1.2.0 resets on
the loopback-qualified one), and the PROBE SPLIT that must not be collapsed:
/healthz proves REACHABILITY only (it is unauthenticated), a guarded route
proves the AUTH path, and a negative control proves the guard guards.
publishArg refuses ephemeral 0 and publishedDaemonEnv refuses an empty bearer,
so a caller cannot silently reproduce either original defect. The parity test
asserts the L3 runbook prose still carries the module's values — change one,
the other fails loudly.
Harness-class by PM's artifact-keyed rule (test/helpers is not packed and not
baked into the image); pin feeda3a80 stands.
Co-Authored-By: Claude Fable 5 <[email protected]>
Operator run (receipt ece72317) confirmed the fixtures increment load-bearing
and surfaced the gap: L5.2 registers only H_B-on-H_A, so the outbound send
resolves but the VERBATIM REPLY — which both legs require, running from H_B
back to H_A — has no route. A leg registered one way dies at the reply step,
not at the send. Same adopt-by-read procedure, opposite direction.
Co-Authored-By: Claude Fable 5 <[email protected]>
The 51-09 live legs named three rigs the fixture directory never shipped (it
contained only RUNBOOK.md), so both legs died at provisioning on a bare
testbed container. Authored + committed:
topologies/rig-a.yaml rig-a -> seat orch-main@rig-a (H_A)
topologies/rig-b.yaml rig-b -> seat dev-main@rig-b (H_B)
topologies/shared.yaml shared -> seat lead-main@shared (BOTH hosts)
plus a shared culture.md and three minimal stub agents. All runtime: stub —
zero tokens by construction; the specs exist only to give each leg a real seat
on a real daemon.
BYTE-IDENTITY (rail 1): shared.yaml is ONE authored file copied to both hosts,
and the runbook now ASSERTS equality by sha256 across the containers before any
leg runs — the identity IS the collision premise, so it is verified, never
assumed, and never hand-typed twice.
ADOPT-BY-READ (rail 2): the runbook's self-id step no longer uses rig whoami
(it answers for a SEAT; a host container has none). It reuses the L3/L5
procedure verbatim — capture selfHostId from each daemon's own /healthz — so
one identity-resolution method serves all three runbooks instead of two that
can drift.
THIRD STALENESS FOUND WHILE AUTHORING: the legs' seat names (orch@rig-a,
dev@rig-b, lead@shared) are NOT derivable — session names are always
deriveCanonicalSessionName = <pod>-<member>@<rig>, which always carries a
hyphen. The steps and assertions now use the real derived names, with the
non-derivability stated so the single-token form is not reintroduced.
Never push.
Co-Authored-By: Claude Fable 5 <[email protected]>
L1-L4 are green (operator receipt 814cd300); L5 failed on four components,
three of which are runbook-vs-shipped-surface staleness and are fixed here.
(1) IDENTITY — adopt-by-read (PM ruling): the runbook now CAPTURES each
container's minted selfHostId from its own /healthz (server.ts:589) and
uses that value verbatim, at every provisioning. It never predicts,
hardcodes, or derives an id. OPENRIG_SELF_HOST_ID was never a knob:
entrypoint.sh:8 only echoes it for docker logs and nothing in the daemon
reads it — the runbook no longer sets it.
(2) HOST-ADD GRAMMAR: "rig host add B --url ..." -> the shipped form
(host.ts:427-433) with required --id/--transport, --url for http, and a
bearer POINTER (--bearer-env NAME), never a token value.
(3) BIND + BEARER CARRIED FROM L3 verbatim: L5 needs cross-container HTTP, so
each daemon binds 0.0.0.0 with OPENRIG_AUTH_BEARER_TOKEN set — satisfying
assertBindAuthInvariant (auth-bearer-token.ts:240-264), never weakening it.
(4) whoami-without-seat is left as a captured product observation with a note,
not worked around; the daemon-surface read is the shipped substitute.
L6: the literal scenario-collision.yaml DOES NOT EXIST in the tree (operator
caught it at re-entry). L6 proves the container RUNNER, so it now drives the
committed scenario-02-baton.yaml in both container- and host-mode, with
L6_SCENARIO documented as an override that must also name a COMMITTED file.
Authoring a real cross-host collision scenario is a separate scenario-
authoring item, not an improvisation inside a runbook.
Co-Authored-By: Claude Fable 5 <[email protected]>
L3.1 passed on the published-daemon procedure (operator receipt dfcdd798:
published healthz green, authenticated transport routed 404-not-401,
unauthenticated negative 401, sqlite container-local). L3.2 then RED:
"missing required argument 'source'".
Two causes, both in the runbook, both now fixed:
- STAGED PATH: the build verb preserves each census entry's full
repo-relative path (build-testbed-image.sh:51-52) and the Dockerfile copies
that context dir wholesale (Dockerfile:52), so the trio lands in-container
at /opt/openrig-testbed/stub-assets/docker/testbed/stub-assets/ — not the
stub-assets root the runbook assumed. The nesting is load-bearing: the same
relative paths are hashed into the byte-reproducible census receipt
(build-testbed-image.sh:101), so flattening the staging would change receipt
bytes r1 verified. The RUNBOOK adapts; the staging stands.
- EXPLICIT SOURCE: rig up takes a required positional (up.ts:77); a bare
invocation is the exact error the operator hit. Now "rig up rig.yaml".
Added a pre-flight fence: assert rig.yaml at the staged path and fail LOUD
naming it (plus a payload listing) rather than surfacing as a downstream
argument error, so a future staging change is self-diagnosing.
PROVENANCE: this failure was PREDICTED and scoped to this runbook by
dev-driver, endorsed in 51-04-STUB-ASSET-TRIO-REVIEW-VERDICT-review-r1.md
(sha-16 5d4a8bb1d0a1605b, "Honest forward-flag") — the note never reached the
runbook text until now; it is quoted in place so it cannot be lost again.
SIBLING SWEEP (desk ask): L4/L5/L6 reference neither the staged payload, nor
rig up, nor the health probe — both defects were L3-only.
Co-Authored-By: Claude Fable 5 <[email protected]>
The A/B at pin feeda3a80 was BLOCKED-BEFORE-MEASUREMENT: L3 failed on BOTH
arms (Docker curl 52, Apple curl 56) because the runbook started a
LOOPBACK-BOUND daemon and probed it through a published port — unreachable by
construction, on any runtime. A control that cannot pass cannot judge the
variable arm, so no verdict was produced (correct) and thresholds stayed
unengaged. Operator receipt: Q2-AB-feeda3a80-20260807-host/AB-RESULT.md
sha256 3433e95f427828ce.
Four coupled mechanics, all grounded at source, IDENTICAL on both arms:
(a) explicit bind OPENRIG_HOST=0.0.0.0 (index.ts:148 reads it; unset defaults
to 127.0.0.1 at :167 — that default is the whole defect);
(b) OPENRIG_AUTH_BEARER_TOKEN set, because assertBindAuthInvariant
(auth-bearer-token.ts:240-264) REFUSES to start a non-loopback bind
without it — the procedure SATISFIES the guard, never weakens or
special-cases it (PM rail: build the missing piece, never soften the gate);
(c) an explicit host port, never 0 — Apple 1.2.0 rejects ephemeral publish
where Docker accepts it (captured parity difference, not an A/B variable);
(d) two probes, two distinct facts, both recorded.
VERIFIED-AT-SOURCE CORRECTION to the dispatched shape: /healthz is registered
directly on the app (server.ts:574) and is UNAUTHENTICATED — there is no
global auth middleware (the app.use("*") at :441 only injects deps) and the
bearer middleware is mounted inside exactly six route modules. So the health
probe does NOT 401 without a header and must not be scored on one. The bearer
is proven separately on a genuinely guarded surface (/api/transport/*, whose
router guards "*" at transport.ts:9), plus a NEGATIVE CONTROL asserting the
same call without a bearer IS 401 — otherwise the runbook would claim to test
an auth path it never exercised.
Acceptance gate written into the runbook: prove the DOCKER BASELINE GREEN
under this exact text before any A/B rerun is requested.
Co-Authored-By: Claude Fable 5 <[email protected]>
The spec model (member.model ?? profile.preferences.model ?? spec.defaults.model) already
resolves onto binding.model at instantiate (rigspec-instantiator :1728), but the claude-code
adapter never emitted it — so a claude seat ignored its spec model and always booted on the
CLI default. This is the one load-bearing gap the 51-07 grounding found (codex -m and pi
--model already consume it).
Adds `--model <binding.model>` (shellQuote, already imported) to the fork, resume, and fresh
launch templates, guarded by binding.model?.trim(). Mirrors the codex modelArg pattern.
D1 model-only fence (ruled): strictly ADDITIVE — modelArg sits beside permissionMode but never
alters it; absent model → empty string → the command is byte-identical (regression pin). The
restore path (claude-resume.ts) + native resume-cmd builder are the named A2 restore-parity
follow-on (P6 enumeration), not this atom.
RED-first (extends claude-runtime-adapter.test.ts, 0 model coverage before): the three --model
builders failed on main (demonstrates-the-miss); the byte-identical + posture-unchanged-both-
directions pins stay green through the change. Gate: 51-07 block 5/5, adapter suite 30/30,
codex adapter 49/49 UNCHANGED, claude-launch blast radius 90/90, daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The desk-caught defect: the full ui run reported "Errors 1" with a fully
green test count (ReferenceError: window is not defined at react-dom
performWorkOnRootViaSchedulerTask, thrown after environment teardown),
which flips the exit code that gates the A/B pin — and per vitest's own
warning can produce FALSE POSITIVE tests.
STRUCTURAL CAUSE (verified at source, not inferred): vitest.config.ts
leaves the "globals" option at its default FALSE, and React Testing Library
only registers its automatic afterEach(cleanup) when the framework afterEach
is a global. So auto-cleanup NEVER RAN: 117 of 153 test files left their
React trees mounted past the test. A mounted tree still has scheduled work;
when that task lands after the environment is disposed it dereferences a
window that no longer exists. The components are innocent —
TerminalPreviewPopover already cancels its rAF in effect cleanup, but effect
cleanup only runs on UNMOUNT, which never happened.
Fix: register cleanup globally in test/setup.ts (unmount, never suppress) —
the R5-family "work must not outlive its context" pattern in the React
harness. RED pin: harness-teardown-hygiene.test.tsx fails deterministically
without the registration (a rendered tree survives into the next test).
HONESTY NOTE: the original error did NOT reproduce in 6 full-suite runs at
this SHA before the fix (it is load/order-sensitive), so this is not an A/B
proof that the flake is gone — it removes the documented precondition of the
reported stack and pins that removal deterministically. 197/197 files, 1723
tests green after the change (no test depended on leaked DOM); tsc 0.
Co-Authored-By: Claude Fable 5 <[email protected]>
Host verify at 4a8d7162 (toolchain fix WORKED — Python found) uncovered break #5 one layer
deeper: `npm install -g` left an EMPTY <cli>/node_modules/better-sqlite3 (no binding.gyp →
'prebuild-install: not found' + 'binding.gyp not found', exit 1).
ROOT (VM-verified, docker-free; corroborated on the operator's artifact of record sha256
7920fdd3 — daemon present ~1826, better-sqlite3 0, matching the VM pack): the BUNDLED
@openrig/daemon package.json declared its cli-SUBSET deps (better-sqlite3 + hono/tar/ulid/
yaml/@hono/node-server/@hono/node-ws), so npm treats them as bundle-provided UNDER
@openrig/daemon and leaves an empty better-sqlite3 at the cli level instead of fetching it
COMPLETE from the registry. NOT a bundle-not-shipping bug (the pack ships the daemon) and NOT
a nesting/completeness bug in better-sqlite3.
FIX: build-package.sh strips the bundled daemon's dependency fields (deps are a strict subset
of the cli's, fetched once at the cli level; node resolves at runtime by hoisting — package.json
`dependencies` is only consulted by npm at install time). VM-verified: strip → install exit 0,
better-sqlite3 complete (binding.gyp + package.json), rig --version = 0.5.0.
RESIDENT INSTALL RED (replaces the false-greening pack-content assertion — 'daemon present +
better-sqlite3 absent' PASSES on the broken pack): build-testbed-cli-pack.test.mjs installs the
pack --ignore-scripts into a temp prefix and asserts a COMPLETE better-sqlite3 (binding.gyp
present). Toolchain-independent, docker-free. Verified discriminating on scratchpad packs:
BROKEN → binding.gyp ABSENT (RED); FIXED → PRESENT (GREEN).
Operator's corrected in-container load adopted (build-testbed-image.sh + fence): --no-kernel +
direct /healthz (deterministic, no fixed sleep) + daemon status + EXIT-trap stop.
PROOF STATUS: fix + install-RED verified in-VM (primary-independent scratchpad packs); bash -n +
image fences 12/12. Container-build GREEN + exact operator-load re-verify remain HOST-SIDE.
Worktree tsc friction (TS2688 @types/node via symlinked node_modules) is orthogonal — a pooled
item, not worked around.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Break #4 in the Q2 series (Dockerfile unchanged since fold 54; revealed by fixing
break #3 — the self-contained tarball now lets npm reach better-sqlite3's native
install). better-sqlite3's install is `prebuild-install || node-gyp rebuild`; the
image had neither a prebuilt path nor a toolchain, so layer 3 died
('prebuild-install: not found' → 'Could not find any Python installation', exit 1).
FIX (a), per the sealed Q2 packaging ruling (better-sqlite3 builds FRESH ON TARGET,
never prebuilt/nested → no wrong-arch binary): layer 1 installs python3 make g++.
Testbed-image scope only (no multi-stage gold-plating).
RIDER (PM, ratified) — close the CLASS 'green gate over a broken install'
(assert-the-EFFECT-not-the-command): the build verb now LOADS the daemon inside the
freshly-built container (rig daemon start → opens the DB → better-sqlite3 must have
bound), reusing the proven L3-daemon-in-container sequence. A host clean-dir install
can't catch this (host has the toolchain); only a container load does. Rides the
pre-pin build verb (not a per-fold docker build); the A/B pin package requires it green.
PROOF STATUS — HONEST: docker is absent in the VM seat BY RULING (build is host-side;
build-testbed-image.sh itself guards `command -v docker` → exit 3). VM-authorable
structural fences GREEN 12/12 (build-testbed-image.test.mjs: toolchain present +
effect-proof load step present) + bash -n clean. The behavioral RED→GREEN (docker
build fails at layer 3 on this branch's parent → builds + daemon loads with this fix)
MUST run HOST-SIDE — handing off. NOT claiming a verified image build.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Follow-up to a310c161 (the docs backfill for 0.4.7 / 0.4.8 / 0.5.0).
The backfilled release-notes surfaces (CHANGELOG entries +
docs/releases/*.md files + GitHub Release bodies) inherited more
internal-workstream jargon from the source commit-body prose than
should ship on user-facing surfaces.
Removed from all three surfaces:
- slice numbers as identifiers (e.g. "slice-12 gates policy")
- issue / seal codes (e.g. "GAP-7 seal CLEAR b7f4cb7d")
- tier / stage classifications (e.g. "Tier-A skill",
"maturation-gated", "0.5.2-lane")
- internal workflow terms (e.g. "back-merge", "release-artifact
history", "working lineage", "fold-wave enumeration", "census not
damage", "delivery-free-noun ruling", "post-cut docs pass",
"sibling of the slice-03 omission")
- internal module / function names (e.g. deliverInitialSessionPrompt,
permission-policy-discovery)
- internal API field names as bare tokens without a plain-language
gloss (lifecycleState / sessionStatus / attention_required /
session_missing)
Kept (still user-facing, still user-configurable):
- policy names: locked / standard / open / yolo / none
- config keys: dontAsk / acceptEdits / bypassPermissions /
--permission-mode / --policy / --context / --body-context
- CLI verbs and flags: rig context / rig walk / rig setup --policy /
rig ps / rig send / rig broadcast / rig scope slice approve
--re-approve --reason
- concrete file paths: ~/.claude/settings.json / config.toml /
docs/reference/developing.md
- Bash-rule patterns as they appear in Claude settings:
Bash(git push:*) / Bash(rig down:*)
Both the docs/releases/*.md files in this repo and the GitHub Release
bodies for v0.4.7 / v0.4.8 / v0.5.0 have been scrubbed to the same
content this commit lands. The older CHANGELOG entries (0.4.6 and
earlier) are left untouched — this pass targets the 0.4.7 / 0.4.8 /
0.5.0 backfill only.
The corresponding release-manager skill in the shared conventions
layer has been updated with an anti-jargon rule under Anti-patterns
so future release-notes writing catches this class before it ships;
that skill change lands separately in the substrate.
Backfills release-notes documentation for the three published releases
that shipped without an in-repo docs pass:
- docs/releases/v0.4.7.md — recovery honesty + starter bootstrap + skills
wave + Slack connector + UI maintenance-mode milestone (web UI moved to
maintenance mode at 0.4.7; CLI/TUI primary from this release forward)
- docs/releases/v0.4.8.md — permission-posture fast-follow + policy-spec
framework (rig setup --policy flag + harness-neutral spec schema; the
built-in template files themselves land in 0.5.0)
- docs/releases/v0.5.0.md — mission control TUI + context library +
permission-policy built-ins (locked/standard/open/yolo/none) +
applying-a-permission-policy skill + permission-guarantee test-pinned +
provider usage observability + plan amendment + honest CLI output +
build discipline
CHANGELOG.md entries added for [0.4.7], [0.4.8], and [0.5.0] with the
same content, prepended above the existing [0.4.6] entry.
Content sourced from the substantive release-commit body prose banked
at cut-time for each ship. No source-code, migration, dependency, or
behavior changes.
Companion to the GitHub Releases created for the same three tags this
session:
- https://github.com/mvschwarz/openrig/releases/tag/v0.4.7
- https://github.com/mvschwarz/openrig/releases/tag/v0.4.8
- https://github.com/mvschwarz/openrig/releases/tag/v0.5.0
Prior gap: tags + npm publish had continued cleanly every cut since
0.4.6, but the CHANGELOG + docs/releases + GitHub Releases surfaces
had fallen behind. This commit closes the CHANGELOG + docs/releases
side; the GitHub Releases side was closed via gh release create
earlier in the session.
review-r1 named this follow-on at the P20 revision: Finding 1 hardened the PER-LOOKUP
throw (broken≠absent → operator_conflict protects that one target). But the WHOLE-TABLE
case is distinct — if projection_manifest is unreadable from BOOT (missing / migration
failed / corrupt), EVERY lastHash lookup throws → EVERY divergent projection classifies
operator_conflict → a protect-ALL degrade in which NO projection ever applies. That is a
SAFE degrade (no data loss) but otherwise SILENT: the operator only discovers nothing
projects.
Characterized handling (small): a boot-time whole-table readability probe on the store
(isReadable — cheap, NEVER throws), consulted at startup; on an unreadable manifest, boot
warns LOUDLY (projections held to PROTECT until the DB is repaired), so the systemic case
is visible, not silent. No change to the per-lookup behavior.
RED-first: store test pins isReadable true(migrated)/false(dropped-table, no throw); a
source wiring pin (projection-manifest-wiring) proves startup consults the probe AND warns
(the boot enable path — a drop is visible, matching the slice's lookup/record pins). P20
suite 40/40; daemon tsc/build 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The P12 gen-check line for wake-issuers (advisor-ruled). WatchdogPolicyEngine now
consults the target's LIVE occupant-generation before firing a GENERATION-bound wake:
- role-bound (job.targetGeneration null) → fire UNCHANGED (gate skipped)
- generation-bound + live gen MATCHES → deliver (still the intended target)
- generation-bound + live gen MISMATCH → SKIP-LOUD (loud reason
target_generation_mismatch → recorded + emitted), structured audit names BOTH
generations (armedForGeneration + liveGeneration); NEVER fires at the successor
- generation-bound + live gen UNKNOWN (null tenure) → DELIVER (fail-open — the
protective act is skipping, so unknown never skips; note-2 inversion)
resolveTargetGeneration injected from sessionRegistry.currentOccupantGenerationForSession
(P12 occupant_tenures) and pinned at source (watchdog-target-gen-wiring.test.ts) — a
drop silently disables the gate (dead-invalidator class). RED-first: the mismatch case
failed on old code (delivered), the 3 fire-unchanged cases passed both before/after
(no regression). daemon tsc 0; (i-c) suite 39/39.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The registration-side ghost (a retired REGISTERER's job firing post-handover) is
already closed (mig 063 + dropArmedByRegisteringGeneration). This adds the FIRE-TIME
complement's input: a nullable target_generation_uuid on watchdog_jobs recording the
occupant-generation a GENERATION-bound wake is meant for.
NULL = ROLE-bound (fire at whoever occupies the seat name — the common case, fires
UNCHANGED); non-NULL opts a job into the fire-time gen-gate (atom-2). Opt-in via a
new RegisterWatchdogJobInput.targetGenerationUuid (default null); no existing arm site
passes it, so all current jobs stay role-bound — the ratified crux pin. Additive ALTER
+ defensive column-detect (066 ⟹ 063); pre-066 dbs degrade the opt-in to role-bound.
Migration registered in all-migrations + test-app createFullTestDb (parity). Store
tests RED-first: NULL default, opt-in round-trip, independent columns, pre-066 degrade.
watchdog-jobs-repository suite 25/25; daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
TRUE-PASS #7 failed on the freshness guard (baseline-fix-packaging: vendored daemon
dist+specs match source) because the PRIMARY checkout held a stale vendored bundle at
packages/cli/daemon — a GITIGNORED build artifact assembled by an old `npm run
build:package`, now behind after 26 folds. A fresh clone has no bundle (guard skips);
the guard fired on a desk leftover, not a source defect.
Durable class-kill: the gate runner removes packages/cli/daemon at gate start, so the
gate tests SOURCE truth and a stale leftover can never poison a run again. Real
package-time assembly is still guarded (the guard covers an assembled-but-stale bundle);
a fresh checkout with no bundle is a force:true no-op.
cleanStaleVendoredBundle(repoRoot, rm=rmSync) — pure/injected, wired into gate-lane.mjs
before the legs run. RED-first: a planted stale bundle is removed; idempotent no-op on a
clean tree. 26 gate-lane node-tests green; smoke run cleans + gates PASS.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The last true-pass blocker: 2 slow-op-recorder 'real server' tests failed in every
full-workspace gate run (loadavg 12.8/7/11) and passed every quiet + worktree run.
FORENSIC CORRECTION: both failed on the SAME assertion — /definitely-missing → 200
not 404 (a ROUTING status), NOT a wall-clock timer. The measurement-only middleware
(isolated in a finally) cannot turn 404→200; the fragility was the tests' full-daemon
boot + a globalThis.fetch monkey-patch under concurrent-workspace execution. Not
CPU-reproducible (loadavg 19-38 passes locally).
Fix (three parts — the injectable-clock discipline's third instance after P6-C + VM-005):
1. Extract createSlowOpRequestMiddleware(recorder, now = () => Date.now()) — an exported
seam with an injectable clock. Production server.ts now app.use's it (inline copy gone).
2. Rework the 2 contract tests onto a HERMETIC minimal Hono app + a fake clock:
deterministic 404, deterministic 5ms-tick durations, no full daemon, no global fetch
patch. Load-independent (verified green at loadavg 38.9).
3. Wiring pin (startup-wiring-pins): moving the contract off the real server deletes the
'prod actually uses this middleware' coverage (the uninjected-service class) — a source
pin asserts server.ts imports AND app.use's the seam.
HONESTY NOTE: the ambient 404→200 instance itself stays UNROOTED, characterized as the
real-server/global-mutation concurrency class. Acceptable because the contract moved
hermetic AND the wiring pin covers the enable path — stated, not buried.
slow-op 14/14, startup-wiring-pins 39 (incl. the new pin), build clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Ghost-stage specimen 5's wake source was gen-1's SESSION-LOCAL memory-only cron,
which lives and dies INSIDE the retiree process. The cutover already terminates
the retiree first (terminate-retiree → respawn-no-k, fold 8c69f9d4), so the class
is satisfied BY DESIGN — this converts it to satisfied-by-TESTED-design.
Adds one assertion to the committed fold-57 money-proof e2e: capture the retiree's
pane process (pane_pid) before cutover, and after handover assert `kill -0` fails
(process gone) on BOTH the graceful and forced retiree-exit paths. A regression
that let the retiree survive the cutover (or respawned without terminating first)
flips this red. e2e 1/1 (isolated -L tmux, both paths); daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
TRUE-PASS #3 caught one deterministic regression from fold 76 (227cd05b): my own
P6(A) chain-pin guard 'external-canon-pending allowlist is minimal + self-policing'
asserted each external-canon entry is DIGEST-tracked. But fold 76's disk-truth digest
regen correctly OMITS a digest for a file not on disk (hashing a ghost is meaningless),
so oversight-team/retiring are no longer in the digest manifest — the predicted
interaction.
Fix: the guard's 'loud, not silent' precondition now reads the LAYOUT/ship-set, not the
digest manifest. The external-canon skills are loud because the founder LAYOUT still
demands them (the control-plane staleness check flags layout-missing, tolerated only by
the named allowlist) — layout = authority, disk = reality. The minimality property is
unchanged: an entry off the ship set, off the layout, or actually on disk still fails.
Chain pin 5/5 green; slow-op-recorder x2 green at this tree across reruns (their
primary-under-load reds are the F1-phantom load class, not a fold-76 code regression —
reclassified by the desk's fold-worktree full-run at the same tip).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review-r1 MEDIUM: classify's manifest-lookup catch set lastHash=null on a THROW,
falling to hash_conflict — which OVERWRITES (delivered + overwritten). That
conflates a BROKEN read (threw) with an ABSENT entry (null), and the comment
"a manifest read error must never green-light an overwrite" was factually wrong
(hash_conflict IS an overwrite). True fail-closed on a data-safety slice: a
lookup THROW -> operator_conflict (PROTECT, held from delivery), distinct from
null -> hash_conflict (the benign P17 fallback for a genuinely-absent entry).
Corrected the catch + the doc comment + the inline comment. The throw case now
composes with atom-4 protect (broken read -> held back, never overwritten).
Test: flipped the mis-encoded "THROWS -> hash_conflict" case to
"THROWS -> operator_conflict" (RED-first, was green on the buggy contract) +
added a BROKEN≠ABSENT guard pinning null->hash_conflict vs throw->operator_conflict.
review-r1 LOW: added the symmetric startup record-at-apply wiring pin (startup
constructs ProjectionManifestStore(db) + wires recordProjection ->
projectionManifestStore.record({lastHash: hashContent(content)})). A drop
safe-degrades to P17 but silently (manifest never populates) — now visible.
daemon tsc 0; P20 suite 46/46.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Makes the "not overwriting" the operator_conflict warning already claimed
actually TRUE. New pure filterProtectedProjections(files, plan, {force}) holds
back skill targets classified operator_conflict (target diverged from BOTH our
last recorded write and the source — an operator edited it), so the adapter
never overwrites the edit. The instantiator delivers .delivered; force
(opts.force -> node input -> filter) is the operator override, default protect.
projectionConflictWarnings now branches by classification:
- operator_conflict -> "PROTECTED: NOT overwritten; re-run with --force ..."
- hash_conflict (no manifest yet) -> softened transitional overwrite notice
(can't tell operator-vs-stale apart without a recorded last hash).
hash_conflict is NOT protected (stays P17 overwrite-with-warning). Source
wiring pin: the instantiator filters through filterProtectedProjections and
threads the real force flag (a drop/hardcode fails the pin before it can
silently overwrite while the warning claims otherwise).
daemon tsc clean; P20 suite 43/43. Pre-existing base reds (openrig-core-plugin
skill-content, stub-runner-restore-e2e real-spawn timeout) reproduce at the
atom-3b tip without this change — not introduced here.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The discrimination (conflict-detector) + enable path (rigspec-instantiator
wiring pin) are inert unless the projector records what it wrote. This wires
the WRITE side: ClaudeCodeAdapter gains an optional recordProjection dep,
invoked at the skill_install write with the exact target + content it wrote;
startup constructs a ProjectionManifestStore on db and feeds
record({targetPath, lastHash: hashContent(content), writtenAt}).
Non-recorded targets safe-degrade to P17 (lookup null -> hash_conflict).
RED-first: record test fails with the call removed (recorded=0), passes with
it; a send_text delivery records nothing (not a projection).
daemon tsc clean; P20 suite 35/35.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
rigspec-instantiator constructs ProjectionManifestStore(this.db) and injects
lastHashLookup into the production planProjection call — activating the atom-2
discrimination in prod (a divergent target is now discriminated operator-modified
vs stale, not blindly P17-conflicted). Fixes the ENABLE PATH.
MANDATORY WIRING PIN (dead-invalidator/uninjected-service class — orch-named; the
same class the P17 comment records biting resolveTargetPath at the 4.8 restack):
a source-structural pin (RigSpecInstantiator is deep-launch-path, not unit-
constructable) asserts the real store construction + the lastHashLookup wire — a
drop fails HERE before it can ship a silent dead invalidator (lookup null forever).
2/2 green. daemon tsc 0.
(atom 3b record-at-apply — populate the manifest at the adapter write sites — is
the deeper remaining half; until it lands the feature degrades SAFELY to P17:
empty manifest → lookup null → fallback, no data loss.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The manifest that discriminates operator-modified from stale-projection (P17
follow-on): records what the projector last WROTE per target. Net-new table
projection_manifest (target_path PK -> last_hash, written_at, source_spec,
category) — DB over a JSON file because nodes launch concurrently (per-row
upserts are race-free). ProjectionManifestStore: record (upsert-on-write,
idempotent) / get / lastHash (the classify-time convenience). Registered in
ALL_MIGRATIONS + the test-app parity list (migration-parity trap).
RED-first 5/5 (null / record+get / upsert-keeps-last / per-target / null-defaults).
daemon tsc 0. Migration 064 per desk assignment (060-063 folded @ fold 65).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
F1's gate found its second real thing: test:repo's 'node mirror-skills.mjs --check'
staleness gate was RED because folded reality had drifted from the edge-digest
manifest — P6(A)'s 10 skill restores (unexpected) + the architect-line restore
(digest), P6(C)'s plugin hook-script edits (digest), and a prior fold that skipped
the manifest regen (core/cross-host-rig-commands, digest). Plus the external-canon
skills oversight-team/retiring-and-inheriting-a-seat, which the founder LAYOUT lists
as ship-set but that live only in the external founder canon (not in repo git).
This is NOT founder-gated: the digests derive purely from disk + the in-repo layout
(already correct); only membership/denylist/layout re-derivation needs the founder
YAMLs, and those inputs are already right. Exported buildEdgeDigests + a local
disk-truth regen (scripts/regen-edge-digests.mjs) refreshes every present file's hash
to match folded main.
LAYERED PROPERTY (layout = authority, disk = reality, regen touches ONLY reality):
buildEdgeDigests hashes PRESENT files only — a layout-demanded file missing from disk
is never given a digest, so the staleness check stays LOUD about it. The external-canon
layout-missing gets the same self-policing allowlist as P6(A): ONLY the two named skills
are tolerated, ONLY while genuinely absent from disk; every OTHER layout-demanded missing
file stays red (a future accidental deletion is never silently blessed), and a name that
reappears on disk is flagged external-canon-allowlist-stale so the list self-destructs.
Blessing current disk is correct because local main IS the source of truth (never-push).
mirror --check green; mirror node-tests 25/25 (+ the allowlist tolerated/loud/stale test);
daemon control-plane 9/9; docs-guard OK. (The symlinked-worktree bundle-proof artifact is
unrelated + passes in primary.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
F1's closed gap-1 (running the never-counted vitest:ui leg) surfaced the one
deliberately-red VM-005 differential — mission-status-reconcile.test.ts:193. It
was NOT an open candidate: the FR-4 bucket logic landed in main; the red was a
CLOCK-INJECTION TIME-BOMB. The Tier-A differential hardcodes NOW=2026-07-11 for
its 'act' recency slice, but projectMissionBucket/partitionProjectMissions read
real Date.now() (36h window), so the 27-day-stale fixture bucketed 'act' to
archive instead of current. Green at the candidate only when run within 36h of
authoring; it rots after.
Fix (differential-PRESERVING): thread an injectable now through projectMissionBucket
+ partitionProjectMissions (isCurrentProjectSlice already exposed now — the seam was
half-there; the param defaults to Date.now() so the two prod callers are unchanged),
and inject NOW in the differential test. Same injectable-clock discipline as P6(C).
Rails (orch):
- Verified STILL-RED-AT-BASE 8757593f empirically (swapped in base's production, ran
the test: both FR-4 differential cases RED — base lacks FR-4 — while the 'green at
BOTH SHAs' control stayed GREEN). The differential property survives; the fix is
candidate-only.
- Class-kill guard added: a pin that projectMissionBucket + partitionProjectMissions
bucket by the INJECTED now (a long-dead 2020 timestamp buckets current under an
injected clock, archive past the window) — so no future ui differential can hardcode
a wall-clock NOW against un-injectable production time and rot.
Candidate suite 13/13 green; ui tsc clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The PM-ruled exclusion ledger 'with teeth': a base-health suite that genuinely
cannot be fixed before a cut may be excluded from the gate — but only VISIBLY,
OWNED, RECEIPTED, and MECHANICALLY EXPIRING. Ships with an EMPTY seed: the named-6
were KILLED (E/C/D/A + B), not excluded, so main is truly green and no suite is a
resident. This is the durable belt for a future cut.
Four rails (scripts/gate-lane-ledger.mjs, pure/injected → unit-tested):
1. green-with-exclusions — a failed leg COVERED by an active resident → gate
PASS, every resident NAMED in-band (loud, never a silent green).
2. mechanical expiry — a resident past its expiry flips the gate RED regardless
of the suite's current state: it must be removed or re-justified. Self-dying,
the same self-destruct shape as P6(A)'s external-canon-pending allowlist.
3. receipt+owner+expiry — every resident carries an A/B receipt, an owner, and
an expiry; a resident missing any field is rejected loud.
4. cut ceiling — no resident's expiry may outlive the 0.5.2 cut.
Wiring: buildVerdict now resolves the gate against the ledger (backward-compatible
— empty ledger collapses to strict 'pass iff every leg ok'); the entrypoint loads
scripts/gate-lane-exclusions.json (absent/corrupt → empty, never a silent skip)
and prints the ledger state in-band. 25 node-tests green (7 lock + 7 runner + 11
ledger); smoke run: gate PASS, '0 exclusions (clean — no residents)'.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Root cause (three faces of one bug, all from the 0.4.8 whole-set mirror 864cea6b):
- LEG 2 (the silent drop): the PM re-added 10 role skills to product_public.
restored_role_pm_selected, but SHIP_CATEGORIES in mirror-skills.mjs never
consumed that category — the mirror accepted-and-dropped it, so the re-add
never shipped. Fixed: restored_role_pm_selected joins SHIP_CATEGORIES (and
SHIP_CATEGORIES is now exported so the gate can prove consumption).
- LEG 3 (the deleted files): 864cea6b removed the 3 pod + 7 pm SKILL.md files
from the pool and canon. Restored verbatim from 864cea6b^ (canonical==shipped,
architect-cleared, all edges=[canonical,spec] — internal, no plugin change).
- 5th assertion (a sibling over-broad drop): 864cea6b also dropped the
'rig spec audit rig.yaml' advisory line from openrig-architect/SKILL.md while
the command + startup guide kept it. Restored (pool + canon).
THE CLASS-KILL — a new chain gate (test/skill-refs-membership-chain.test.ts) so a
stranding has no layer to hide in, each list validated against its consumer:
Leg 1 refs ⊆ membership (agent.yaml → oracle)
Leg 2 membership ⊆ SHIP_CATEGORIES (oracle category → mirror consumer)
Leg 3 ship-set ⊆ disk (oracle-ships → file-exists, edge-aware)
Each leg has a fixture RED proving it catches its 864cea6b face. Leg 3 excludes a
NAMED, self-policing external-canon-pending set (oversight-team,
retiring-and-inheriting-a-seat — ship_after_fix, digest-tracked, not in repo git):
a guard asserts every entry is ship-set + digest-tracked + absent-from-disk, so no
real stranding can park there. Mirror node-test updated (six shipping categories).
Founder-gated tail (NOT in this commit): the full mirror APPLY (regen the edge
digests + mirror the 2 external-canon-only skills into the repo) needs
OPENRIG_SKILL_CANON_ROOT + the authority YAMLs, which are unset here. The repo
restores + the chain gate are green in the daemon vitest suite (79/79) and the
mirror node-test (24/24); the digest regen awaits a founder mirror run.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The openrig-core-plugin suite drifted against the shipped plugin after the whole-set skill
mirror passes. RED-provenance per assertion (real-regression vs stale-expectation), owner
verdicts + desk/PM rulings cited:
1. Skill count 10 → 12 (STALE): the 0.4.8 mirror (864cea6b) added applying-a-permission-policy
+ delegating-work, which that commit declares TIER-A explicitly; skill-edge-layout marks both
edges=["plugin"] = intended plugin members (dev-driver edge resolution; (A) pod-restore is
edges=["canonical","spec"] so it adds ZERO to the plugin — decoupled, no leapfrog). Update
EXPECTED_SKILLS=12 + README "Skills (12)".
2. openrig-user routing (STALE): the 0.5.0 mirror (cabd2b2f) REVERSED the routing — openrig-user is
now a self-contained CLI guide and the openrig-skills index is the router. Assert the index names
every shipped skill (its membership invariant) instead of openrig-user pointing at the index.
3. Index path form (STALE): cabd2b2f rewrote the index to a human map with repo paths
(packages/daemon/specs/agents/shared/skills/core/...), superseding the ${OPENRIG_CLI_ROOT}/~.openrig
path table. Assert the repo-path form; assert OPENRIG_CLI_ROOT is gone by design.
4. (README count — folded into #1.)
5. Advisory-audit pointer (desk/PM ruling): 864cea6b dropped `rig spec audit rig.yaml` from the
openrig-architect skill. That skill is MIRROR-GENERATED from the external authored canon, so a
pool-only restore would be silently re-deleted at the next mirror pass. PM ruled RESTORE but
UPSTREAM in the canon (mirror-law: fix at source, never patch the generated pool). This test now
asserts the pointer in the STARTUP GUIDE (agent-startup-guide.md — stable, hand-maintained shipped
truth, not mirror-generated); the skill assertion re-adds in a later commit when the canon change
lands + mirrors, with the mirror on our side.
Kill: openrig-core-plugin was one of the named base-health set (named-4 → named-3). openrig-core-plugin
32/32 + plugin parity/projection/files-routes 29/29 + daemon tsc0; zero blast radius (no other test
asserts the plugin count/set).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The stub-runner-restore-e2e real-spawn test timed out at 12s: fireRestore's
bridge payload carried session_name + cwd but OMITTED transcript_path. The
compaction seam records marker.transcriptPath (the stub's own authored
transcript), so the bridge's R5 premise gate — which delivers ONLY when the
delivery event's transcript_path matches the marker's recorded compaction —
correctly refused an identity-less event: delivered=false, the additionalContext
directive never rendered, waitFor timed out.
Same failure form as P6(C)'s determinism pin, now recurring in a SECOND suite:
the fix drives the marker's REAL compaction identity through the gate (the real
SessionStart/UserPromptSubmit event shape), never weakens the gate. fireRestore
now takes transcriptPath; the runner forwards its authored transcript path (the
same one fireCompaction stamps on the marker).
Caught only by the real-spawn e2e — stub-runner-exec mocks io and never hit the
bridge (in-memory-hides-real-spawn). e2e 12s-timeout -> ~160ms green, 3x stable;
stub-runner-exec 11/11, build clean, no regression.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
InboxHandler was constructed allow-all (authenticate defaulted to () => true) and /inbox/drop
forwarded BOTH the claimed sender (body.senderSession, written to inbox_entries.sender_session)
and the "authenticated" principal (body.authenticatedSender) straight from the request body — so a
buggy/stale (or malicious) caller could write false history into the channel of record, which the
receiver then absorbs into their queue and acts on.
Fix (PM approve-actor pattern, one chokepoint):
- CLI: DaemonClient.fetch stamps X-OpenRig-Session, derived ONCE from the seat env
(OPENRIG_SESSION_NAME) on every request — spread LAST so the transport identity is authoritative
and cannot be overridden by a caller header or a request body. Absent env ⇒ no header.
- /inbox/drop route: reads the sender ONLY from the X-OpenRig-Session header; body.senderSession and
body.authenticatedSender are REMOVED (no live body fallback — that fallback IS the vuln); refuses
unattributable LOUD (401) naming the missing header when it is absent.
- InboxHandler.drop: dropped the pluggable authenticate predicate + authenticatedSender param (the
allow-all default was the fabricated-authority surface); senderSession is now the trusted
transport-derived value its route caller supplies.
- CLI inbox-drop: stops sending body senderSession; --sender deprecated + ignored (non-breaking).
Seed-4 cross-check (ghost-stage): /inbox/drop IS a distinct unenveloped-authority injection path
into the channel of record (drop → inbox_entries.sender_session → absorb → the receiver's queue),
SEPARATE from ghost-stage's compaction-restore→pane path but the same fabricated-authority class —
so seed-4's "separate injection path?" = YES; P18 closes the inbox one.
Scope-fenced to the inbox path. The pervasive body-identity finding (send / scope-approve /
mission-control actorSession; outbox-record) + the unforgeable-channel hardening pool as P21 (arch-routed).
Tests: queue-routes P18 proof 2/2 (forged body sender ignored → header wins; no header → 401 loud) +
inbox-handler 8/8 + cli client-headers 7/7 (env-derived, absent→none, chokepoint stamps,
transport-authoritative) + blast radius (coordination-integration, view-event-bridge) 57/57 daemon +
108/108 cli; daemon+cli tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two orthogonal failure modes in the compaction-clock-injection suite:
(1) SHARED-PATH CROSS-WRITER (desk-ruled): the packet base was the FIXED
/tmp/claude-compaction-restore. Under the injected clock the timestamp STAMP is
fixed, so two concurrent writers with the same sessionId land on the same
${sessionId}-${stamp} dir and race the same restore-summary.json. Fix mirrors
the injectable-clock seam: a bounded OPENRIG_COMPACTION_OUT_ROOT — /tmp default
in production, per-run isolated when set — so each test owns a unique outputDir.
New RED pins: SEAM (packet lands under the injected root, not /tmp) + ISOLATION
PIN (two concurrent same-clock/same-sessionId writers under distinct roots
produce independent uncorrupted packets).
(2) DETERMINISM-PIN deliveredAt=null (a SECOND mode beyond the shared-path
attribution): the pin's UserPromptSubmit event passed a bare, identity-less
payload; the bridge's R5 premise gate correctly refuses to deliver a marker
whose recorded compaction identity does not match the event. Fix drives delivery
through the gate (pass the marker's transcript identity — the real event shape),
NOT by weakening the gate.
Suite 7/7 green; 104 compaction-consumer tests green, no regression.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A4: role_no_live_preferred_target existed but nothing ever constructed a
SeatLivenessCheckFn (instantiate passed undefined explicitly). The runtime
now owns the default probe (the hostRegistryLookup sibling precedent): a
preferred target is live iff a session with that exact name is running in
this daemon's DB; validate + instantiate both inject it; ADVISORY-NEVER-THROW
held — a partial-schema DB cannot vouch either way, so it never warns and
never fails the caller (the member-advisory precedent; first cut broke 83
tests on bare fixtures before the guard). RED: warns dead -> silences
running -> re-warns stopped.
A5: AskService's PsProjectionService omitted seatActivity while the
attention path injected it — divergent terminalActive truths. The ask path
injects the same service; the P16 A5 it.todo graduates to a live wiring pin.
Co-Authored-By: Claude Fable 5 <[email protected]>
Named-6 death (cause E, env-sensitivity — the disease the scaffold exists to cure). RED-PROVENANCE: the
SCAFFOLD is correct (prepareHermeticEnv copies baseEnv via {...baseEnv} and never touches process.env);
the TEST was the defect. 'does NOT mutate process.env' asserted process.env.OPENRIG_NO_KERNEL not.toBe('1')
— an ABSOLUTE assertion that false-fails when the ambient SEAT env already carries OPENRIG_NO_KERNEL='1'
(desk env has it → 1/6 fail; a scrubbed env → 6/6 pass — the observed env-split). The invariant is
UNCHANGED, not a value: capture beforeNoKernel + assert toBe(beforeNoKernel).
A/B receipt: RED reproduced with OPENRIG_NO_KERNEL=1 ('expected 1 not to be 1'); GREEN after the fix
6/6 BOTH with the var (desk condition) AND without (insensitive either way).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
r1 flagged: the store is unit-tested but the boot WIRING (startup->recordBoot) was
proven only by the live kill-9 e2e; a future wiring drop would pass CI silently.
This cheap boot-level pin (createDaemon boot -> lifecycle row exists) closes that
in-suite gap. 1/1 green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
describeLifecycle (pure): stopped_at present = clean-shutdown; its ABSENCE with a
boot record = no-clean-shutdown (crash/kill-9/power-loss); no record = unknown.
readDaemonLifecycle opens the daemon db READ-ONLY at the D15-resolved path
(explicit OPENRIG_DB else OPENRIG_HOME/openrig.sqlite) — the crash-surviving read:
on a dead pid daemon.json is deleted but the SQLite row persists (my pin). rig
status stopped/stale branch now renders 'cleanly shut down at T' vs
'⚠ no clean shutdown recorded — last seen T' (the money line). A missing db/table
returns null → unknown, never crashes rig status.
RED-first 6/6 (describeLifecycle 4 + real-db reader 2). daemon+cli tsc 0.
(kill-9 e2e money proof next, on the isolated-daemon rail.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
startup: mint a boot epoch + recordBoot at boot (beside the 059 identity
reconcile); expose daemonLifecycleStore + daemonBootEpoch via AppDeps.
index post-bind: heartbeat timer (5s) → recordHeartbeat; a write failure logs
and continues (render degrades to an older last-seen honestly).
index shutdown (write-order pin): clear the heartbeat timer BEFORE the stop-write,
then recordStop(bootEpoch) for THIS epoch — a failed stop-write surfaces as
no-clean-shutdown, never a false clean. Store guards not-stopped as belt-and-
suspenders.
Glue over the store data-layer (unit-tested 6/6); the end-to-end integration proof
is atom 4's kill-9 e2e (boot→heartbeat→SIGTERM-clean vs kill-9-no-clean→render).
daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Arch verdict d6a6c1db FLAG-1: NET-NEW own lifecycle table (061, NOT columns on
the 059 identity singleton — identity vs lifecycle is a type + write-cadence
separation). Singleton + boot_epoch; a new boot mints a new epoch, sets
started_at, and CLEARS the prior run's heartbeat + stopped_at (render epoch-guards).
DaemonLifecycleStore: recordBoot / recordHeartbeat / recordStop / get. The
write-order pin lives at the data layer — recordHeartbeat guards not-stopped (a
stray post-stop tick can't advance last-seen), recordStop is terminal-per-epoch;
each write is a single stmt = its own tiny SQLite txn (file-touch/jsonl rejected).
Migration in ALL_MIGRATIONS AND the test-app parity list (migration-parity trap).
RED-first 6/6 incl. new-boot-clears-stopped + the two write-order guards. daemon tsc 0.
(P7 uses 061 not the arch-cited 060 — 060 claimed in flight by the tenure-ledger
lane; own-table semantics are the ruling, number incidental.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The C2 evidence run surfaced a real defect: the P3 test acquired GATE_LANE_PORT (the default 40404) — but
the running gate HOLDS that port while it executes this very suite (test:repo runs gate-lane-lock.test.mjs),
so the acquire EADDRINUSE'd against the parent gate and P3 failed (the gate could not run its own suite
cleanly). P3 now asserts only the named-constant shape; acquire-behavior is covered by the explicit-port
tests. Lock 7/7 green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
F1 gate-lane (arch d6a6c1db, 5 pins). The runner wraps the port-mutex (3c9032ef) around the gate:
- acquire lane (non-blocking) → on contention render the P5 teaching refusal (gate-holder NAMES pid/
started-at; foreign squatter = honest-unknown; BOTH teach the port constant; ALWAYS hard-refuse) and
exit nonzero — never block-wait.
- HONESTY gaps closed: runLegs runs ALL THREE legs — typecheck (lint, incl. P9 typecheck:prep) AND
vitest (test) AND vitest:ui (test:ui — which
> [email protected] test
> npm run test:repo && npm run test:workspaces
> [email protected] test:repo
> node --test scripts/*.test.mjs && node scripts/check-docs-guard.mjs && node scripts/mirror-skills.mjs --check
TAP version 13
# === OpenRig Package Build ===
# Repo root: /private/tmp/f1-gate-lane
# [1/6] Cleaning previous artifacts...
# [2/6] Building daemon...
# > @openrig/[email protected] build
# > tsc && node scripts/copy-workflow-specs.mjs
# [3/6] Building UI...
# > @openrig/[email protected] build
# > tsc && vite build
# vite v6.4.1 building for production...
# transforming...
# ✓ 3 modules transformed.
# ✗ Build failed in 214ms
# error during build:
# [vite:css] Failed to load PostCSS config (searchPath: /private/tmp/f1-gate-lane/packages/ui): [Error] Loading PostCSS Plugin failed: Cannot find module 'tailwindcss'
# Require stack:
# - /private/tmp/f1-gate-lane/packages/ui/postcss.config.js
# (@/private/tmp/f1-gate-lane/packages/ui/postcss.config.js)
# Error: Loading PostCSS Plugin failed: Cannot find module 'tailwindcss'
# Require stack:
# - /private/tmp/f1-gate-lane/packages/ui/postcss.config.js
# (@/private/tmp/f1-gate-lane/packages/ui/postcss.config.js)
# at load (file:///Users/admin/code/openrig-build-source/node_modules/vite/dist/node/chunks/dep-D4NMHUTW.js:11776:11)
# at async Promise.all (index 0)
# at async plugins (file:///Users/admin/code/openrig-build-source/node_modules/vite/dist/node/chunks/dep-D4NMHUTW.js:11805:12)
# at async processResult (file:///Users/admin/code/openrig-build-source/node_modules/vite/dist/node/chunks/dep-D4NMHUTW.js:11876:14)
# file: /Users/admin/code/openrig-build-source/node_modules/@fontsource/inter/400.css
# npm error Lifecycle script `build` failed with error:
# npm error code 1
# npm error path /private/tmp/f1-gate-lane/packages/ui
# npm error workspace @openrig/[email protected]
# npm error location /private/tmp/f1-gate-lane/packages/ui
# npm error command failed
# npm error command sh -c tsc && vite build
# Subtest: pre-check: @openrig/cli, ships the `rig` bin, and BUNDLES the unpublished @openrig/daemon
ok 1 - pre-check: @openrig/cli, ships the `rig` bin, and BUNDLES the unpublished @openrig/daemon
---
duration_ms: 1.246583
type: 'test'
...
# Subtest: bundle proof: the assembled tarball ships @openrig/daemon's exports-map surfaces (standalone resolution)
not ok 2 - bundle proof: the assembled tarball ships @openrig/daemon's exports-map surfaces (standalone resolution)
---
duration_ms: 10533.98125
type: 'test'
location: '/private/tmp/f1-gate-lane/scripts/build-testbed-cli-pack.test.mjs:40:1'
failureType: 'testCodeFailure'
error: 'Command failed: bash /private/tmp/f1-gate-lane/scripts/build-package.sh'
code: 'ERR_TEST_FAILURE'
stack: |-
genericNodeError (node:internal/errors:983:15)
wrappedFn (node:internal/errors:537:14)
checkExecSyncError (node:child_process:916:11)
execFileSync (node:child_process:952:15)
TestContext.<anonymous> (file:///private/tmp/f1-gate-lane/scripts/build-testbed-cli-pack.test.mjs:42:3)
Test.runInAsyncScope (node:async_hooks:214:14)
Test.run (node:internal/test_runner/test:1047:25)
Test.processPendingSubtests (node:internal/test_runner/test:744:18)
Test.postRun (node:internal/test_runner/test:1173:19)
Test.run (node:internal/test_runner/test:1101:12)
...
# Subtest: GATE (target-native): install the tarball + a cli command LOADS the daemon subpath
ok 3 - GATE (target-native): install the tarball + a cli command LOADS the daemon subpath # SKIP opt-in RUN_TESTBED_PACK_GATE=1; needs TARGET build tools (better-sqlite3 builds fresh) — the operator's Docker rerun is this gate
---
duration_ms: 0.073583
type: 'test'
...
# Subtest: is a strict bash script (shebang + set -euo pipefail)
ok 4 - is a strict bash script (shebang + set -euo pipefail)
---
duration_ms: 0.958875
type: 'test'
...
# Subtest: derives the image tag from the git sha (openrig-testbed:<git-sha>)
ok 5 - derives the image tag from the git sha (openrig-testbed:<git-sha>)
---
duration_ms: 0.910583
type: 'test'
...
# Subtest: builds OpenRig from the tree via npm pack — never npm publish, never the registry
ok 6 - builds OpenRig from the tree via npm pack — never npm publish, never the registry
---
duration_ms: 0.20225
type: 'test'
...
# Subtest: runs docker build with the digest-pinned base + tarball build-args
ok 7 - runs docker build with the digest-pinned base + tarball build-args
---
duration_ms: 0.166167
type: 'test'
...
# Subtest: Q2 fix A: packs the ASSEMBLED @openrig/cli (has the `rig` bin), NEVER the private monorepo root
ok 8 - Q2 fix A: packs the ASSEMBLED @openrig/cli (has the `rig` bin), NEVER the private monorepo root
---
duration_ms: 0.129375
type: 'test'
...
# Subtest: Q2 fix B: resolves the target arch HOST-side + passes it explicitly, fail-CLOSED (never a silent amd64 default -> exit 133)
ok 9 - Q2 fix B: resolves the target arch HOST-side + passes it explicitly, fail-CLOSED (never a silent amd64 default -> exit 133)
---
duration_ms: 0.107792
type: 'test'
...
# Subtest: Q2 fix B: the Dockerfile fails CLOSED on an empty TARGETARCH (no silent amd64 default)
ok 10 - Q2 fix B: the Dockerfile fails CLOSED on an empty TARGETARCH (no silent amd64 default)
---
duration_ms: 0.685792
type: 'test'
...
# Subtest: NEVER pushes the image (the never-push fence)
ok 11 - NEVER pushes the image (the never-push fence)
---
duration_ms: 0.100083
type: 'test'
...
# Subtest: consumes the committed base-image slot + the stub-assets list (census scope)
ok 12 - consumes the committed base-image slot + the stub-assets list (census scope)
---
duration_ms: 0.25075
type: 'test'
...
# Subtest: emits the manifest via the tested node orchestrator
ok 13 - emits the manifest via the tested node orchestrator
---
duration_ms: 0.256958
type: 'test'
...
# Subtest: baseline-fix-packaging guard: vendored daemon dist carries slice-09 rig-policy routes when assembled (qitem-20260518054224)
ok 14 - baseline-fix-packaging guard: vendored daemon dist carries slice-09 rig-policy routes when assembled (qitem-20260518054224)
---
duration_ms: 0.407083
type: 'test'
...
# Subtest: baseline-fix-packaging guard: vendored conveyor spec carries slice-01 can_observe edge when assembled (qitem-20260518054046)
ok 15 - baseline-fix-packaging guard: vendored conveyor spec carries slice-01 can_observe edge when assembled (qitem-20260518054046)
---
duration_ms: 0.134792
type: 'test'
...
# Subtest: baseline-fix-packaging guard: vendored daemon dist+specs match source when both exist (general staleness)
ok 16 - baseline-fix-packaging guard: vendored daemon dist+specs match source when both exist (general staleness)
---
duration_ms: 0.109959
type: 'test'
...
# Subtest: findBlockedDocsPaths allows durable docs folders and rejects other docs paths
ok 17 - findBlockedDocsPaths allows durable docs folders and rejects other docs paths
---
duration_ms: 0.604833
type: 'test'
...
# Subtest: buildDocsGuardMessage explains the policy and offending files
ok 18 - buildDocsGuardMessage explains the policy and offending files
---
duration_ms: 0.100125
type: 'test'
...
# Subtest: findBlockedDocsPaths allows the ratified docs/DESIGN.md root placement
ok 19 - findBlockedDocsPaths allows the ratified docs/DESIGN.md root placement
---
duration_ms: 0.05075
type: 'test'
...
# Subtest: the DESIGN.md exception is exact-path — sibling root docs and plan/note folders stay blocked
ok 20 - the DESIGN.md exception is exact-path — sibling root docs and plan/note folders stay blocked
---
duration_ms: 0.446084
type: 'test'
...
# Subtest: the real check-docs-guard.mjs exits 0 against the actual repository
ok 21 - the real check-docs-guard.mjs exits 0 against the actual repository
---
duration_ms: 52.063542
type: 'test'
...
# Subtest: package.json has engines.node set to LTS-only constraint
ok 22 - package.json has engines.node set to LTS-only constraint
---
duration_ms: 0.422625
type: 'test'
...
# Subtest: packages/daemon/package.json has engines.node set to LTS-only constraint
ok 23 - packages/daemon/package.json has engines.node set to LTS-only constraint
---
duration_ms: 0.684542
type: 'test'
...
# Subtest: published CLI allows odd Node majors to reach the postinstall ABI guard
ok 24 - published CLI allows odd Node majors to reach the postinstall ABI guard
---
duration_ms: 0.194625
type: 'test'
...
# Subtest: full mode scans tracked files and fails loudly on a planted leak
ok 25 - full mode scans tracked files and fails loudly on a planted leak
---
duration_ms: 146.369833
type: 'test'
...
# Subtest: full mode passes a clean tracked tree and ignores untracked files
ok 26 - full mode passes a clean tracked tree and ignores untracked files
---
duration_ms: 192.089708
type: 'test'
...
# Subtest: staged mode reads index bytes rather than the worktree
ok 27 - staged mode reads index bytes rather than the worktree
---
duration_ms: 218.138167
type: 'test'
...
# Subtest: staged mode handles rename and deletion without reading missing worktree paths
ok 28 - staged mode handles rename and deletion without reading missing worktree paths
---
duration_ms: 158.752459
type: 'test'
...
# Subtest: range mode scans exact committed bytes between from and to
ok 29 - range mode scans exact committed bytes between from and to
---
duration_ms: 411.246292
type: 'test'
...
# Subtest: range mode rejects a leak from an intermediate pushed commit even when cleaned at to
ok 30 - range mode rejects a leak from an intermediate pushed commit even when cleaned at to
---
duration_ms: 402.052875
type: 'test'
...
# Subtest: each explicit mode refuses a missing generated rules file
ok 31 - each explicit mode refuses a missing generated rules file
---
duration_ms: 207.296542
type: 'test'
...
# Subtest: each explicit mode refuses an empty generated rules object
ok 32 - each explicit mode refuses an empty generated rules object
---
duration_ms: 189.958
type: 'test'
...
# Subtest: Plain B keeps guard enforcement out of root test:repo while fixture tests remain discovered
ok 33 - Plain B keeps guard enforcement out of root test:repo while fixture tests remain discovered
---
duration_ms: 0.339333
type: 'test'
...
# Subtest: T6: build-package.sh stages daemon policies into the assembled package
ok 34 - T6: build-package.sh stages daemon policies into the assembled package
---
duration_ms: 0.479708
type: 'test'
...
# Subtest: T7: the canonical repo-source built-ins are exactly the known four, byte-equal to authority
ok 35 - T7: the canonical repo-source built-ins are exactly the known four, byte-equal to authority
---
duration_ms: 1.715875
type: 'test'
...
# Subtest: npm pack of @openrig/cli includes scripts/check-abi.mjs in tarball
ok 36 - npm pack of @openrig/cli includes scripts/check-abi.mjs in tarball
---
duration_ms: 290.072667
type: 'test'
...
# Subtest: the private product-factory VPS runbook and its pointers do not ship
ok 37 - the private product-factory VPS runbook and its pointers do not ship
---
duration_ms: 5.193459
type: 'test'
...
# Subtest: build-package stages the conventions doc as the daemon's stable-path input, and the package allowlist ships it
ok 38 - build-package stages the conventions doc as the daemon's stable-path input, and the package allowlist ships it
---
duration_ms: 0.627333
type: 'test'
...
# Subtest: staged conventions doc is byte-identical to the repo source (skipped when no assembled package present)
ok 39 - staged conventions doc is byte-identical to the repo source (skipped when no assembled package present) # SKIP no assembled package at packages/cli/daemon — run scripts/build-package.sh to exercise this check
---
duration_ms: 0.078167
type: 'test'
...
# Subtest: acquires the lane on a free port + writes holder-info (pid, started-at)
ok 40 - acquires the lane on a free port + writes holder-info (pid, started-at)
---
duration_ms: 14.332292
type: 'test'
...
# Subtest: gate-vs-gate contention → HARD-REFUSE naming the holder (pid/started-at), non-blocking
ok 41 - gate-vs-gate contention → HARD-REFUSE naming the holder (pid/started-at), non-blocking
---
duration_ms: 2.044209
type: 'test'
...
# Subtest: FOREIGN process on the port + NO holder-info → FAIL-CLOSED 'foreign-holder' (load-115)
ok 42 - FOREIGN process on the port + NO holder-info → FAIL-CLOSED 'foreign-holder' (load-115)
---
duration_ms: 0.588875
type: 'test'
...
# Subtest: P2 exclusivity (no SO_REUSEPORT): a second CONCURRENT bind on the same port MUST fail
ok 43 - P2 exclusivity (no SO_REUSEPORT): a second CONCURRENT bind on the same port MUST fail
---
duration_ms: 0.380667
type: 'test'
...
# Subtest: P3: GATE_LANE_PORT is the ONE named lock (numeric, valid range, env-overridable default)
ok 44 - P3: GATE_LANE_PORT is the ONE named lock (numeric, valid range, env-overridable default)
---
duration_ms: 1.189833
type: 'test'
...
# Subtest: P4 best-effort: a failed holder-info write does NOT lose the already-held lane (bind is the lock)
ok 45 - P4 best-effort: a failed holder-info write does NOT lose the already-held lane (bind is the lock)
---
duration_ms: 0.726542
type: 'test'
...
# Subtest: release frees the lane (kernel-released) so a subsequent acquire succeeds
ok 46 - release frees the lane (kernel-released) so a subsequent acquire succeeds
---
duration_ms: 0.88075
type: 'test'
...
# Subtest: P5 refusal — gate holder is NAMED (pid/started-at) + teaches the port constant, always refuses
ok 47 - P5 refusal — gate holder is NAMED (pid/started-at) + teaches the port constant, always refuses
---
duration_ms: 0.411292
type: 'test'
...
# Subtest: P5 refusal — foreign squatter is HONEST-UNKNOWN + still teaches the port constant + refuses
ok 48 - P5 refusal — foreign squatter is HONEST-UNKNOWN + still teaches the port constant + refuses
---
duration_ms: 0.141375
type: 'test'
...
# Subtest: HONESTY gap-1: runLegs runs ALL THREE legs incl. test:ui (npm test excludes ui)
ok 49 - HONESTY gap-1: runLegs runs ALL THREE legs incl. test:ui (npm test excludes ui)
---
duration_ms: 0.120542
type: 'test'
...
# Subtest: HONESTY gap-2: green = typecheck AND vitest BOTH — one failed leg fails the gate
ok 50 - HONESTY gap-2: green = typecheck AND vitest BOTH — one failed leg fails the gate
---
duration_ms: 0.656208
type: 'test'
...
# Subtest: advisory foreign-load — counts foreign node/vitest/tsc processes + loadavg (never the gate's own pid)
ok 51 - advisory foreign-load — counts foreign node/vitest/tsc processes + loadavg (never the gate's own pid)
---
duration_ms: 0.477958
type: 'test'
...
# Subtest: C2 verdict — a GREEN carries the foreign-load context it ran under (recorded, not just printed)
ok 52 - C2 verdict — a GREEN carries the foreign-load context it ran under (recorded, not just printed)
---
duration_ms: 0.060625
type: 'test'
...
# Subtest: scanner blocks internal paths before reading content
ok 53 - scanner blocks internal paths before reading content
---
duration_ms: 0.994709
type: 'test'
...
# Subtest: scanner reports case-insensitive text tokens with file, token, line, and remedy
ok 54 - scanner reports case-insensitive text tokens with file, token, line, and remedy
---
duration_ms: 0.419334
type: 'test'
...
# Subtest: scanner honors an allowed-context substring on the same line only
ok 55 - scanner honors an allowed-context substring on the same line only
---
duration_ms: 1.964292
type: 'test'
...
# Subtest: scanner finds tokens in NUL-containing bytes and does not crash on clean binary bytes
ok 56 - scanner finds tokens in NUL-containing bytes and does not crash on clean binary bytes
---
duration_ms: 0.339708
type: 'test'
...
# Subtest: scanner returns findings in deterministic path/token/line order
ok 57 - scanner returns findings in deterministic path/token/line order
---
duration_ms: 0.170708
type: 'test'
...
# Subtest: parseChanges extracts file-change and deletion lines from itemize-changes output
ok 58 - parseChanges extracts file-change and deletion lines from itemize-changes output
---
duration_ms: 1.4705
type: 'test'
...
# Subtest: parseChanges returns empty array on a clean (already-mirrored) run
ok 59 - parseChanges returns empty array on a clean (already-mirrored) run
---
duration_ms: 0.11275
type: 'test'
...
# Subtest: buildStaleMessage names the npm script and lists the pending changes
ok 60 - buildStaleMessage names the npm script and lists the pending changes
---
duration_ms: 0.086625
type: 'test'
...
# Subtest: EXCLUDES bars curation-cycle bookkeeping and runtime artifacts from public surface
ok 61 - EXCLUDES bars curation-cycle bookkeeping and runtime artifacts from public surface
---
duration_ms: 0.054167
type: 'test'
...
# Subtest: source SKILL.md inventory is non-empty (sanity check)
ok 62 - source SKILL.md inventory is non-empty (sanity check)
---
duration_ms: 1.345292
type: 'test'
...
# Subtest: mirror is in sync with source (drift-detect via --check)
ok 63 - mirror is in sync with source (drift-detect via --check)
---
duration_ms: 39.807667
type: 'test'
...
# Subtest: excluded patterns are absent in the mirror target
ok 64 - excluded patterns are absent in the mirror target
---
duration_ms: 0.892875
type: 'test'
...
# Subtest: stagePublicSkills applies membership, path, frontmatter, and fence transforms
ok 65 - stagePublicSkills applies membership, path, frontmatter, and fence transforms
---
duration_ms: 3.663042
type: 'test'
...
# Subtest: stagePublicSkills applies existing EXCLUDES before scanning or copying
ok 66 - stagePublicSkills applies existing EXCLUDES before scanning or copying
---
duration_ms: 3.922458
type: 'test'
...
# Subtest: stagePublicSkills recursively strips configured keys and internal values while preserving clean metadata
ok 67 - stagePublicSkills recursively strips configured keys and internal values while preserving clean metadata
---
duration_ms: 1.523667
type: 'test'
...
# Subtest: stagePublicSkills aborts an unmatched internal fence with file and line
ok 68 - stagePublicSkills aborts an unmatched internal fence with file and line
---
duration_ms: 1.1645
type: 'test'
...
# Subtest: stagePublicSkills strips internal fences from non-SKILL Markdown without sanitizing ordinary content
ok 69 - stagePublicSkills strips internal fences from non-SKILL Markdown without sanitizing ordinary content
---
duration_ms: 1.813458
type: 'test'
...
# Subtest: stagePublicSkills rejects an unmatched fence in non-SKILL Markdown with file and line
ok 70 - stagePublicSkills rejects an unmatched fence in non-SKILL Markdown with file and line
---
duration_ms: 1.479167
type: 'test'
...
# Subtest: stagePublicSkills rejects canon file symlinks before reading or staging outside bytes
ok 71 - stagePublicSkills rejects canon file symlinks before reading or staging outside bytes
---
duration_ms: 1.036333
type: 'test'
...
# Subtest: stagePublicSkills rejects canon directory symlinks before recursion or staging outside bytes
ok 72 - stagePublicSkills rejects canon directory symlinks before recursion or staging outside bytes
---
duration_ms: 1.124959
type: 'test'
...
# Subtest: stagePublicSkills is deterministic for the same clean input
ok 73 - stagePublicSkills is deterministic for the same clean input
---
duration_ms: 2.773125
type: 'test'
...
# Subtest: shipSetFromMembership consumes exactly the five shipping categories
ok 74 - shipSetFromMembership consumes exactly the five shipping categories
---
duration_ms: 0.141833
type: 'test'
...
# Subtest: shipSetFromMembership subtracts not_public and pending overlaps
ok 75 - shipSetFromMembership subtracts not_public and pending overlaps
---
duration_ms: 0.078625
type: 'test'
...
# Subtest: checkGeneratedEdges detects layout and digest drift without invoking the leak scanner
ok 76 - checkGeneratedEdges detects layout and digest drift without invoking the leak scanner
---
duration_ms: 6.052
type: 'test'
...
# Subtest: checkGeneratedEdges fails closed on empty or malformed control manifests
ok 77 - checkGeneratedEdges fails closed on empty or malformed control manifests
---
duration_ms: 0.688333
type: 'test'
...
# Subtest: authoring regeneration stages canon and projects exact manifest layouts to all three edges
ok 78 - authoring regeneration stages canon and projects exact manifest layouts to all three edges
---
duration_ms: 40.910166
type: 'test'
...
# Subtest: default apply refreshes controls after regeneration and enumerates every diff
ok 79 - default apply refreshes controls after regeneration and enumerates every diff
---
duration_ms: 0.294792
type: 'test'
...
# Subtest: mirror main --check uses layout/digests and never activates the provisional scanner
ok 80 - mirror main --check uses layout/digests and never activates the provisional scanner
---
duration_ms: 0.2075
type: 'test'
...
# Subtest: checkModeAbsolute retains its one-source/one-target checksum contract
ok 81 - checkModeAbsolute retains its one-source/one-target checksum contract
---
duration_ms: 0.109
type: 'test'
...
# DRIFT: packages/daemon/src/domain/scope/attestation-lineage.generated.ts does not match the canonical derivation.
# Re-run: node scripts/sync-scope-lineage.mjs
# DRIFT: packages/daemon/src/domain/scope/attestation-lineage.generated.ts does not match the canonical derivation.
# Re-run: node scripts/sync-scope-lineage.mjs
# Subtest: the repo is in sync (--check passes on the committed tree)
ok 82 - the repo is in sync (--check passes on the committed tree)
---
duration_ms: 26.638375
type: 'test'
...
# Subtest: a hand-edited generated mirror is DRIFT: --check exits 1 naming the fix
ok 83 - a hand-edited generated mirror is DRIFT: --check exits 1 naming the fix
---
duration_ms: 34.7565
type: 'test'
...
# Subtest: a canonical mutation propagates to BOTH surfaces in one sync (the drift class is structurally dead)
ok 84 - a canonical mutation propagates to BOTH surfaces in one sync (the drift class is structurally dead)
---
duration_ms: 92.882
type: 'test'
...
# Subtest: produces a 64-hex digest + a sorted per-file census receipt over the asset set
ok 85 - produces a 64-hex digest + a sorted per-file census receipt over the asset set
---
duration_ms: 2.509916
type: 'test'
...
# Subtest: REBUILD CONTRACT: identical files => identical digest, INDEPENDENT of input list order
ok 86 - REBUILD CONTRACT: identical files => identical digest, INDEPENDENT of input list order
---
duration_ms: 2.269083
type: 'test'
...
# Subtest: content sensitivity: changing ANY file's bytes changes the digest
ok 87 - content sensitivity: changing ANY file's bytes changes the digest
---
duration_ms: 1.19425
type: 'test'
...
# Subtest: path sensitivity: same content at a different path changes the digest (path is identity)
ok 88 - path sensitivity: same content at a different path changes the digest (path is identity)
---
duration_ms: 0.703667
type: 'test'
...
# Subtest: set sensitivity: adding or dropping a file changes the digest
ok 89 - set sensitivity: adding or dropping a file changes the digest
---
duration_ms: 0.692417
type: 'test'
...
# Subtest: forgery-resistance: a delimiter/quote in a path cannot forge another honest set's digest
ok 90 - forgery-resistance: a delimiter/quote in a path cannot forge another honest set's digest
---
duration_ms: 0.6815
type: 'test'
...
# Subtest: loud-fail: empty asset set (never a silent empty receipt)
ok 91 - loud-fail: empty asset set (never a silent empty receipt)
---
duration_ms: 0.744583
type: 'test'
...
# Subtest: loud-fail: a named asset that does not exist on disk
ok 92 - loud-fail: a named asset that does not exist on disk
---
duration_ms: 0.654375
type: 'test'
...
# Subtest: loud-fail: a path escaping the asset root (containment guard)
ok 93 - loud-fail: a path escaping the asset root (containment guard)
---
duration_ms: 1.002584
type: 'test'
...
# Subtest: parseDigestPinnedBase accepts a digest-pinned ref and returns its name + sha256 digest
ok 94 - parseDigestPinnedBase accepts a digest-pinned ref and returns its name + sha256 digest
---
duration_ms: 0.364875
type: 'test'
...
# Subtest: parseDigestPinnedBase REFUSES a tag-floating (non-@sha256) base — the fence
ok 95 - parseDigestPinnedBase REFUSES a tag-floating (non-@sha256) base — the fence
---
duration_ms: 0.163458
type: 'test'
...
# Subtest: parseDigestPinnedBase REFUSES a malformed digest (short hex / non-hex / wrong algo)
ok 96 - parseDigestPinnedBase REFUSES a malformed digest (short hex / non-hex / wrong algo)
---
duration_ms: 0.094333
type: 'test'
...
# Subtest: parseDigestPinnedBase REFUSES empty / non-string input
ok 97 - parseDigestPinnedBase REFUSES empty / non-string input
---
duration_ms: 0.050542
type: 'test'
...
# Subtest: readBaseImage reads a comment-tolerant single digest-pinned ref from a file
ok 98 - readBaseImage reads a comment-tolerant single digest-pinned ref from a file
---
duration_ms: 0.500958
type: 'test'
...
# Subtest: readBaseImage loud-fails on an UNRESOLVED slot (comment-only — no ref yet)
ok 99 - readBaseImage loud-fails on an UNRESOLVED slot (comment-only — no ref yet)
---
duration_ms: 0.467333
type: 'test'
...
# Subtest: readBaseImage loud-fails on multiple refs (ambiguous) and on a missing file
ok 100 - readBaseImage loud-fails on multiple refs (ambiguous) and on a missing file
---
duration_ms: 0.507958
type: 'test'
...
# Subtest: Dockerfile exists at docker/testbed/Dockerfile
ok 101 - Dockerfile exists at docker/testbed/Dockerfile
---
duration_ms: 0.483834
type: 'test'
...
# Subtest: base is parameterized via ARG BASE_IMAGE + FROM ${BASE_IMAGE} — no floating tag baked in
ok 102 - base is parameterized via ARG BASE_IMAGE + FROM ${BASE_IMAGE} — no floating tag baked in
---
duration_ms: 0.670709
type: 'test'
...
# Subtest: installs the PTY/tmux substrate: tmux AND git
ok 103 - installs the PTY/tmux substrate: tmux AND git
---
duration_ms: 0.13575
type: 'test'
...
# Subtest: node is pinned via ARG NODE_VERSION to an in-range (even LTS) engines version
ok 104 - node is pinned via ARG NODE_VERSION to an in-range (even LTS) engines version
---
duration_ms: 0.132625
type: 'test'
...
# Subtest: OpenRig is installed from a COPY'd local tarball, NEVER the npm registry
ok 105 - OpenRig is installed from a COPY'd local tarball, NEVER the npm registry
---
duration_ms: 0.213458
type: 'test'
...
# Subtest: runs as a non-root `openrig` user (creates it + a trailing USER openrig)
ok 106 - runs as a non-root `openrig` user (creates it + a trailing USER openrig)
---
duration_ms: 0.175875
type: 'test'
...
# Subtest: entrypoint execs under tini/dumb-init (PID1 reaping)
ok 107 - entrypoint execs under tini/dumb-init (PID1 reaping)
---
duration_ms: 0.157667
type: 'test'
...
# Subtest: stages the stub runtime assets into the image (layer 4)
ok 108 - stages the stub runtime assets into the image (layer 4)
---
duration_ms: 0.084208
type: 'test'
...
# Subtest: emits a manifest.json wiring the host-resolved base digest + stub census + identity
ok 109 - emits a manifest.json wiring the host-resolved base digest + stub census + identity
---
duration_ms: 5.08125
type: 'test'
...
# Subtest: also writes the stub-assets census receipt alongside the manifest
ok 110 - also writes the stub-assets census receipt alongside the manifest
---
duration_ms: 2.367625
type: 'test'
...
# Subtest: REBUILD CONTRACT: same inputs => byte-identical manifest.json + receipt on disk
ok 111 - REBUILD CONTRACT: same inputs => byte-identical manifest.json + receipt on disk
---
duration_ms: 2.445041
type: 'test'
...
# Subtest: loud-fail propagates when the base-image slot is unresolved (never a partial manifest)
ok 112 - loud-fail propagates when the base-image slot is unresolved (never a partial manifest)
---
duration_ms: 1.324041
type: 'test'
...
# Subtest: loud-fail on a missing required identity input (delegated to the manifest validator)
ok 113 - loud-fail on a missing required identity input (delegated to the manifest validator)
---
duration_ms: 0.838292
type: 'test'
...
# Subtest: CLI shim emits the manifest from an inputs.json + outDir (exit 0)
ok 114 - CLI shim emits the manifest from an inputs.json + outDir (exit 0)
---
duration_ms: 48.765041
type: 'test'
...
# Subtest: CLI shim fails LOUDLY (non-zero + stderr) on an unresolved base-image slot
ok 115 - CLI shim fails LOUDLY (non-zero + stderr) on an unresolved base-image slot
---
duration_ms: 27.417375
type: 'test'
...
# Subtest: produces the four identity fields + image tag + a sha256 manifest digest
ok 116 - produces the four identity fields + image tag + a sha256 manifest digest
---
duration_ms: 0.645667
type: 'test'
...
# Subtest: REBUILD CONTRACT: same inputs => byte-identical manifest JSON + identical digest
ok 117 - REBUILD CONTRACT: same inputs => byte-identical manifest JSON + identical digest
---
duration_ms: 0.317334
type: 'test'
...
# Subtest: per-field sensitivity: changing ANY identity field changes the digest (no collision)
ok 118 - per-field sensitivity: changing ANY identity field changes the digest (no collision)
---
duration_ms: 0.202959
type: 'test'
...
# Subtest: forgery-resistance: a delimiter/quote inside a value cannot forge another input set's digest
ok 119 - forgery-resistance: a delimiter/quote inside a value cannot forge another input set's digest
---
duration_ms: 0.067292
type: 'test'
...
# Subtest: loud-fail on a missing or empty required identity field (never a silent partial manifest)
ok 120 - loud-fail on a missing or empty required identity field (never a silent partial manifest)
---
duration_ms: 0.292833
type: 'test'
...
# Subtest: stub-assets.list census is populated (empty census = L0.2 FAIL, build verb refuses)
ok 121 - stub-assets.list census is populated (empty census = L0.2 FAIL, build verb refuses)
---
duration_ms: 1.346125
type: 'test'
...
# Subtest: census names EXACTLY the stub trio and every listed file exists
ok 122 - census names EXACTLY the stub trio and every listed file exists
---
duration_ms: 0.440625
type: 'test'
...
# Subtest: L0.2 receipt: deriveStubAssetsHash lists exactly the trio, each with a 64-hex sha256
ok 123 - L0.2 receipt: deriveStubAssetsHash lists exactly the trio, each with a 64-hex sha256
---
duration_ms: 3.627708
type: 'test'
...
# Subtest: lint builds the daemon dist BEFORE the cli tsc
ok 124 - lint builds the daemon dist BEFORE the cli tsc
---
duration_ms: 0.329708
type: 'test'
...
# Subtest: typecheck:prep builds the daemon (emitting the dist the cli tsc consumes)
ok 125 - typecheck:prep builds the daemon (emitting the dist the cli tsc consumes)
---
duration_ms: 0.114333
type: 'test'
...
1..125
# tests 125
# suites 0
# pass 122
# fail 1
# cancelled 0
# skipped 2
# todo 0
# duration_ms 10572.285167 EXCLUDES). green = every leg ok (tsc AND
vitest both legs).
- C2 verdict: observeForeignLoad records the foreign non-gate load (advisory, exit UNCHANGED) IN the
verdict — a green carries the load context it ran under. Verdict written to OPENRIG_GATE_VERDICT.
- release before exit (kernel also frees the port on death).
gate-lane-run.mjs = the pure/injected logic (renderRefusal / runLegs / observeForeignLoad / buildVerdict),
unit-tested; gate-lane.mjs = the entrypoint glue (real mutex + spawnSync legs + ps/loadavg + artifact);
root script added. RED-first: gate-lane-run 6/6 + gate-lane-lock 7/7 (13 total). Smoke-verified
end-to-end: single run PASS + verdict + advisory-load recorded; gate-vs-gate → hard-refuse exit 2 naming
the holder. (Full-legs real run = the C2-style evidence drop, next → PM gate.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
F1 gate-lane (arch d6a6c1db). flock(2) is unavailable on macOS (no shell flock, no native module, node fs
has none); arch RATIFIED mechanism (B) — a bound-localhost-port — because the rationale is the PROPERTY
(machine-wide, kernel-RELEASED-on-death, non-blocking probe, no stale-lock), not the syscall. A bound port
delivers all three with zero native deps. (Unix-socket bind REJECTED for the record: the socket file
lingers post-kill-9 = stale-artifact class.)
Built to the 5 binding pins:
P1 — bind 127.0.0.1 EXPLICITLY.
P2 — exclusivity is LOAD-BEARING: NO SO_REUSEPORT (never set) so a second concurrent bind MUST fail —
with REUSEPORT the mutex silently vanishes. Guard-tested (two raw concurrent binds → EADDRINUSE).
P3 — the PORT NUMBER is the lock name: ONE named constant, one home (GATE_LANE_PORT, env-overridable).
P4 — holder-info file is NAMING-ONLY: written AFTER a successful bind (a pre-bind write = option-C pid-lock
by the back door), best-effort read/cleanup, NEVER consulted for the lock decision (the bind decides).
P5 — foreign-squatter honesty (rendered by the runner): gate-holder → names pid/started-at; foreign →
honest-unknown; both teach the port constant; ALWAYS hard-refuse, never auto-override.
acquireGateLane({port=GATE_LANE_PORT, holderInfoPath}): bind → write holder-info {pid,startedAt}; on
EADDRINUSE → gate-holder (info present, valid) refuse-naming | foreign (absent/corrupt) FAIL-CLOSED
(load-115). release() unlinks + closes (kernel also frees on death).
RED-first: scripts/gate-lane-lock.test.mjs 6/6 (acquire+holder-info / gate-vs-gate refuse-naming / foreign
fail-closed / P2 exclusivity guard / P3 named-constant / release-frees-lane) — runs in the test:repo gate.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Finding A2 (finder-owns): the planner's conflict lane existed but production
never injected resolveTargetPath (dropped in the 4.8 restack — the
instantiator's own §6 comment records the warnings-site threading loss), so
every entry classified safe_projection and operator-modified projected files
were silently overwritten. Now: claudeConflictTargetPath (skill SKILL.md /
subagent basename / guidance CLAUDE.md; merged+dir categories stay deferred)
injected at the production planProjection call; dir-shaped skill sources
compare the representative SKILL.md (a readFile(dir) throw was a false
verdict); every conflict rides the instantiate warnings surface naming the
file, reason, and consequence. CONSCIOUS CALL: overwrite-with-loud-warning,
not conflict-block — without a projection manifest, blocking would break every
legit spec update; manifest-based operator-vs-stale discrimination is the
routed follow-on. RED-first (5 pins incl. a P16-style wiring pin).
Co-Authored-By: Claude Fable 5 <[email protected]>
Param-faithful daemon-backed grammar (provider.ts precedent): --window
parses 90m/1h/2d/bare-hours and ARRIVES as window_hours (option-parity
pin); --json is the verbatim route payload for the scripted detector; the
human render keeps the honest-unknown rail explicit (reason, never a 0
row) and teaches accepted forms on a bad --window without firing a request.
Co-Authored-By: Claude Fable 5 <[email protected]>
GET /api/telemetry/usage/series (raw rows, absolute since/until bounds) +
/usage/top (tokens/hour from summed positive deltas over the actual sample
span — a totals reset counts as a fact, never negative burn; per-window
percent velocity). Honest-unknown rail: <2 in-window samples = explicit
unknown with reason, never a fabricated zero. topN truncation reported via
totalRankedSeats (no silent caps). Option-A negative pin at the response.
Co-Authored-By: Claude Fable 5 <[email protected]>
Plan-lock rev-1 (README d606b9cbe2e5f4e8). Migration 062 (060/061 left for the
in-flight tenure/P7 lanes per the desk pin). Advance-only rows: an idle seat
adds zero rows; context_usage's point-in-time upsert untouched (regression
pin). Provider-window lane persists five_hour/weekly per seat from the same
statusline cache dir the read model scans — first persistence that lane has
ever had. Option-A bar: no account identity column exists (negative pin).
Co-Authored-By: Claude Fable 5 <[email protected]>
Completes Class-B. A queue item for a seat ROLE legitimately continues to the successor, so
the ghost is narrow: an item the RETIRING generation CLAIMED and left in-flight. The successor
reuses the seat name, so the claimant GENERATION — not the name — is the discriminator.
queue_items stamps (migration 063 columns):
- minting_generation_uuid at create (the SOURCE occupant's gen; forensic provenance) — via a
persist-after-INSERT helper mirroring persistSummary, at all three create-new-item sites.
- claimed_by_generation_uuid at claim (the CLAIMANT's gen — the ghost discriminator), cleared at
unclaim. Both guarded by a defensive column-detect so pre-063 harnesses degrade.
releaseClaimsByGeneration(gen): RELEASES (never hard-drops) every in-progress item whose
claimed_by_generation_uuid matches the retiring generation back to pending — clears the claim +
claimed_at, appends an audit transition ("claimant generation retired"). The role work is durable;
the successor re-claims it. Gen-scoped, NEVER name-scoped (the successor's own claims, under the
reused name but its live gen, are untouched); a NULL/empty gen never matches (UNKNOWN != retired).
DefaultOccupantInvalidator's Class-B branch calls it when retiringGeneration is present; startup
wires the queue store into the invalidator.
Tests: queue-repository 46/46 (minting@create, claimed_by@claim + clear@unclaim, release-not-drop
+ audit, successor's live-gen claim untouched, empty/NULL no-op, pending never released) +
occupant-invalidator 6/6 (queue branch releases w/ gen). Blast radius green: queue-routes,
workflow-projector, view-projector, hot-potato-enforcer, seat-handover, startup, watchdog (210/210
across 9 suites); daemon tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Class-B durable stores (queue_items, watchdog_jobs) are keyed by seat NAME, which continues
across a handover — so a name-scoped drop would neutralize the SUCCESSOR's own role entries.
Only an entry bound to the RETIRING generation's specific in-flight work is a ghost. This
sub-commit lands the migration + the watchdog behavior (orch ruling: watchdog first).
Migration 063 (covers BOTH tables, one migration): queue_items gains minting_generation_uuid
+ claimed_by_generation_uuid; watchdog_jobs gains registered_by_generation_uuid. All nullable
+ no default — pre-063 rows and unresolved writers stay NULL, and a NULL generation is UNKNOWN
so the invalidator's gen predicate never matches it (never dropped on unknown — note-2).
Watchdog (3b):
- WatchdogJobsRepository stamps the ARMING occupant's generation at register() (via an injected
resolver = SessionRegistry.currentOccupantGenerationForSession); defensive column-detect
degrades any harness that predates 063 (mirrors queue-repository's detectQueueColumn).
- dropArmedByRegisteringGeneration(gen) stops (auditable terminal_reason, not hard-delete) every
ARMED job whose registering generation matches — gen-scoped, never name-scoped; empty/NULL gen
never matches. A stale wake firing into the successor's context is the specimen; it re-arms.
- DefaultOccupantInvalidator's Class-B branch calls it when retiringGeneration is present.
Call site: SeatHandoverService.commit() now captures retiringGeneration BEFORE registerClaimedSession
mints the successor's tenure (after which the reused name resolves to the successor's gen) and
passes it to invalidateRetiringOccupant. Startup wires the watchdog store + the arm-time resolver.
Queue_items (3a) — claimed-by-retired-gen RELEASE-to-pending — lands in sub-commit 2.
Tests: watchdog-jobs-repository 21/21 (stamp, gen-scoped drop leaves the successor's job, NULL/empty
no-op, pre-063 degrade) + occupant-invalidator 5/5 (watchdog branch fires w/ gen; skipped w/o dep)
+ seat-handover-service 31/31 (call passes the RETIRING gen, proven != successor's post-mint gen)
+ startup 24/24; daemon tsc0. Full daemon suite: 500 files pass; the 6 pre-existing real-spawn
e2e failures (e.g. stub-runner-restore-e2e) reproduce at PURE main 8c69f9d4 — not from this change.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The seat-handover fold (main) added the invalidateRetiringOccupant CALL at
SeatHandoverService.commit() plus an OPTIONAL occupantInvalidator dep, but no concrete
invalidator was ever constructed or injected — so in production the call resolved to
undefined and the re-key invalidation was a silent no-op. Class-A ghost protection (the
in-mem compaction maps 1a-1f + the name-keyed context sidecar 2a) was therefore DEAD.
Wire it: construct the canonical DefaultOccupantInvalidator once in startup (where both
Class-A deps — the compaction enforcer's maps + the ContextUsageStore sidecar — are in
scope) and inject it through AppDeps → server context → the seat route, mirroring the
existing dep-injection chain. An absent enforcer (degraded boot, no sessionTransport)
falls back to a sidecar-only invalidator; never throws.
This is the seam-coexist reconciliation from the fold-55 dedup (orch rail 1): the
dedup keeps MY canonical invalidator (interface + DefaultOccupantInvalidator) and the
production call must keep firing — now pinned. The service-level call-fires stays pinned
in seat-handover-service.test.ts; this adds the startup-wiring pin (deps.occupantInvalidator
constructed + injected). Class-B stays loud-pending (retiringGeneration not yet passed).
startup 24/24 + seat-handover-service 31/31; daemon tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A lifecycle message that waits on a busy / mid-handover successor arrives long after it was
composed, with no sign it was delayed. Stamp the compose→write gap at the WRITE moment so a
delayed delivery is visible forensics.
At the write moment (session-transport.send, after any idle-wait) compute delta = now − the
envelope's compose stampISO and, ONLY when it crosses the 10s threshold (a delayed-delivery
FLAG, not sub-second idle-wait noise), append a relative ' · delivered +Ns' segment to the
Sent: line via appendDeliveredSegment. sent-ISO + the delta gives absolute delivered time for
free. Daemon-write-moment only — the CLI composes, never delivers, so there is no compose twin;
but the ' · delivered ' segment mirrors g's containment discipline (appended only to the Sent:
line within the header block, so a body carrying ' · delivered …' cannot forge it).
Also lands the SINGLE HG-5 baseline change deferred from g (orch scope ruling): the queue-handoff
nudge now stamps a Sent: line (byte-parical with a rig send) and carries the SOURCE seat's atom-B
generation on it (g's already-wired render; a gen resolver injected into QueueRepository via the
validateRig-precedent layering — absent=UNKNOWN=omit, never forged). The nudge threads its stampISO
so its delivered-latency flag rides send() for free.
Threshold is a ruled value ⇒ boundary twins (9.999s → nothing / 10.000s → '+10s', floors) + a
value pin (DELIVERED_LATENCY_FLAG_MS === 10_000); NaN/unparseable delta is a no-op. SendOpts gains
stampISO (moved down from BroadcastOpts) so send() can read it. QueueNudgeTransport.send widened to
carry stampISO (the real impl SessionTransport already accepts it).
Tests: pane-envelope 25/25 (helper: boundary twins, value pin, gen+delivered co-render, containment,
no-Sent no-op, NaN no-op) + session-transport 65/65 (write-moment injection: 30s flags '+30s', 3s
nothing) + queue-repository 40/40 (nudge now stamps + carries source gen + threads stampISO; absent
resolver omits gen) + startup 23/23; daemon tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A delayed lifecycle message composed by a prior occupant of the sender's node would reach
a successor with no mark of which generation authored it. Stamp the SENDER's atom-B
occupant generation onto the envelope so a recipient can attribute it rather than inherit
it as the current occupant's.
Resolved ONCE per fan-out at the transport seam (session-transport, same site as stampISO)
via currentOccupantGenerationForSession; a cross-host --from relay whose sender isn't local
resolves to null → UNKNOWN → the suffix is OMITTED (never forges this host's generation onto
a foreign sender). Rendered as a short first8 suffix on the Sent: line
('Sent: 08-06 17:42Z · gen a1b2c3d4') — discriminates at per-node scale; the ledger keeps
the full uuid for exact joins; no dedicated line (512B caps / brevity).
Both byte-identical twins updated in lockstep (daemon pane-envelope + cli wrapSendBody), with
the cross-package parity anchor asserting an identical literal in both suites. Pins:
- (a) absent/empty gen → omit entirely (no "gen unknown", no forge).
- (b) containment: a body carrying ' · gen …' cannot forge the Sent: header's generation
(the suffix is positionally bound to the header block before the first "---").
- interim: the meta-less queue-handoff nudge carries no Sent:/gen today (absent=omit);
the delivered-at stamp (h) adds the Sent: line there, at which point the gen rides this
same render for free (one HG-5 baseline change, in h — orch scope ruling).
Tests: 6 g cases per twin. daemon pane-envelope 18/18 + cli send-header 17/17 (byte-parity)
+ session-transport 63/63; daemon tsc0 + cli tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Under handover the successor resumes into the same pane and REUSES the name, so the
existing session_mismatch guard passes while context_usage still holds the retiree's
frozen reading (the mixed-gen window before the successor samples). That stale sample
would drive the threshold evaluator — the frozen-88% specimen.
Add a generation guard keyed on the atom-B occupant tenure: a reading sampled strictly
before the live occupant's boot_at belongs to a retired generation and is rejected as
unknown("stale_generation") in both getForNode and getForNodes. Evaluate current-gen
only; a pre-boot sample reports insufficient-data and never leaks its percentage across
the boundary. No migration — uses the existing sampled_at vs the tenure boot_at.
NOTE-2 honored: absent tenure = UNKNOWN → gate inert (never treat unknown as stale);
session_mismatch + freshness remain. Opt-in via resolveOccupantBootAt; unwired = no gate.
Tests: 6 c-id cases (mixed-gen-window reject + no-leak, at/after-boot admit, exact-boot
boundary, unknown-inert, batch path, no-resolver). daemon vitest 26/26 + startup 23/23 +
context-monitor 19/19; tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The ghost-stage ruling's core defect kill (identity layer). A restore stage now captures the
occupant GENERATION at queue time (atom-B currentOccupantTenure via an injected resolver); at
drain the enforcer compares it to the LIVE generation and REFUSES (reason stale_generation) +
drops the stage when they KNOWABLY differ — a successor can no longer inherit a retired
generation's queued compaction prompt, even when enabled.
NOTE-2 (orch): an ABSENT/unknown tenure on EITHER side is UNKNOWN — the gate is INERT (never
compares the captured stale generation as if it were live; the enabled-gate (a) + cutover
invalidation (e) remain the fail-closed layers when identity is unknown). Absent resolver =
inert (backward-compatible). Production: startup wires the resolver to
SessionRegistry.currentOccupantGenerationForSession (session -> node_id -> live tenure), which
returns null (UNKNOWN) on any lookup failure — never a stale match.
Also (orch note-1): the atom-B best-effort mint-warn now carries node_id + kind/verb so a failed
mint is attributable from logs.
RED-first: 4 (b) pins (retired-gen refused+dropped, matching-gen drains, unknown=inert,
no-resolver=inert). enforcer 46/46 + session-registry 43/43 + startup 23/23, tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
P12 verdict 3548d8eb: the occupant-generation identity the ghost-stage gen-scoping
rides on. A sessions row is a REGISTRATION, not a tenure — row-as-tenure mints PHANTOM
generations. Migration 060 adds an APPEND-ONLY occupant_tenures ledger
{node_id, generation_ordinal, generation_uuid, kind, native_session_id_at_boot, boot_at},
minted INSIDE the registry at BOTH verbs (registerSession=initial, registerClaimedSession=
adopt default; seat-handover declares 'handover'). RELAUNCH is a CONTINUATION: mintOccupantTenure
keys on (node_id, native_session_id_at_boot) so the same native session re-mints nothing; a
FRESH native session on the SAME (surviving) node mints the NEXT generation — which is exactly
the outage's real signal (a resume-failed relaunch that lost its lineage LOOKS like a new
occupant). currentOccupantTenure(node) = the live generation consumers compare against.
FAILURE ISOLATION: the mint at the register verbs is BEST-EFFORT (loud-warn once, register
proceeds) — a core session registration must not die because the additive ledger write hit a
db without migration 060; gen-scoped consumers degrade to loud-pending, never silent-wrong.
This also contains the blast radius of ~60 stale-copy test migration lists (pre-existing
migration-parity debt) — the shipped test-db harness (test-app.ts) now includes 060.
HONEST BOUNDARY (labelled in the migration): node_id does NOT survive a rig teardown-recreate
(a recreated node is a new identity) — out of scope. Datapoint: this morning's outage produced
MIXED node_id survival per-seat; the ledger honestly represents both (survival = node_id/registry
path; mint-vs-continue = the native-session boundary; boot_at records which boots minted).
RED-first: 5 tenure pins (initial mint, kind + ordinal increment, relaunch=continuation,
fresh-native=new-generation, per-node ordinals + null). session-registry 43/43, register-path
+ boot integration 216/216, tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The CANONICAL OccupantInvalidator interface (byte-for-byte the shape dev-driver's cutover
wired at SeatHandoverService.commit(), c6a6273b — their file imports this one; one
definition, no twin) + DefaultOccupantInvalidator composing the per-store invalidations:
- Class-A (safe by TIMING at commit, before the successor accumulates name-keyed state —
retiring===successor under a cutover, so the name does NOT isolate): enforcer.invalidateOccupant
+ ContextUsageStore.invalidateOccupantSidecar (unlinks the retiring name's frozen telemetry
sidecar, census 2a).
- Class-B (durable queue_items/watchdog_jobs, seat-ROLE-bearing): a name-scoped drop would
neutralize the SUCCESSOR'S OWN live items, so it MUST gen-scope → LOUD no-op until atom-B
supplies retiringGeneration, NEVER a name-scoped drop.
Composition tested (Class-A hard-deletes retiring; Class-B no-gen logs the loud atom-B-pending
marker + never name-scopes). occupant-invalidator 3/3, context-usage-store green, tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First (e) impl: ClaudeCompactionEnforcer.invalidateOccupant(sessionName) drops EVERY
in-memory compaction-state entry for one seat name (census 1a-1f: lastAutoCompactAt,
postCompactRestoreCooldownUntil, triggeredAboveThreshold, pendingPreCompactPrep,
pendingPostCompactRestore, manualCompactionState), so a handed-over successor under the
same session name inherits none of the predecessor's queued stage / dedup / cooldown —
the ghost prompt. Also closes the census-1f LEAK: manualCompactionState was NEVER deleted
on drain, so a same-name successor read a stale terminal record.
Occupant-scoped (no atom-B needed): the retiring occupant is gone, so a name match IS the
ghost. The cutover seam's OccupantInvalidator (next) calls this at
SeatHandoverService.commit(); Class-B durable stores (queue/watchdog) ride atom-B.
RED-first: queue a stage + a manual record -> invalidateOccupant -> the same-name successor's
below-threshold tick drains NOTHING (reason below_threshold, no send) + getManualCompactionState
is null (1f closed). enforcer suite 42/42, tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The operator disabled legacy compaction; `rig config set` reported success but the
daemon never saw it. Root cause (confirmed): the CLI ConfigStore resolved its write
target via getCompatibleOpenRigPath — existence-based, which prefers a legacy
~/.rigged/config.json SIDECAR when canonical is absent — while the daemon reads
canonical (getOpenRigHome/config.json) ONLY. Set → sidecar; daemon → canonical → stale.
Fix (dev-planner amendment): the ConfigStore default WRITE target is now the canonical
path the daemon reads (getDefaultOpenRigPath, not getCompatibleOpenRigPath); reads align
too (the daemon already ignores ~/.rigged). Plus verify-readback + fail-loud: after every
set() write, re-read this.configPath and confirm the value persisted — refuse loudly on a
mismatch rather than report a phantom success (accept-and-drop family #5). The daemon
SettingsStore twin gets the same verify-readback (defense-in-depth; it already writes
canonical).
Pins: a DEFAULT store writes to <OPENRIG_HOME>/config.json where the daemon reads (RED on
any host with a legacy ~/.rigged) + the value round-trips. cli config 97/97, daemon
settings 71/71, tsc0 both. NOTE: a host-independent ~/.rigged-divergence RED needs
os.homedir() mocking (harness follow-on).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Legacy compaction-stage defect (operator-confirmed ruling 05c174e0): pending restore
stages are keyed by canonical session name and DRAIN before the enabled gate, so a
handed-over successor inherits the predecessor's queued AUTO stage and it fires as an
unenveloped user-channel GHOST prompt with fabricated telemetry.
Fix (a): the below-threshold back-half is now gated by `enabled` — a disabled system
drains NOTHING, including stages queued while it was enabled. This SUPERSEDES the
documented OPR.0.4.3.14 intent (which drained the back-half regardless of enabled);
the stale comment is rewritten in the same diff (the contradictory source guidance is
part of the defect surface).
EXEMPTION (orch-ruled, PM-approved): an OPERATOR-INITIATED manual /compact is
enabled-independent by construction (the operator is the live premise). The exemption
is ACTOR-GATED (PM pin): triggerManualCompact records operatorInitiated (fail-safe
default false); automation calling the manual verb is NOT drain-exempt, so it cannot
launder a drain past gate (a). The manual-INHERITED-across-generations residue is
covered by fix (b)'s generation gate (layered defense, rides atom-B/P12).
RED-first pins: (1) a stage queued while enabled then disabled drains nothing (auto);
(2) PM pin — an automation-initiated manual trigger is refused the exemption (disabled
tick drains nothing). test:951 updated to pass operatorInitiated for the operator path.
Interpretation surfaced here for the PM evidence read (veto if the literal reading was
intended). Enforcer + route suites 47/47, tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The twice-recurred class (validateRig-uninjected, dead occupant-invalidator):
an optional safety dep passes every unit test while production startup never
injects it. One real createDaemon composition (production dbPath — :memory:
deliberately skips the slow-op recorder), then: 25 AppDeps definedness pins +
7 domain-internal wire pins (validateRig behavioral probe, frontier predicate,
transport eventBus + slowOpRecorder, compaction enforcer, lease isAlive,
queue transport attach). FIVE census findings ride as it.todo ledger entries
(A1 occupant-invalidator, A2 dead hash-conflict detector, A3 allow-all inbox
authenticate, A4 dead seat-liveness advisory, A5 divergent seatActivity) —
routed to desk; each todo graduates to a pin when its fix lands. The 51-08
sampler pin graduates at that slice's fold.
Co-Authored-By: Claude Fable 5 <[email protected]>
WRITER-EXCEEDS-ITS-OWNERSHIP (PM-ruled, found at the 51-08 lock-verify): the
stamp re-serialized the whole frontmatter block, so approving a slice
invalidated any seal taken over the file — seal-then-lock broke by
construction. Each owned key now serializes alone and splices by index:
replace-in-place when present, append when absent; unowned bytes (folded
scalars, quoting, ordering) are byte-preserved, and stripping exactly the
owned lines restores the pre-stamp file. RED = the 51-08 drift instance;
compat belt: all existing approve/re-approve/projection suites unchanged.
Co-Authored-By: Claude Fable 5 <[email protected]>
The blast-radius class: hand-copied migration subsets drift from
ALL_MIGRATIONS, silently suppressing production paths (the workflow member
advisory was invisible because the subset schema made its probe error-skip
— the ADVISORY-NEVER-THROW comment names this exact fixture class) and
taxing every new migration. 25 harness files now import the shipped list
(~230 stale import lines deleted); deliberate subsets (PRE_DISCOVERY_MIGRATIONS,
legacy-degrade fixtures) kept by name. workflow-bound-rig fixture made
realistic: a canonical pod-member coordinate, seeded member — the
no-advisory pins now assert against a working probe, not a broken one.
Full suite: exactly the known 6/10 base set, zero deltas from this sweep.
Co-Authored-By: Claude Fable 5 <[email protected]>
r1's LOW-optional follow-on (pooled P14): make the money-proof e2e CI-reproducible so it guards the
tmux-version regression class that the real-run caught — respawn-pane -k CLEARS scrollback, and tabs
sanitize to '_' under env -i. Drives the REAL handover (createSuccessor -> terminateRetiree -> respawn-
no-k -> commit) against an ISOLATED tmux server, asserting on BOTH retiree-exit paths (graceful SIGTERM-
trap + forced SIGKILL-fallback): same pane id, predecessor deep history PRESERVED in native scrollback,
successor booted, stopgap from-record recap fired.
D15 isolation: a per-run -L socket on every tmux command (overrides $TMUX), full env MINUS $TMUX (env
-i strips the locale and breaks tab-delimited formats — the bug this guards), verify-isolation-first,
teardown by SESSION NAME (never kill-server). Guarded via it.runIf(tmux available) so tmux-less envs skip.
Verified: passes both paths (~7s), daemon tsc 0, tree clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The L3b seat-resolve gate hand-rolled the pre-B8 literal guard (the exact
~55-site class fold 45 killed); the census belt caught it. daemonStatusGuard
renders the epistemic 3-part; the raw-token escape hatch survives as a
supplementary tip. RED pin: positive-refused down (default-port probe must
REFUSE — ok:true fakes a live daemon) + hermetic OPENRIG_URL scrub.
Co-Authored-By: Claude Fable 5 <[email protected]>
The e2e caught a core defect the component gates could not: tmux respawn-pane -k CLEARS the pane
scrollback (verified 3->0 markers on tmux 3.6a), defeating the money-proof's central claim. My earlier
'scrollback survives' was an UNVERIFIED assumption — I checked pane-id/command/env (a proxy), never the
scrollback itself. Empirically, only respawn-pane WITHOUT -k, after the retiree EXITS with remain-on-exit
ON, preserves it (deep history intact minus the final screen the successor overwrites: 128/146 ~88%).
FIX (the plan's own no-k form + 'retiree exits in place'):
- respawnPane: drop -k (respawn refuses a still-live pane — the correct guard; never clobber a live
retiree). tmux-adapter test pins NO -k.
- New adapter methods: setRemainOnExit (pane-scoped, set BEFORE exit so the pane survives dead),
isPaneDead (the poll gate), signalPaneProcess (TERM graceful / KILL fallback).
- createSuccessor: terminateRetiree — setRemainOnExit(true) -> graceful SIGTERM -> bounded isPaneDead
poll -> SIGKILL fallback (pinned degraded path) -> respawn-pane no-k on the dead pane. If the retiree
never dies -> structured retiree_not_terminated, NEVER respawn (never clobber a live retiree).
Graceful signal = SIGTERM (desk-provisional under build-ahead; dev-planner ratifies the exact graceful
mechanism at resume). RED pins: graceful path (dead after TERM, no KILL) + forced fallback (survives
TERM -> KILL -> respawn) + never-exits (retiree_not_terminated, no respawn) + same-pane + successor.
RED-first: tmux-adapter 100 + successor-session-launcher 14 + seat-handover-service 33 + recap suites,
158/158, tsc 0. E2e re-run PROVES scrollback now survives (was 0 with -k).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Construct makePredecessorRecapResolver at the /handover route from the ContextUsageStore already in
context (readAndNormalize = claude transcript_path; readCodexAndNormalize = codex rollout_path) + a
resume-token lookup for the codex thread id; parseJsonlExchanges is the resolver default. Absent store
-> resolver omitted (recap sections omitted honestly). This closes (a): the stopgap now FIRES in
production — the successor boot packet carries the bounded from-record recap when a record resolves.
tsc 0; route regression green (wires-the-daemon-route + full seat-handover-service + resolver +
launcher suites). The lazy resolver fns are type-checked here; their live firing (real transcript read
-> parsed recap in the delivered packet) is proven in the money-proof e2e next.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
makePredecessorRecapResolver: resolve the DEPARTING seat's provider record path — claude transcript_path
(sidecar by session name) or codex rollout_path (thread by the departing resume token) — and parse the
last N exchanges into the bounded from-record recap. Honest-degraded: no resolvable path OR zero parsed
exchanges -> null (the composer omits the recap sections, never fabricates). Pure + injected deps so the
runtime-branch + null handling is unit-tested; startup wires the real ContextUsageStore reads + resume-
token lookup + parseJsonlExchanges, proven firing live in the money-proof e2e.
RED-first: predecessor-recap-resolver 4/4 (claude path no-codex-probe / codex resume-token+rollout /
null when unresolved parse-not-attempted / null on zero exchanges), tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Wire the stopgap render (105bb025) to actually FIRE: on the fresh cutover path the composer resolves the
predecessor's bounded from-record recap via an injected PredecessorRecapResolver and threads recap +
recordPath through deliverRestorePacket -> buildRestorePacket. Absent resolver / no record -> the recap
sections are omitted honestly (the preserved scrollback is the money-proof; this is the labeled
fallback). The resolver is an optional dep (byte-safe, never blocks the handover).
RED-first: seat-handover-service 31/31 (threads recap+recordPath when a record resolves incl. the
honest-degraded label; omits honestly when none) + restore-packet-recap 4/4, tsc 0. Production resolver
(ContextUsageStore transcript_path / codex rollout_path -> parseJsonlExchanges) + startup wiring land
next, proven firing live in the money-proof e2e.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The ghost-stage (e) contract is authored (dev50-driver), so the cutover's re-key seam-stub becomes the
REAL interface call: SeatHandoverService.commit() invalidates the RETIRING occupant's seat-name-keyed
stores (inside the rebind tx, retiring + successor names in scope) so the successor never inherits a
ghost — a drained compaction stage, a frozen telemetry sample, a lifecycle message to the retired
generation. This seat OWNS the single mechanical call; the ghost-stage slice owns the per-store impls
behind OccupantInvalidator (occupant-invalidator.ts, injected optionally — absent → skipped, never
blocks the handover).
⚠ CUTOVER COORDINATION (flagged to the ghost-stage author + in occupant-invalidator.ts): under the
cutover the successor REUSES the canonical seat name, so retiring === successor at this call site. The
contract's Class-A 'safe because the successor boots under a NEW name' premise does NOT hold — the impls
must be same-name-safe: Class-A relies on being invoked at commit BEFORE the successor accumulates
name-keyed state; Class-B MUST gen-scope (a shared name cannot discriminate retiree from successor).
retiringGeneration omitted until atom-B → Class-B no-ops loud.
RED-first: seat-handover-service 29/29 (calls invalidateRetiringOccupant at commit with the retiring+
successor names; NOT called when the handover fails before commit) + launcher 10/10, tsc 0. The launcher
swap-point comment now points to the commit() call site (no stale seam guidance).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Rewrite the successor path from the new-session+kill workaround to the cutover model (plan 411c43de):
a SEAT is one durable tmux session; the successor takes over the retiree's EXACT pane via respawn-pane,
so the canonical session name is PRESERVED (no -h shuffle) and native scrollback survives — predecessor
history stays above the successor boot (the money proof).
createSuccessor: resolve the DEPARTING session's active pane -> respawnPane in place (no command =
default login shell, identity env carries the preserved name) -> launchHarness into the preserved
session/pane -> upsert the discovery candidate on the preserved name (commit rebinds to it). No
createSession, no distinct name.
Safety-model inversion (orch-ratified, 3 choices):
(1) respawn -k FORCE-replaces the live retiree = exit-in-place; the provider session file
(claude transcript / codex rollout) is the durable, ratified wake target (rig-ask L3 wake).
(2) UNWIND INVARIANT: a failed successor NEVER kills the preserved seat. createSuccessor returns a
structured failure without killSession; cleanup only marks the candidate vanished. The seat stays
re-wakeable from its session file. (Pinned with a dedicated RED per orch.)
(3) finalizeWithDiscovered's successor_is_current guard now applies to non-fresh sources only — the
cutover successor reuses the seat name BY DESIGN; only a discovered-source successor must be distinct.
PLAN-TENSION SUPERSESSION (surfaced for PM's record, not silent): mini-PRD A's 'Rebuild: fresh agent
boots off-screen' is SUPERSEDED by the cutover FIX's 'same pane' — the founder scrollback fix is the
field confirmation. The re-key leg remains a NAMED seam-stub at the swap (ghost-stage, non-blocking).
RED-first. daemon: successor-session-launcher 10/10 (cutover happy-path, unwind-never-kills pins,
respawn/resolve failures) + seat-handover-service 27/27 (preserved-name compose, no-kill unwinds) +
tmux-adapter 100 + adjacent consumers 56/56, tsc 0. Composer guidance/comments updated to the new
invariant (binding untouched until commit; retiree re-wakeable, never destroyed).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The cutover successor takes over the retiree's pane via respawn-pane rather than a fresh
new-session. Two capabilities the primitive needs for that:
- inject the successor's OpenRig identity env + cwd onto the reused pane via respawn-pane's
-e/-c flags (tmux >=3.0), exactly as createSession does (a respawn carries no fresh env).
- make the command OPTIONAL: omitting it re-runs the pane's creation command, which for every
OpenRig seat is the default login shell — the harness is send-keys'd INTO a shell, never
exec'd as the pane command. Verified on tmux 3.6a: new-session(zsh) -> send-keys 'sleep 300'
(fg=sleep) -> respawn-pane -k (no cmd) -> fg=zsh again, same pane id, -e env present. That
fresh shell in the retiree's pane is what launchHarness drives, scrollback intact above.
RED-first: 4/4 tmux-adapter respawnPane (env+cwd ordering pin, no-command/default-shell pin,
2 pre-existing), tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The stopgap render piece (pure): buildRestorePacket now appends a bounded LABELED-FROM-RECORD
recap of the predecessor exchanges + a receipt line naming the predecessor provider record path
(honest-degraded), when a record is available. Never called 'scrollback' (the cutover's respawn-pane
owns real scrollback; this is the zero-product stopgap that ships first). No record → sections
omitted, no fabrication. Exported for the focused unit test; recap/recordPath optional → backward-compat.
RED-first 4/4 (recap-from-record + receipt honest-degraded + omit-when-absent + backward-compat) +
session-jsonl 6/6 + seat-handover-service 27/27 (no regression) + tsc 0. NEXT (resolution wiring):
resolve the predecessor transcript_path (claude sidecar / codex rollout) + parseJsonlExchanges, thread
into deliverRestorePacket. THEN the cutover: createSuccessor respawn + commit rebind-to-preserved-name + re-key seam-stub.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The shared provider-JSONL role/content reader (dev-planner ruled this mine — their L2 only
greps raw lines). parseJsonlExchanges(path, N) reads a KNOWN transcript path (claude sidecar
transcript_path / codex rollout_path) into the last-N exchanges for the successor boot recap.
Grounded at source on the real claude-projects shape ({type,message:{role,content}}, content
string OR [{type,text}] blocks); handles the codex {payload:{type:message,role,content}} shape.
HONEST-DEGRADED by contract: metadata + thinking/tool_use-only + unparseable lines skipped;
missing/corrupt file → [] never throws (a boot recap must never crash the successor).
RED-first 6/6 (claude string+text, multi-text-join skip-tool_use, last-N bound, corrupt-tail
skip, missing→[], codex shape) + tsc 0. NEXT: buildRestorePacket stopgap recap consumes this
(seat-handover-service.ts:753) + the receipt line; then createSuccessor respawn + rebind + re-key seam.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The cutover's foundational primitive (planner-reconciled — FIX confirmed = createSuccessor
respawns into the existing pane): tmux respawn-pane -k -t <pane> <cmd>. The successor takes
the retiree's EXACT pane, so native scrollback survives untouched (predecessor history stays
above the successor boot — the founder's scroll-up money proof). -k kills the retiree's pane
process first; command shell-quoted as one unit.
RED-first 2/2 (exact quoted exec string + write-error classify) + tsc 0. Divergence-independent
build-ahead per orch/planner ruling. NEXT (this slice): createSuccessor rewrite + commit
rebind-to-preserved-name + buildRestorePacket stopgap (provider-JSONL recap, SHARED parse
helper w/ dev-planner's rig-ask L2) + the re-key leg as a NAMED SEAM-STUB.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Third stacked break the effect test caught: @openrig/cli imports @openrig/daemon via
bare-specifier value-imports at RUNTIME (await import("@openrig/daemon/{crash-cart,
gateway-protocol,gateway-human-registry}") — all M1/C3 code landed tonight), but
@openrig/daemon is UNPUBLISHED (404). The shipped <cli>/daemon dir is resolver-INVISIBLE
(it is the daemon-SPAWN path, not an import target), so npm install -g the tarball 404'd.
Fix (dev-planner ruling, option a): build-package.sh assembles a clean resolver-visible
<cli>/node_modules/@openrig/daemon (package.json + dist — the exports map resolves the
subpaths), and packages/cli declares it in bundledDependencies so npm pack ships the copy
and npm install uses it, never the 404 registry. Caveats honored: better-sqlite3 is NOT
nested under the daemon (stays a hoisted cli dep, builds fresh on target); the daemon's
runtime dep set is a subset of the cli's, so it resolves by hoisting.
Proof: bundle-proof (host) — the tarball ships node_modules/@openrig/daemon + the 3
exports-map surfaces, better-sqlite3 not nested; install now progresses PAST @openrig/daemon
(404 gone) to better-sqlite3's native build. The full install+LOAD gate (opt-in
RUN_TESTBED_PACK_GATE=1) needs target build tools (better-sqlite3 builds fresh) — the
operator's Debian Docker rerun is that gate. Structural 10/10 + pack pre-check/bundle-proof.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First real host run of the 51-04 testbed build verb (reviewed+folded, never host-RUN
— the runtime-proof gap) surfaced two blockers:
A. build-testbed-image.sh npm-pack'd the private monorepo ROOT ([email protected], NO
bin) → the image's rig --version smoke died exit 127. FIX: assemble via
build-package.sh, then pack packages/cli (@openrig/cli, ships the rig bin).
B. plain docker build → the legacy builder never populates the automatic TARGETARCH
→ the Dockerfile defaulted amd64 → x64 Node in an arm64 image → exit 133. FIX:
resolve the host arch (uname -m) and pass --build-arg TARGETARCH explicitly
(builder-agnostic), fail-CLOSED on unknown; the Dockerfile also fails-closed on an
empty TARGETARCH instead of the silent amd64 default.
RED pins: 3 structural (packs @openrig/cli via build-package.sh not root; resolves+passes
TARGETARCH fail-closed; Dockerfile no :-amd64 default) + an offline ARTIFACT effect proof
(the packed tarball is @openrig/cli and SHIPS dist/bin-wrapper.js — a root pack ships none).
KNOWN-REMAINING (flagged to orch, arch-class): the cli tarball is NOT yet standalone-
installable — @openrig/cli hard-depends on @openrig/daemon ("*"), which is 404/unpublished
and imported at runtime via a bare specifier, while build-package.sh bundles daemon to
<cli>/daemon/ not <cli>/node_modules/@openrig/daemon. So npm install -g still fails to
resolve @openrig/daemon. The full install+run proof + operator success ride that packaging
fix. Structural+artifact tests green (11/11).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The founder-acceptance proof-4 (screenshot->attach->phone) needs an outbound image
path; slice-11 v1 deferred it CONSCIOUSLY (message.ts T1076 -> Slice-12) leaving the
extraBlocks/media seam. A5b wires that seam — not a redesign.
- message.ts: SlackMediaRef + buildImageBlocks -> Block Kit `image` blocks; item-7
hygiene REFUSES a secret-bearing or non-https image_url (never forward a smuggled
secret) and redacts alt_text; the notification fallback notes the attachment count.
- slack-delivery.ts: a media-bearing OutboundDecision.payload (.media) is forwarded
to buildOutboundMessage on the SHIPPED postWebhook path.
- no-loss RED extended: a MEDIA-bearing decision retains + replays BYTE-IDENTICAL
through the payload-agnostic dispatch buffer (the screenshot is not lost on a failed
delivery, exactly like text).
cli message+connector 10/10, daemon buffer 5/5, tsc0 both (+ test belt-typecheck).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Close the inbound forgery: previously an inbound Slack message stamped
human-<slackid>@kernel for ANY sender (an unregistered stranger forged human
provenance). Now the InboundRouter resolves ev.user against the human registry:
- REGISTERED sender -> land with the entity's canonical @external address as
source (never the raw platform id).
- UNREGISTERED sender -> REFUSED: no qitem, no dead-letter (a policy refusal, not a
transient failure), LOUD teaching logged.
- registry LOAD FAILURE -> refused fail-CLOSED, surfacing reg.error (r1 A4b
follow-on: a broken registry is distinct from an absent entity; never fabricate
a seat because we could not check).
The gate is injected (resolveSender) so inbound.ts stays a pure testable core;
makeInboundSenderResolver wires it to the daemon resolver (lazy import, per-message
load so a newly-registered human is admitted without a restart). The verb gains an
optional handle= token (kind:connectorRef:secretsRef:role[:handle=<id>]) that
coexists with a ':'-bearing secretsRef. Existing inbound mechanics tests keep an
admit-all resolver; new tests cover registered/unregistered/fail-closed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Additive per-binding `handle` (the human's platform-native id, e.g. Slack user id)
on HumanConnectorBinding, with the three sealed pins:
- pin-1 UNIQUE per kind across ALL bindings of ALL humans (one platform id = one
human): within-fragment in validate; cross-fragment in projectHumans (so add,
load, and drift all refuse a conflict) + a pre-write guard in addHumanFragment
(never leave a conflicting fragment with a failed re-projection).
- pin-2 resolveSlackHandle(handle, entities): ev.user -> (kind=slack,handle) ->
entityId; unknown = LOUD teaching refusal (admit-iff-registered, never a
fabricated human seat).
- pin-3 handle REQUIRED to be inbound-resolvable — a handle-less binding is
outbound-only and never matches the resolver (fails inbound loudly).
handle is HANDLE_PATTERN-constrained (no ':' '@' whitespace) so it can never forge
a session ref. Schema stays closed-key; A3 unchanged (22/22 regression green).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
r1 HIGH: the transport drained the durable row on ANY ack (onAck ignored msg.ok).
So a connector ok:false (delivery FAILED, e.g. Slack 500) on a LIVE socket drained a
decision the connector never delivered -> replay found nothing -> notification LOST.
Broke invariant-2 (the exact no-loss property proof-9 exists for); untested (the
stub only acked ok:true or never).
Fix: drain ONLY on ok:true. On ok:false retain the row (leave it pending) so
replayPending re-sends on the next (re)connect; the connector's decisionId dedup keeps
the eventual re-delivery a no-double-post. Surface the failure via onError for
observability.
RED-first: the new e2e stub acks ok:false (failClass) -> asserts the row SURVIVES
(reds against the old unconditional drain) -> then a recovered connector replays +
delivers it. A4a gateway suite 22/22, tsc0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The hardened slice-11 relay evolves into the connector the gateway dials: a unix-
socket SERVER that greets with its CapabilityDescriptor, receives OutboundDecisions,
delivers on the SHIPPED postWebhook path, and acks AFTER delivery.
- ack-after-delivery (proof-1): decisionId recorded delivered BEFORE ok:true ack, so
a replay of a delivered decision is de-duped + re-acked with no double-post; a
failed delivery -> ok:false{class}, NOT recorded -> gateway retains + replays.
- connector delivered-set (SeenStore by decisionId) is DISTINCT from the gateway
dispatch buffer — the two stores are never merged.
- proof driven end-to-end through the REAL connector loop; only fetch is faked at
the HTTP boundary (induced 500 -> replay -> 200); shipped redaction verified on
the wire (a secret in the body never leaves).
- lane rule for the wire codec: @openrig/daemon/gateway-protocol subpath (surface +
exports + cli tsconfig paths, all three); runtime codec lazy-imported per precedent.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The daemon-spawned gateway OS process (arch a8343a38) and the thin wrapper that
launches it. Completes A4a: logic core + transport + process + spawn.
- runGatewayProcess: the long-lived brain. A ref'd heartbeat is the keepalive AND
re-dial driver — during a connector outage there is no open socket, so without a
ref'd handle the event loop would empty and the process would SILENTLY EXIT
(lesson idle-process-needs-refd-handle). Never parks on new Promise(()=>{}).
- gateway-process-main: thin executable entry (env config, SIGTERM/SIGINT, self-run
guard); spawn-gateway: resolves the dist entry via import.meta.url + spawns it.
- transport: onClose hook so the brain drives re-dial.
- REAL-SPAWN e2e proves liveness (survives outage, re-dials) — the class an
in-process test cannot catch (vitest's own loop masks a missing ref'd handle);
in-process brain test proves replay-on-redial no-loss.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Wire the GatewayDispatcher to a real UNIX-domain socket: out-dial the connector,
exchange newline-framed JSON (protocol.ts codec), route capability/ack frames to
the dispatcher, and expose the dispatcher for the daemon to dispatch decisions.
- unknown/partial frame = LOUD refuse via onProtocolError (never a silent drop)
- always-attached socket error handler: a down connector never crashes the daemon
(the durable buffer already guarantees no-loss; the wrapper drives reconnect)
- proof-9 over a REAL socket: unadvertised op refused-not-attempted; advertised
round-trip acks+drains; connector outage -> buffer -> reconnect replays no-loss
- sun_path ~104-byte cap: socket in a short mkdtemp, asserted <104
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fat-slice sub-commit 3 — the gateway MODULE's core logic (the OS process's brain),
TRANSPORT-INJECTED so proof-9 is unit-testable without a real socket (contract a305310d).
- handshake: dispatch is REFUSED until a CapabilityDescriptor arrives.
- proof-9: an unadvertised op (or pre-handshake) is REFUSED, never attempted — no send,
no buffer write on a refusal.
- durable-FIRST: an advertised dispatch enqueues to the DispatchBuffer BEFORE the send.
- ack drains; replayPending re-sends every un-Acked decision on (re)connect = connector-
outage -> NO LOSS (decisionId keeps the re-send a byte-identical dup). Verified across a
fresh dispatcher over the same durable buffer (restart/reconnect).
14/14 (dispatcher 4 + buffer 4 + protocol 6), tsc 0. Never push. NEXT: the unix-socket
TRANSPORT (out-dial, framed newline-JSON, sun_path SHORT dir) + the thin daemon spawn
WRAPPER (compose-adapter precedent) + a proof-9 outage->drain e2e over a real socket.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fat-slice sub-commit 2 (A4a proof-9 no-loss mechanism). The gateway-side DISPATCH BUFFER:
durable OutboundDecisions held until the connector Acks — a DISTINCT store from the
connector-side slice-11 delivery ledger (never merged, per the arch verdict).
- enqueue = durable-FIRST (persist before dispatch), idempotent on decisionId.
- ack = ack-gated DRAIN (remove only after the connector's Ack).
- pending() = restart-surviving (reads from disk) — the un-Acked decisions a re-dispatch
replays on recovery = connector-outage -> no-loss.
Durability = the slice-11 atomic pattern (temp sibling + rename, 0600). decisionId is the
end-to-end idempotency key (re-dispatch = byte-identical dup, not two records).
4/4, tsc 0. Never push. NEXT: the gateway MODULE (dial socket + descriptor handshake +
advertised-op dispatch + wire the buffer) + thin spawn wrapper + proof-9 outage-drain e2e.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
M1 completion fat-slice, sub-commit 1 (A4a wire contract). The gateway<->connector protocol
(arch a8343a38 / contract a305310d): a CLOSED, minimal framed-JSON union over a local UNIX
socket — an internal IPC seam, NOT a new interop protocol.
- CapabilityDescriptor {connectorId, platform, protocolVersion, ops (closed set), limits?}
- OutboundDecision {decisionId, op, entityBindingRef, payload}
- Ack {decisionId, ok | failed{class, detail}}
decodeGatewayMessage validates the closed union: unknown kind = LOUD refuse, no silent partials,
protocolVersion is a field (additive-only). isAdvertisedOp = the proof-9 dispatch guard (an
unadvertised op is refused, never attempted). decisionId is the end-to-end idempotency key.
6/6, tsc 0. On the fat-slice branch off the A4b tip (497cb5c8, re-fold in flight). Never push.
NEXT sub-commits: the gateway MODULE + dispatch BUFFER (durable decisions until Ack, ack-gated
drain) + the thin spawn WRAPPER + proof-9 outage-drain; then A5 (relay->connector) + A6 (inbound).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
39a1c477 review nit (r1 CLEAR-with-nit): the comment promised the kind:spec
entry's path but the code took the first path: line anywhere in frontmatter.
Entry-delimited parse: kind:spec wins, first-path fallback, then PRD.
Co-Authored-By: Claude Fable 5 <[email protected]>
D1 (answered at source): the store carries NO hash but carries the locked
artifact PATH — specShaShort computes sha256[:8] of the CURRENT bytes at
projection time (store-derived, live; absent file = honest null). Rendered
in the spec-lock line. D2 (answered at source): NO assignment field exists
anywhere in the scope store — the mock's 'with:' segment is ASPIRATIONAL
and deliberately unrendered (render only what the store enforces); one-liner
documented in the model for the founder batch. Captures refreshed.
Co-Authored-By: Claude Fable 5 <[email protected]>
Incident 2026-08-07: index.ts defaulted dbPath to a bare "openrig.sqlite" which
resolves against the process CWD, so a daemon launched with an isolated
OPENRIG_HOME could still open the SHARED fleet db (whatever sat in its CWD).
resolveDaemonDbPath now anchors the default under OPENRIG_HOME (explicit
OPENRIG_DB/RIGGED_DB still wins) — an isolated-home daemon isolates its db too.
RED-first 4/4, incl. the tonight-case-as-RED: two daemons with the SAME CWD but
different OPENRIG_HOME resolve to DIFFERENT home-anchored dbs (neither can open
the other's). daemon tsc 0.
Sweep (parts 3): NO pattern-kill (pkill/killall/-f index) anywhere in
packages/scripts; the scenario harness teardown is already home-scoped (rig daemon
stop reads the scratch-home daemon.json, scenario-daemon.ts:139-143). Tonight's
outage was an ad-hoc 'pkill -f dist/index.js' matching production pid 65245 — a
PROCEDURE gap, fixed via the captured-PID teardown RUNBOOK + memory, not harness code.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Daemon: WakeResolveService + POST /api/wake-resolve (sessions-by-seat newest-first
query on the sessions-nodes join, delegates to resolveWakeTarget); wired in
server.ts + startup.ts. CLI: --wake with an '@' target now resolves via the daemon
(parseSeatGen handles seat[@generation]) -> wakes with the resolved token+runtime;
an unresolvable seat renders the teaching REFUSAL (which tenures exist, token-present
per tenure) at exit 2 and NEVER wakes a guessed target. --wake metavar widened to
<seat[@gen]|token> + --seat to <session-name> (closes the flag-diff cut-proof gap
vs skill 2516ea27; --session already exact).
RED-first: WakeResolveService (query+resolve+codex-map+refuse) 4/4; CLI seat
resolve-and-wake + teaching-refusal-no-wake. daemon 18 + cli 25 ask/wake green,
daemon+cli tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review-r1 finding (MEDIUM, gates the L3a fold): runWake dropped res.code/stderr on
the non-timeout path, so a bad-token/missing-binary/auth-fail wake (code=1, empty
stdout) rendered '(no answer returned)' at EXIT 0 — indistinguishable from a
genuine empty answer, violating the L1/L2 honest-degraded doctrine. Fix: propagate
code/stderr to WakeOutcome; when code!=0 && !timedOut, return failed + render
'Wake failed (exit N): <stderr>' with exit 2. Only exit 0 is an answer.
RED-first: non-zero-exit failure at the runner level + CLI (renders exit N,
never 'no answer returned', exit 2). 10/10 wake tests, cli tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
resolveWakeTarget resolves a seat[@generation] to a resume token on the stores
that exist today — the sessions rows for the seat (newest-first), no dedicated
tenure ledger (that's the pooled boot-capture atom). generation 1 = newest
(default). REFUSES with a teaching listing (which tenures exist, token-present
per tenure) for: unknown seat, out-of-range generation, or a tenure with no
captured resume token. Raw tokens accepted, guessed tokens never (pin 1 intent
on real stores).
RED-first 5/5. daemon tsc 0. (Route + CLI --wake <seat> wiring + runtime
captures are the remaining L3b leg.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
runWake spawns the runtime headless (claude -p --resume <token> / codex exec
resume) with a no-preamble prompt, captures the snapshot answer, and lets the
process exit (back-to-cold — the wake target is the session FILE, no lingering
process; mini-PRD A). EXECUTES: the only level with runtime cost, run ONLY on
explicit --wake, never an implicit escalation from a search. Bounded timeout +
large-file advisory (pin 5, founder's ~minutes-on-635MB datapoint) — never a
silent hang. Answer rendered as snapshot testimony: checked, not believed (pin 3).
Seat[@gen] resolution (L3b) rides the session store — a seat target refuses with
a teaching message for now; raw token wakes directly.
RED-first: runner (claude+codex command build, timeout honesty, timeout
passthrough, large-file advisory) 5/5 + CLI (raw-token answer render;
seat-refusal no-wake exit2; timeout exit2) 3/3. cli tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
searchSession locates a session's JSONL by token alone (<claudeProjectsRoot>/
<encoded-cwd>/<token>.jsonl — scans the encoded-cwd dirs) and greps it (rg->grep,
streaming so a large session file is safe; excerpts truncated). Honest:
session_not_found teaching when the token has no file (never a silent empty);
large-file advisory (pin 5). RED-first 2/2 (locate+grep-on-located-file; not-found).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
searchSeat scopes to ONE seat's <rig>/<sessionName>.log (never the whole rig
dir) and labels every hit with the generation (tenure) segment it fell in —
segments delimited by the SESSION BOUNDARY markers a seat's log accumulates as
agents come and go. Cross-generation is the point (a hit before AND after a
boundary proves the search spans tenures). Honest-degraded, never a silent
zero-hits: capture_missing / capture_empty / boundary_only / capture_unreadable
each say so; large-file advisory (pin 5) surfaced not hidden.
RED-first 3/3: cross-gen labeling + seat-scoping (other seat's log not read) +
boundary-only honest-degraded + capture_missing.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold-gate fix: the relocate added the daemon exports map + surface dist + the cli LAZY import,
but MISSED the cli tsconfig `paths` entry — so a CLEAN restack (daemon not yet built) fails cli
tsc with TS2307 on '@openrig/daemon/gateway-human-registry' (my in-worktree tsc passed because the
daemon dist already existed). Adds the paths entry beside the crash-cart precedent, pointing at the
daemon surface .d.ts. Lane rule: any NEW daemon subpath needs all THREE — exports map + dist +
cli tsconfig paths entry. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Wires the A4b resolver into the queue-destination gate — the entity-admission integration
that completes A4b (contract a305310d).
- startup.ts topologyValidateRig: a kind==='external' branch resolves <local>@external against
the A3 registry (loadHumanRegistry) via resolveExternal — registered/scheme ADMIT, unregistered
=> false. Checked BEFORE the isHumanSeatSessionRef bare-admit (which A2 also makes true for
@external); the registry is read only on the @external path. Admission = the gateway's job,
never the classifier's (arch 8cd30094).
- queue-repository.ts: externalAdmissionTeaching(session) builds the ENTITY-level teaching for an
unregistered @external destination; destinationRefusalTeaching dispatches it OR the existing 4b
host-suffix teaching (destinationRigTeaching) — ONE helper, all four refusal sites rewired.
- proof-2 BOTH texts: the ENTITY teaching (unregistered @external, register-how + not-an-agent)
AND the DOMAIN bounce (non-'external' domain -> unknown_destination_rig, A1/A2).
proof-2 test 4/4; no regression (queue-routes 127, queue-repository 38, session-name-parity + gateway
27 green); tsc daemon 0, build 0. Base = local main (A1 fold-36). Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
dev-planner-caught footgun (pre-land): OPERATOR_HUMAN_DEFAULT_SLOT was 'operator-human@kernel' — the
'-human' SUFFIX matches NEITHER isHumanSeatSession NOR isHumanSeatSessionRef (both need the 'human-'
PREFIX ^human…@(kernel|host)$), so a HUMAN-typed slot classified as NON-human — a footgun primed to
fire the moment it's wired into a human-class check. RULING (b): it is the default HUMAN operator
IDENTITY (concept-2), distinct from the inbound-AGENT routing destination operator-agent@kernel (which
keeps its own home in the slack connector config).
- value -> 'human-operator@kernel' (prefix convention; classifies human-CLASS).
- consolidate the DUAL definition (was in human-registry.ts AND external-admission.ts — drift risk) to
ONE canonical source in human-registry.ts; external-admission re-exports it.
- DURABLE class-guard pin: the default HUMAN slot satisfies isHumanSeatSessionRef (and the buggy legacy
form does NOT) — kills the '-human'-suffix class for a future rename of this slot.
28/28, tsc 0. On the A4b branch (pre-fold, per 'fix pre-land'). Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Planner+orch-ruled (C3/crash-cart precedent): the human registry is home-state (fragments under
getOpenRigHome, daemon owns home-state), so its canonical home is the daemon — not a cli/daemon
TWIN (byte-parity burden + permanent drift). ONE source:
- MOVE cli/src/gateway/human-registry.ts -> daemon/src/domain/gateway/human-registry.ts (import
../openrig-compat -> ../../openrig-compat; behavior-identical).
- NARROW surface daemon/src/gateway-human-registry-surface.ts (export * from the module) + a
./gateway-human-registry subpath in daemon/package.json (mirrors ./crash-cart).
- cli/src/commands/gateway.ts LAZY-imports addHumanFragment from @openrig/daemon/gateway-human-registry
at invocation (dep rail 2; type-only static import keeps it off the eager cli graph). Verb surface
unchanged.
- TEST split: the module-unit + proof-5 + hardening tests move to daemon/test (import the module
directly); the verb-integration tests -> cli/test/gateway-human-registry-verb.test.ts (createProgram +
the subpath). daemon 27/27 (22 module + 5 A4b resolver), cli verb 3/3; tsc daemon/cli 0, build 0.
Unblocks A4b's daemon reader to feed resolveExternal (external-admission.ts) at the gate. Base = local
main (A1 fold-36). Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
M1 A4b entity-admission core (contract a305310d/2a57d099 sealed). The RESOLVER for an
<local>@external destination against the human registry (A3) — the four ruled outcomes,
never silent, NEVER an agent-class downgrade:
- registered mike@external -> { registered, entityId }
- literal scheme slack:U012AB3CD@external -> { scheme } one-off ADDRESS (never a registry lookup)
- unregistered stranger@external -> { unregistered } LOUD STRUCTURED TEACHING refusal
(carries proof-2's entity-level text: how to register +
'NOT downgraded to an agent seat')
- default inbound slot -> OPERATOR_HUMAN_DEFAULT_SLOT = operator-human@kernel
Reader-location-agnostic: takes RegisteredEntity[] {entityId,address}, so it composes with the
relocated daemon-side registry reader (planner-ruled: relocate human-registry into daemon via a
narrow-subpath surface + cli lazy-import, no twin). 5/5, tsc 0. Base = local main (A1 fold-36).
NEXT (A4b): the relocate + daemon reader + gate wiring (topologyValidateRig/4b) + proof-2 both-texts;
then A4a (daemon-spawned process + unix-socket framed-JSON union) on v2. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
SWEEP-b: --session without --nodes rejects with the teaching error (it is a
--nodes filter; the rig tier accepted-and-dropped it into a silent all-rigs
listing). SWEEP-c honest floor: setting a boot-only key (daemon.port/host,
db.path, transcripts.*) while the daemon runs prints the loud takes-effect-
on-restart notice; non-boot keys and daemon-down stay quiet; probe failure
never fabricates the claim. Live-reload routed as its own arch item per the
shape. Both RED-first.
Co-Authored-By: Claude Fable 5 <[email protected]>
Beside the reference summary_evidence reject: closure_reason/closure_target
hard-reject unless state=done, the blocked park-record form (blocked_on
reason — the workflow gate/park writers' shape), or the handed-off handoff-
close form (found via neighborhood suites — the shape's admits-map corrected
from LIVE schema use, surfaced to PM); blocked_on rejects off state=blocked.
Incoherent fields can no longer silently COALESCE-persist. RED-first (2 REDs
+ park/done controls); neighborhood 104/104.
Co-Authored-By: Claude Fable 5 <[email protected]>
printTransportFailure branches on DaemonTimeoutError: timeout = 'Delivery
UNCONFIRMED — the daemon may have received and delivered' + reconcile-by-
EFFECT-before-resend guidance (duplicate risk named); connection refusal
keeps the hard 'was not sent' truth. RED-first (2 pins). B8-3 = floor-A
per the ruled decision: interior catches stay honest+nonzero+err.message,
no cosmetic normalization (recorded, zero code).
Co-Authored-By: Claude Fable 5 <[email protected]>
SCOPES explorer section (missions -> slice rows with ●/✓/⊙ glyphs from
lock/stage state); slice detail per the mock: header card with the founder
lock-glyph form (proof: N/M 🔒 ONLY when delivery-locked — the count
carries the honesty), spec-lock line, INTENT verbatim, MINI-REQUIREMENTS
(m collapses), PROOF CONTRACT ✓/○ with per-item C1-drop attribution lines.
PROGRESS.md renders ONLY under n as the narrative display (pinned: never
feeds counts). m/n ride the NEW registered commands reqs/narrative
(accelerators through parseCommand — the one path; parity auto-covers).
Hydrate gains the one-read scopes fetch (honest-empty + named error on
old daemons); demo fixture mirrors the mock; scopes-open is a dedicated
dispatch action (jump closes it).
Co-Authored-By: Claude Fable 5 <[email protected]>
scope-view-projection: locks from README frontmatter stamps; proof N/M
paired EXCLUSIVELY from proof/ C1 drops (evidences join by 1-based index
or exact text); intent/mini-reqs/contract sections parsed; PROGRESS.md
never read for data (pinned: a lying checkbox moves nothing) — surfaced
only as the n-narrative path. Routes /api/scopes (+/slice, /narrative)
mounted beside scope/audit. RED-first projection pins + real-fs route leg.
Co-Authored-By: Claude Fable 5 <[email protected]>
PM LOOK: 4/5 pass, the FROM-triple leg failed — 'orch-advisor@v-openrig-build@v-openrig-build'
doubled the rig into the host slot (malformed reply target + defeats the 51-09 collision case).
Root cause: the CAPTURE HARNESS hardcoded HOST = the RIG name ('v-openrig-build') in the third
(host) slot. The CODE path is correct — both callers resolve the 51-09 host_id (getSelfHostId ←
startup.ts:228 setSelfHostId(selfHost.hostId); fetchSelfHostId ← /healthz selfHostId). Fixed the
capture to the real origin host (mm2, distinct from the rig) per the 51-09 shipped rendering →
From/Reply now render clean member@rig@host: 'orch-advisor@v-openrig-build@mm2' + a valid reply hint.
Recaptured all 4 (deterministic); To/scale/timestamp/storm legs unchanged (they stood at LOOK).
No code change (verified correct at source). OPEN Q for PM (flagged, separate): if this VM's LIVE
self_host_identity.host_id was minted == the rig name, real fleet messages would also double — a
51-09 host-id-minting note, not the header slice.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The desk census caught transport-routes B1 failing at the send-header tip: it asserted
per-recipient-ISOLATED To (each seat sees only 'To: <that seat>'). That is the OLD contract
— ruling 03c35295 INTENTIONALLY changed it: a sessions multi-send now renders the FULL
recipient list on every recipient's To (the anti-storm behavior — each recipient sees WHO
ELSE got it, so it never re-forwards to peers who already have it). Characterized green-at-base;
this is case (a) — a deliberate To-projection contract change, NOT a lost envelope (each send
is still per-recipient wrapped: own From + reply hint; only the To projection changed).
B1 updated to the new contract (both recipients get 'To: dev-impl@my-rig, dev-qa@my-rig').
transport-routes 24/24. REVIEW-VISIBLE contract change — noted for r1's delta.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Atop the CLEARed slice tip (40ba5b9c), the two review-r1 non-blocking findings:
(1) LOW: send.ts fan-out comment said 'every seat To: <that seat> — byte-identical to a
single send' — stale, contradicted the new multi=full-list anti-storm behavior. Corrected
to describe the scale render (contradictory source guidance is part of the defect surface).
(2) broadcast fall-open asymmetry: send falls open to a SENDER_FALLBACK-WRAPPED envelope but
broadcast fell open to RAW → a session-less broadcast still storms. Closed SYMMETRIC with
send (broadcast now wraps with '<unknown sender>' when session-less), per the founder's
root-cause intent (no session-less storm). SURFACED, not silently absorbed: review-r1
flagged it as a founder call — trivially revert to raw-when-session-less if the founder prefers.
cli tsc 0 + send-header 11/11. Comment-only + one-line symmetric change atop the CLEAR.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
capture-send-header.mjs renders the four envelope types through the REAL wrapPaneEnvelope
(DM / multi-full-list / rig-broadcast-with-scale / topology) at a fixed stamp, drops .txt +
SHA256SUMS + a STORM-TEST.txt to artifacts/send-header-captures. STORM TEST = the four To
lines are mutually distinct (a recipient tells DM from broadcast header-alone — the anti-storm
teeth). Deterministic → byte-identical on re-run. STORM TEST: PASS.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The surface sweep at the CLI layer:
- rig broadcast: sets body.envelopeSender = the broadcasting seat, so the daemon fan-out
WRAPS each recipient with the scale header ('broadcast to <rig> (N seats)' / topology)
instead of raw — a recipient tells it was a broadcast header-alone (the anti-storm teeth,
killing the storm root the founder named). Absent session env falls open to raw. One body
field covers both the local route + the cross-host remote-daemon broadcast.
- rig send (directed DM, local + cross-host): stamps at send-time (Sent: MM-DD HH:MMZ) so
transcripts are timestamped; the To line stays the single recipient. --raw unchanged.
cli tsc 0 + 72/72 (send-header + send/broadcast regression). Green sub-boundary. REMAINING:
the 4 pane captures (DM / multi / rig-broadcast w/ scale / topology) + storm-test proof (pin 6),
then whole-slice review.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
broadcast() computes the EnvelopeScope (scopeForTarget) + the transport stampISO ONCE per
fan-out at send-time, and passes them to the daemon-side wrapPaneEnvelope — so every
recipient's header carries the same envelope facts. A multi-send now shows the FULL
recipient list on the To line (WHO got it, per founder intent) + a Sent stamp; the raw
paths (--raw / --dangerously-interact / absent envelopeSender) still deliver unwrapped.
stampISO is injectable (BroadcastOpts) for deterministic tests, defaults to send-time.
RED-first: session-transport 63/63 (2 new — multi full-list+stamp on each recipient; raw
broadcast unchanged) + tsc 0. Green sub-boundary of the surface-sweep fat slice. NEXT:
make itself wrap with the scale (thread the broadcasting sender through
the route/CLI — today it's raw) + the CLI directed-send scope + the 4 pane captures.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The transport fan-out knows the target shape + resolved recipient set, so it builds the
honest scale scope daemon-side: {session}→dm / {sessions}→multi(full list) / {rig}→
rig-broadcast{rig,seats} / {pod,rig}→scoped broadcast <rig>/<pod> / {global}→topology.
Pure + unit-tested — the first green boundary of the surface-sweep fat slice.
RED-first 5/5 + tsc 0. Next: thread scope+stamp into the broadcast() fan-out wrap (broadcast
starts wrapping with the scale header — today it delivers RAW, the storm root) + multi full-list.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Proves the scroll contract end-to-end through the real store: top/bottom clamp to the
true extremes (0 / contentMaxOffset), and intermediate offsets are visited ONE LINE at a
time (0→1→2→3) — never a page skip, so every row is grep-able at some offset (the founder's
no-pagination greppability guarantee). The path is view-agnostic (content-scroll reducer
never branches on viewTab); per-view contentMaxOffset wiring is covered by the render suites.
3/3 + tsc 0. Completes the scroll deliverable's proof leg (with the wheel/arrow unit tests
+ the registry parity suite + the no-pagination audit).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The :top/:bottom/:find leg, registered as VERBS in COMMAND_REGISTRY (not grammar
special-cases — the P10 lesson). Registered as top/bottom/find (NOT :top) because ':' is
the section-jump prefix (dev50-planner collision flag): typed ':top' parses as an unknown
section. top/bottom reuse content-scroll (the reducer clamps ±MAX_SAFE_INTEGER to the
bound — no new action); find reuses the filter action (verb form of the / prefix).
The registry parity suite auto-forces all three into the unknown-command listing + CLI
dump + palette + socket (zero hand-maintained surfaces — the directive working).
RED-first: 5/5 (top→top-clamp / bottom→bottom-clamp / find→filter / find-empty→loud error
/ all three in VERB_TABLE); registry parity 23/23; full tui 346/346 + tsc 0.
SPELLING NOTE (flagged for review): ruling wrote ':top/:bottom/:find'; I registered VERBS
per the ':' section-prefix collision + dev50-planner's steer. If the founder mandates the
':top' spelling, the ':' branch needs a registry-aware fallthrough (unknown-section → verb
table → else section error) — a small deliberate prefix-semantics extension, not done here.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
content-focused, the selection moves among visible targets; at the viewport EDGE with
more content beyond, the arrow SCROLLS the viewport (reveal) instead of clamping — so
↑↓ reach every row without PgUp/PgDn (most keyboards lack them — the founder fix). Away
from the edge it still moves the selection; at a true end (no more content) it clamps
(no phantom scroll). Localized to resolveKeyAction — no core target-model refactor.
RED-first: 5/5 (down-edge→scroll / mid→select / bottom-no-more→clamp / up-edge→scroll /
top→clamp) + wheel 3/3 + input regression 3/3; full tui suite 326/326 + tsc 0.
Remaining scroll: :top/:bottom/:find via the registry (I1 landed — coordinate dev50-planner
entry shapes) + no-pagination audit + enumerated per-view proof.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Founder bug: clicking works, WHEEL does not. The SGR mouse decode only emitted events
for button < 32 (clicks); wheel notches (button & 64 → 64=up/65=down) were decoded then
DROPPED. Now they emit a content-scroll action via the SAME pageup/pagedown pass-through
path (most keyboards lack PgUp/PgDn — the founder's point). content-scroll is view-agnostic
(clamped per-view by contentMaxOffset), so wheel works on every scrollable view.
RED-first: wheel down → +delta / wheel up → -delta / plain click still a mouse event
(regression). 3/3 + input-stream regression 3/3 + tsc 0. Remaining scroll: arrow
selection-driven auto-scroll (k9s) + :top/:bottom/:find via the registry (I1 dep) +
no-pagination audit + enumerated per-view proof.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
currentCommandContext maps the detector: up/absent=standard, down=crash-cart,
unverified=unverified (its own honest context — nothing pretends up or down).
The live context threads to all three consumers: the palette (render option +
main input path), the socket commands query (context supplier), and the CLI
dump stays statically documented per the increments doc. RED-first (mapping
fn absent at import); the composition pin proves a detector flip changes
availability IDENTICALLY on palette and socket via the one shared rule.
Co-Authored-By: Claude Fable 5 <[email protected]>
The socket answers 'commands' with serializeCommands(context): the data
contract + LIVE per-session availability — a second read-only verb beside
'state', inside the arch constraint's OBSERVE class. ONE availability rule
extracted (evaluateAvailability) and shared by palette + socket; the test
pins byte-deep parity between the socket rows and the serializer (one
source, derived surfaces; none hidden). Discriminating power verified by
mutation (verb removed -> test fails).
Co-Authored-By: Claude Fable 5 <[email protected]>
help/? is itself a REGISTERED command (context always) opening the palette;
palette state rides dispatch (open/query/move/close actions — no side
mutation). Fuzzy match scores canonical + aliases first-class (the real
alias g surfaces graph). Unavailable entries render dimmed-with-reason,
never hidden. Execution is BYTE-EQUAL to direct typing: argless entries
run perform(parseCommand(line)) — the BR-9 one-resolver path verbatim —
argful entries pre-fill the command bar (the palette never fabricates
args). Overlay renders at both cmd-line sites (standard + pulse).
RED-first: model suite failed at import pre-palette.
Co-Authored-By: Claude Fable 5 <[email protected]>
The commands subcommand dynamically imports the TUI's BUILT registry
(resolveTuiPath dist pattern — no TUI process, no new package edge) and
serializes the data contract: --json verbatim entries, human table with
the context column on every row (pin 3). RED-first (planted-entry test
pre-implementation). I1-review nits folded: the unknown-command prefix
fragment serializes from the prefix entries (zero hand-written listings);
graph gains the first real alias 'g' — the alias parity leg exercised.
Co-Authored-By: Claude Fable 5 <[email protected]>
The command registry ({name,aliases,args,description,context}) is the sole
source: grammar.ts derives its verb table and unknown-command listing from
it, so an unregistered action cannot parse (undocumented actions impossible
by construction). Parity suite RED-first (failed at import pre-registry).
P10 graph command migrated from its grammar special-case to the teaching
first registrant. Error listing serialized from entries (mirror-law), which
also fixes its stale omission of style/scroll. Aliases first-class in the
verb table.
Co-Authored-By: Claude Fable 5 <[email protected]>
Bare quoted argv over ssh runs in a non-login shell (no operator PATH ->
rig exits 127 on nvm/npm-global installs). The remote command line now runs
under sh -lc so the operator's own login PATH resolves it — one exec, zero
config. Shape rationale: (a) hosts.yaml rigPath pushes operator config +
drifts; (c) command -v needs a second round-trip; (b) sh -lc restores the
exact interactive-login PATH in one exec — with rigPath named in the hint
as the escape hatch. Exit 127 / 'command not found' classifies as its own
remote-command-not-found step with a teaching hint (D14 loud pattern).
Argv pin flipped with supersession comment; RED-first (2 REDs + control).
Co-Authored-By: Claude Fable 5 <[email protected]>
A throwing transport (timeout/connection/response) in any queue verb now
prints the classified error + the cross-host target + the indeterminate-
outcome reconcile hint, exit 1 — never a silent exit. RED-first: the raw
rejection escaped unhandled at base.
Co-Authored-By: Claude Fable 5 <[email protected]>
stopped requires positive evidence (connection refused); timeout/wedged =
unverified, never a down assertion (C3 crash-cart-detect semantics, lockstep).
Empty resolved home with a live sibling .openrig* home or HOME-MOVED marker
surfaces BOTH paths (siblingHint) instead of asserting daemon-down from the
wrong home. withClient hard-blocks only on positive down; unverified proceeds
to the configured-target request. RED-first: 4 honest REDs at base.
Co-Authored-By: Claude Fable 5 <[email protected]>
Step-3 (dead-code exits WITH the increment that killed its only caller):
- render-crash-cart.ts: remove renderCrashCartScreen/renderUnverifiedScreen/linesToScreen
(the full-width Screen wrappers) — the shell (render.ts crashCartShell) now owns the
Screen; the content builders (renderCrashCartView/renderUnverifiedView/renderFirstRunView)
stay, unchanged.
- stylize.ts: remove the full-width segRows paint branch — its ONLY caller was the full-width
cockpit; in-shell paints via the split-pane │ path. (characterized: no other full-width
segRows producer.)
- crash-cart-states.test.ts: the removed-screen block → renderUnverifiedView (content level);
the full-width crash-cart-screen/crash-cart-stylize tests removed (SUT gone, behavior now
covered by crash-cart-in-shell.test.ts).
Step-5: capture-crash-cart.mjs renders IN-SHELL via renderScreen → 4 captures for the PM
placement-delta LOOK: cockpit-in-shell / unverified-in-shell / first-run-in-shell /
post-restore-swap (the swap = same shell now live-fed, no daemonState). Deterministic + hashed.
Full tui suite 318/318 (incl. stylize + pulse regression — split-pane path intact) + tsc 0.
Step-4 (restore=data-source swap) is emergent: re-probe UP flips crashCartOpts present→absent,
same shell renders live (proven by post-restore-swap capture).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Ruling 3c6c2be0: the daemon-down cockpit renders as a CONTENT-PANE view inside the
standard explorer│content shell (mirrors renderPulseScreen). renderScreen dispatches
DOWN/UNVERIFIED to crashCartShell — the LEDGER-FED explorer on the left (from the model's
foundOnHost = the SAME one-JSON, never a second read; honestly marked 'ledger-sourced ·
daemon down'), the approved content on the right VERBATIM. Content segs paint via the
normal split-pane │ stylize path (no full-width bypass). All rails stand (content builders
unchanged: honest-null header, first-run onboarding, UNVERIFIED-no-restore).
RED-first: in-shell 6/6 (split present / ledger-fed+marked / content verbatim / stylize
strip-invariant + accent-bg / UNVERIFIED-no-restore / first-run onboarding) + ledger 2/2;
tsc 0; existing crash-cart + pulse/stylize suites 60/60 (full-width path still present,
removed next in step-3). buildLedgerExplorer reads the model VM (name+seatCount).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
buildLedgerExplorer: daemon-down the explorer sidebar is fed from the SAME one-JSON
crash-cart discovery (rigs+seats — never a second read path) and HONESTLY MARKED
ledger-sourced (the founder's honesty requirement). One row per rig (label + seat
count) + the honest note; empty discovery → no rows, still marked.
RED-first 2/2 + tsc 0. First buildable piece of the shell-placement rework (the
in-pane split renders these in the left column). Remaining steps (render restructure
+ full-width-branch removal + restore data-source swap + 4 in-shell captures) per
the marker plan.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The cli tsc resolves @openrig/daemon/crash-cart via the daemon's BUILT dist .d.ts
(packaging ruling A + the cli tsconfig paths mapping). The root 'lint' ran daemon tsc
with --noEmit (no dist) then cli tsc — so a CLEAN checkout failed cli tsc with TS2307.
Fix (smallest honest form — script ordering): a 'typecheck:prep' that builds the daemon
dist, run FIRST in 'lint' (&& ordering) so cli tsc cannot run before the dist exists.
Makes the repo self-carrying (was only desk-side in desk-fold-gates.sh).
Guard: scripts/typecheck-order.test.mjs pins the ordering (prep before cli tsc). RED-first
2/2. Real-run proof: rm daemon/dist → cli tsc TS2307 (constraint); typecheck:prep → cli
tsc passes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
M1 gateway A1 (narrowed per arch 8cd30094 / delta ad1e72c6, PM-approved): the TWO reserved-token
rails, enforced at the DAEMON. @external CLASSIFICATION is A2 (done); A1 does NOT re-classify and
does NOT do entity-admission (that rides the A4/A3 gateway layer). ONE SOURCE OF TRUTH: both rails
source the A2 closed-set constant VIRTUAL_DOMAIN_TOKENS.
- export VIRTUAL_DOMAIN_TOKENS from the 3-copy session-name contract (daemon-first, verbatim mirror
to cli + ui; cli==ui byte-identical) so the reserved rails derive from the ONE classifier set.
- HOST namespace: RESERVED_HOST_IDS twin (cli host-registry.ts + daemon hosts-registry-reader.ts)
spreads in ...VIRTUAL_DOMAIN_TOKENS — a host 'external' would collide with the <local>@external
classifier. Refused in BOTH twins (parity fixture pins it).
- RIG namespace (net-new): rigspec-preflight rejects a rig named a virtual-domain token at the mint
gate — a rig 'external' would make member=<local> rig=external ambiguous with a virtual-domain ref.
Proof-7 reserved-token half: 119/119 (parity 'external' host refused in both twins + rig-mint
'external' refused + session-name parity holds); tsc daemon/cli 0; build 0. (proof-2 unregistered
bounce rides the A4/A3 gateway-admission piece, per PM proof-ownership pin.) Based on local main
(A3 fold-35 18d0e618). Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
M1 A3 part 2: the verb-add-only CLI surface + the three r1 pt1-review items.
r1 MUST (no silent clobber — managed-config data-safety, mirror addHostEntry): addHumanFragment
now REFUSES an existing <entityId>.yaml unless { replace: true } is passed explicitly; a re-run
never silently replaces a human's data.
r1 note 1 (unknown-key): validateHumanFragment rejects unknown keys at the fragment, binding, AND
prefs levels (closed key sets) — a typo'd 'awya:' fails loud, never silently degrades.
r1 note 2 (address shape): address is pinned to <entityId>@external (the registered convention),
not a loose @external match — mike@externalx / other@external are refused.
VERB: `rig gateway human add <entityId> --display-name --binding kind:connectorRef:secretsRef:role
(repeatable) --delivery-class A-D [--away] [--replace]`. address is DERIVED (<entityId>@external);
--binding parse keeps a vault-pointer secretsRef intact (secretsRef may contain ':', e.g.
vault://slack/mike — kind/connectorRef first, role last, secretsRef the colon-bearing middle).
Mounted on createProgram beside crashCart.
Tests 25/25 (module + verb wiring + hermetic integration incl. the vault-pointer parse + no-clobber
exit-1); index createProgram wiring 4/4; tsc 0; build 0. Based on local main (A3 pt1 fold-34 87bcbfa6).
DAEMON READER (r1 noted for pt2): the gateway-side resolve of @external against the projection is
A4's consumption surface (the gateway process resolves admission) — rides A4, flagged. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
M1 gateway A3 (part 1: the fragment/registry CONTRACT + proof-5). Schema sealed b2a2594b
(supersedes e499dab6): prefs PER-ENTITY (loudness once per human), role PER-BINDING (routing;
exactly-one-primary = default channel) — two distinct axes; deliveryClass+away only.
- Fragment = one YAML per human at <home>/gateway/humans/<entityId>.yaml. Closed enums
(class 'human', kind 'slack', role primary|secondary, deliveryClass A-D); entityId is a
slug = filename key; secretsRef is a vault POINTER (never the secret). validateHumanFragment
enforces >=1 binding + EXACTLY-ONE-primary-per-entity; add-time == load-time.
- Registry = GENERATED projection (humans.generated.yaml, DO-NOT-EDIT header) via the ONE
projectHumans generator; addHumanFragment is the verb-add-only atomic writer (tmp+rename,
re-projects). loadHumanRegistry REFUSES a hand-edited/drifted projection (fragment is truth)
— the runtime drift pin. operator-human@kernel = the fallback slot, NOT a fragment.
- Mirrors two precedents: hosts-registry addHostEntry (atomic verb-add + add==load validation)
+ the codegen-mirror DO-NOT-EDIT/drift convention.
PROOF-5 GREEN (16/16, hermetic tmp-home): two fragments -> sorted projection; a fragment edit
re-projects; a projection hand-edit is REFUSED at load; invalid fragment + filename!=entityId
fail LOUD. tsc 0. Based on local main (A2 fold-33 3e688f63). Never push.
PART 2 (next, noted for review): the `rig gateway human add` CLI verb (nested-binding ergonomics)
+ the daemon-side reader A4 resolves against. FLAG-1/FLAG-2 ruled per b2a2594b (per-entity prefs,
per-binding role). operator-human@kernel default-slot resolution wiring rides A6/A4.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
M1 gateway A2. Contract 2cf541c6 / arch verdict 8cd30094 (both verified on-box).
Adds a CLOSED virtual-domain leg (<local>@external) to the parity-pinned FR-8
session-name contract — a LEXICAL widening only; ADMISSION stays at the A1/A4 gateway.
- kind 'external' {local, domain}; VIRTUAL_DOMAIN_TOKENS = ["external"] as a CLOSED
NAMED CONSTANT beside a derived pattern (arch: not inline alternation; pure fn, no IO).
- isHumanSeatSessionRef WIDENED = HUMAN_SEAT || VIRTUAL_DOMAIN: @external refs are
human-CLASS (they JOIN the attention/human-route union — NEVER a silent agent-class
downgrade). parseSessionName precedence human -> external -> canonical -> legacy -> malformed.
- Edited daemon session-name.ts FIRST -> mirrored VERBATIM to cli + ui (cli≡ui byte-identical).
- Parity test: +7 vectors (mike@external, scheme slack:U...@external, dotted/hyphenated,
decided human@external POSITIVE; @external + mike@externalx NEG) + agreement-pin RHS
updated to PIN the intentional widening (not silent).
- 5-consumer consolidation onto the ONE predicate: switched routes/queue.ts isAttentionItem
from human-route-enforcer's isHumanSeatSession to isHumanSeatSessionRef (behavior-identical
today, gains @external). Sweep clean: queue.ts + queue-bridge + startup + attention-feed +
feed-classifier all on isHumanSeatSessionRef; no hidden re-impl.
DOCUMENTED DIVERGENCE (route as follow-on, per dev-planner): human-route-enforcer.ts stays
READ-ONLY this slice, so its other consumers (mission-control-write-contract, queue-repository
park-validation, review/gather) keep the narrow @(kernel|host) pattern — @external is not
human-class THERE yet. Reserved-token 'external' rail is A1's build (proof-7 verifies post-A1).
Parity 66/66; attention-route + queue-route targeted 54/54; tsc daemon/cli/ui 0. Based on
local main c4d5f4e3 (origin/main was 12 folds stale — never-push means local main is truth).
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two named base-health causes in the cli suite, characterized hermetically + multi-run at b2435e68
(D12 method; per r1's isolated-flap correction — a single isolated pass is not a flake discriminator).
TEST + vitest.config only; NO src/ change (keep-production-loud).
CAUSE 1 — createProgram-wired FLAP:
A (kill): the 'wired via createProgram' tests did `await import("../src/index.js")` in the test
BODY -> an on-demand tsx COMPILE of the whole CLI graph (2-5s, variable) under the 5000ms per-test
timeout -> flapped. Static top-level import in the 10 mock-free lazy files (add/doctor/env/expand/
export-import/reconcile-session/status/setup/snapshot-restore/ui) moves the compile to the transform
phase; wiring tests now ~21ms. Mirrors the shipped norm (queue.test.ts).
B (quarantine): the 'mounts both commands' tests do createProgram()x2 + parseAsync x2 (~2s inherent,
Commander consumes parse); cli testTimeout/hookTimeout = 20000 (daemon P6-2 precedent) gives honest
headroom. Neither masks product latency (tsx dev-compile + tree-build; prod runs bundled dist).
CAUSE 2 — response-integrity STALE --actor FIXTURE (deterministic, failed at BASE — not the flap, not
a regression): `queue update` requires --actor when OPENRIG_SESSION_NAME is unset (hermetic setup
scrubs it), so 3 tests short-circuited before the daemon-response-render path they assert. Added
--actor to the 3 invocations (D12 fix-fixtures); product requirement kept loud.
Full cli suite 2122 pass / 1 skip (147 files), NO createProgram timeouts. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
PM ruling (the convention without its teaching layer is just hope): the shipped
warn-then-require rail (OPR.0.4.1.18 FR-7) told callers a missing --summary COSTS a Story-node
truncation. Add the TEACHING LAYER — the stderr advisory + the --summary hint now also TEACH the
convention: a good summary is 1-2 plain sentences a human skims in the needs-you view (what the
work is and why it needs this seat), not the agent-speak --body. Applied across all three
authoring surfaces (create / handoff / handoff-and-complete), advisory + hint.
Rails held: warn->stderr (json stdout clean); warn-not-require (omission still creates the qitem,
pre-18 exempt) pinned; the two handoff advisories stay BYTE-IDENTICAL (one replace_all + a parity
pin); facts-not-fabricated (teaches, never invents; served-field fallback untouched); single-parse
(pure static strings, no downstream re-parse).
RED-first: 3 pins (create advisory teaches + no-hard-break; handoff/h-and-c advisories teach +
byte-identical parity; create --summary hint teaches). RED at base -> GREEN. queue suite 83/83,
tsc 0, build 0. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Make the eleven LOCKED 51-03 scenarios COMPOSABLE against the real 51-02 runner
(run-scenarios.mjs, main) and record the honest composition ledger. Form confirmed by
review50-r1: green-through-bound-product-steps + LOUD unbound verb + ROUTE the unbound
runner verbs = honest ledger (a seed-bearing clean-PASS pre-binding would be the shim).
CO-LOCATION (fixture-form pass): the runner resolves `topology: ./<rig>.yaml` as a
scenario-file SIBLING and needs culture.md + agents/<seat>/agent.yaml siblings. Absorbed
fixtures/ into scenarios/ (flat, matches shipped scenario-02-baton): fixed topos (version
0.2, culture_file, per-pod edges, no top-level edges), culture.md, agents/{impl,qa,worker,
lead}, all topology refs -> ./.
RUNTIME FIELD-NAME PASS: send to: -> canonical <pod>-<member>@<rig> (#7/#10/#11); expect
pane seat: -> canonical session (#5/#7); #1 expect ps -> bare-array PsEntry (per-rig, not
sketch {rigs:[{seats}]}); #11 first expect queue -> bare-array {qitemId,state,destinationSession}.
LEDGER (live runner, main 2760f5c4): GREEN through every bound product step (block only at
the routed seed) = #1,#2,#9. Surface-resolves/content routed to 51-01 stub = #5 (preseed),
#7 (echo). LOUD unbound verb routed to 51-02 = #3,#4 (policy), #6,#8 (emit), #10 (mutate).
51-06 gated expected-RED = #11. All runner/stub gaps ROUTED (never shimmed) — see
FORMAT-CONFORMANCE-NOTES increment 5 + ROUTING.md. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The graph VIEW existed (per-rig, honest-empty when no graph served) but only `tab graph`
reached it. Adds a first-class `graph` command:
- opens the graph view from the command bar (parseCommand 'graph' → {tab: graph});
- discoverable — named in the unknown-command listing (+ 'tab <view>');
- honest-degraded rail confirmed: an unavailable graph renders the honest proven/empty
message ('not fabricated'), never a silent no-op (existing view behavior, pinned).
RED-first: 3/3 + tsc 0; grammar regression 10/10. Founder-caught small increment.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
buildTriageModel flattens per-seat restore-check evidence into triage rows (green
omitted; red-before-yellow); renderTriage → a keyboard-walkable list (header +
'<glyph> <seat> — <remediation> (<check>)' per need) or the all-clean line. Each row
= a seat + exactly what it needs (the failing check's remediation) — never one
blocking prompt per seat (plan §C4). No mock (a text list).
Seams (C1 EXCLUDED this wave): the conductor aggregate (attention_required/resume_failed)
+ the live restore-check fetch (C4 is post-run, daemon-UP) + the resolve→shipped-resume
handoff. TUI-local CheckEntry input type (no @openrig/daemon dep — the light-TUI fence).
RED-first: 4/4 + tsc 0. Branch hv/crash-cart-c4 off the CLEARed follow-on (978ce3ec);
restacks when the follow-on folds.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
L1 verb DOWN+discovery (money leg, read-only proof) / L2 UNVERIFIED (timeout never
->DOWN) / L3 first-run (DOWN+no-DB -> onboarding) / L4 TUI render in fixed-viewport
tmux (capture -e) matching Mock 2, with the deterministic LOOK-gate capture drop
(capture-crash-cart.mjs -> 3 hashed screens) built in for PM comparison. Host-side/
operator-executed (needs built dist + a real daemon-down); the render + wiring cores
are unit-tested, the verb's real emit + the spawn are the real-run legs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
capture-crash-cart.mjs renders the three daemon-down screens (cockpit POPULATED incl.
the honest-null header slots / UNVERIFIED / first-run onboarding) through the REAL
render+stylize pipeline at a fixed 120x32 viewport, dropping per-screen .ans (the exact
terminal bytes — cat to see the true look) + .txt (plain) + SHA256SUMS. Deterministic:
a re-run reproduces byte-identical captures. Drop → artifacts/crash-cart-captures/ for
PM Mock-2 (3d3c90a0) structure/ordering/emphasis comparison.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
resolveCrashCartKey (pure, RED-first 4/4): while a daemon-down screen is active, single
keys are cockpit actions — recovery: s=start-daemon i=inspect n=onboarding enter=restore;
first-run: only s+n (never restore-of-nothing/inspect); unverified: r=retry. Else null
(keys fall through to normal TUI). main.ts intercepts char + enter when daemonState is set
→ performCrashCart: start-daemon/restore exec 'rig daemon start' (the enter-flow's s step;
the C1 batch conductor RESTORE ultimately drives is EXCLUDED this wave) then re-probe;
retry re-probes; inspect/onboarding are entry-point seams. TUI tsc 0 + 46/46.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The TUI now shows the recovery cockpit when the daemon is down:
- from-emit.ts: crashCartRenderOpts maps the parsed verdict → renderScreen opts
(DOWN+discovery → cockpit; UNVERIFIED+evidence → cannot-verify; UP or DOWN+refusal
→ normal TUI — rail 3: never a cockpit from a refusal); probeCrashCart runs the verb
+ maps, and ANY failure → normal TUI (never a fabricated cockpit). RED-first 7/7.
- main.ts: on launch, probe {"ok":false,"error":{"code":"commander.unknownCommand","message":"unknown command 'crash-cart'"}} (its JSON is the truth even on a
hint non-zero exit) → feed crashCartOpts into draw(). Key-triggered re-probe +
the cockpit action keys (s/i/n/⏎, needing daemon-start exec + the C1-excluded
RESTORE) are the follow-on increment.
TUI tsc 0 + 71/71 (crash-cart + pulse/stylize regression). The spawn glue is
real-run-proven (a real daemon-down run); the parse+map cores are unit-tested.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- daemon: NARROW exports map { './crash-cart': dist/crash-cart-surface } + barrel
crash-cart-surface.ts re-exporting ONLY the read/emit/detect surface (rail 1 —
never a blanket daemon export). loadCrashCartDiscovery reused VERBATIM (rail 2).
- cli: @openrig/daemon workspace dep; the verb's real default emit LAZY-imports
@openrig/daemon/crash-cart at invocation (rail 2 — rig startup + other verbs never
load the daemon/better-sqlite3 module) and composes resolveDaemonState +
loadCrashCartDiscovery with live probes + read-only IO (a db-aware readDaemonJson
for the C2 read). Registered in index.ts.
- tsc resolution: cli paths → the daemon's built dist .d.ts (a declaration, so no
rootDir violation; runtime resolves via node's exports 'import' → dist).
CLI tsc 0 + command 4/4 (JSON contract, injected emit); daemon tsc 0 + crash-cart
22/22. The real lazy-emit is real-run-proven (a daemon-down run). Next: unit-C main.ts.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The 'rig crash-cart --json' command: prints ONE JSON = the emit verdict (read-only,
public agent-readable — the 4 rails). UP→state / DOWN→discovery / DOWN+refusal→
STRUCTURED refusal note (never exit-code-only) / UNVERIFIED→evidence. emit + write
injected; the real default LAZY-imports @openrig/daemon/crash-cart at invocation (dep
rail 2: rig startup + other verbs stay free of the daemon load). Exit code is a HINT
(non-zero when not cleanly up); the JSON is the contract.
RED-first: 4/4 (JSON contract per state). Not yet registered in index.ts — that + the
real emit wiring land with the daemon subpath export (packaging ruling A).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Per the option-C coupling ruling, the 'rig crash-cart --json' verb (daemon-side) is
the SSOT; the TUI becomes a thin exec+parse client. This unit:
- git-mv the detector + probes tui → daemon (crash-cart-detect.ts / crash-cart-probes.ts
+ tests), where the verb runs; logic unchanged (18/18).
- ADD emitCrashCartState (daemon) — the verb's JSON verdict: composes resolveDaemonState
+ loadCrashCartDiscovery VERBATIM (rail 2); UP → state; UNVERIFIED → state+evidence;
DOWN → discovery, or a STRUCTURED refusal note on the C2 fail-closed (rail 3, never a
cockpit from a refusal). RED-first 4/4.
- TUI decouple: new crash-cart/contract.ts (local DaemonState/evidence parse-target types);
render + dispatch import from it, NOT the relocated detector — TUI keeps no daemon dep
(the light-TUI fence).
daemon 22/22 + tsc 0; tui 32/32 + tsc 0. Next: (B) the cli verb, (C) tui main.ts exec+parse.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
probeHealthz (2xx=answered, non-2xx=not-openrig, ECONNREFUSED=refused, abort/other=
timeout — conservative, never a fabricated down) + isProcessAlive + readDaemonJson.
Feeds resolveDaemonState. 7/7. NOTE: per the C3 coupling ruling (option C), these +
the detector relocate into the daemon-side 'rig crash-cart --json' verb; committed here
to preserve the RED-first logic ahead of the relocation.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
INC2 sub-increment 3b (part 1) — the load-bearing paint piece (the bold-only no-op
class orch flagged; asserted POST-stylize, not on the pre-stylize Screen).
- stylize.ts: ADDITIVE full-width segRows paint branch — a canvas row with no
explorer │ split now paints its segs (token/bg/bold/inverse) instead of falling to
paintContent and DROPPING them. Same seg-paint rule as the split-pane path;
plain(segs) === the row text so the strip-invariant holds. Split-pane path untouched.
- render-crash-cart.ts: renderCrashCartScreen wraps the cockpit view into a full-width
Screen — benign line 0 (stylize special-cases index 0), content + segRows from
index 1, pad-only (never truncate — truncation would break the invariant).
RED-first: full-width accent-bg (48;2;) was proven to no-op pre-branch, GREEN after;
renderCrashCartScreen through-stylize paints the RESTORE accent bg + warn header,
strip-invariant holds. 65/65 (incl. stylize + pulse regression: split-pane intact) + tsc 0.
Remaining SUB-3b: renderScreen dispatch on the daemon-down signal + main.ts wiring +
UNVERIFIED screen + onboarding framing + s/i/n/⏎ input.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
INC2 sub-increment 2 (plan c015d9ed §C3). evaluateOneClickGate over the C2 read:
⏎ RESTORE EVERYTHING is ONE keystroke IFF every rig is fully resumable
(resumableCount == seatCount); any rig with non-resumable seats yields a delta
(rig + nonResumable count) so ⏎ leads to a confirm screen naming exactly those —
never a silent resume→fresh downgrade. Empty host is vacuously zero-generation
(the no-rigs/onboarding path is handled upstream in render).
RED-first: 4/4 + tsc 0. Pure fn, no daemon/detector dependency.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
INC2 sub-increment 3a — the DOWN/UP/UNVERIFIED classifier + bounded-retry resolver
(plan c015d9ed §C3; planner+PM ruling). The cockpit + C2 direct read fire ONLY on
DOWN; a probe blip must never fabricate a crash narrative.
- classifyDaemonState (single-shot): DOWN only on POSITIVE evidence (no daemon.json
OR pid dead) AND /healthz connection-REFUSED; UP on answered; else UNVERIFIED
(timeout / wedged pid-alive-refused / non-openrig occupant). REFUSED is the ONLY
strong down signal.
- resolveDaemonState (bounded retry): UP/DOWN decisive on first hit; UNVERIFIED
retries (small/bounded, injected clock) and a TIMEOUT NEVER promotes to DOWN at
any retry count. probe + clock injected → deterministic verdict pins.
RED-first: 11/11 (classify 7 + resolve 4) + tsc 0. Two render-side rulings carried to
the integration sub-increment (marker): UNVERIFIED = a minimal DISTINCT screen (evidence
+ retry + quit, ZERO recovery actions); DOWN + no DB found (C2) => onboarding framing,
never a crash story.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The mock renders the active (trailing) PULSE tab in BOLD, but its seg was bold-ONLY
(no token), so the segRows painter (paints only when token||bg||inverse) dropped it to
plain — the same no-op class as the exception subject (602af82d). Per-site fix: the
emphasis ink `bright` carries the bold (bold needs a color token to emit in this
pipeline). NO stylize.ts / paint-condition change (C3 has an in-flight full-width paint
branch on hv/crash-cart-c3 — no colliding stylize edits).
RED-first: a stylized bold-SGR-walk pin (the trailing PULSE renders bold) — RED at base
(bold-only no-op) -> GREEN. 45 pulse tests pass, tsc 0, build 0. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The daemon startup path assembled its migration list as an inline COPY of the ~60
schemas (startup.ts:270) — a mirror of db/all-migrations.ts's ALL_MIGRATIONS that
could silently drift (a drifted startup list boots the daemon against a schema the
code doesn't expect). The two were verified IDENTICAL (59=59, byte-diff clean), so
this is behavior-preserving: startup now imports ALL_MIGRATIONS (the single source)
and the 58 now-dead individual schema imports are removed.
Mirror-law pin (startup-migrations-mirror.test.ts): asserts startup applies the
imported ALL_MIGRATIONS and carries no inline `migrate(db, [` array — RED at base
(inline copy) -> GREEN. daemon tsc 0; schema-bootstrap boot test unchanged. 0.5.2
release-gate. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
R5 marker-lifecycle, absent-when-needed face. Expectation is an EXPLICIT signal, not
inferred (fallbacks-need-explicit-success-signals): the PreCompact hook now drops a
lightweight EXPECTED sentinel (<seat>.expected.json, same identity binding) FIRST —
before the packet + marker — so a hook that dies partway still leaves the sentinel.
The bridge is LOUD (stderr, never polluting the additionalContext stdout) iff a
matching-identity sentinel is present AND the marker is absent/unresolvable; it clears
the sentinel once the marker delivers. Policy OFF = no hook = no sentinel = SILENT by
construction (no false-loud); a stale/mismatched-identity sentinel is silent too.
Rejected reading the policy in the bridge (duplicated parse + TOCTOU vs compact-time).
RED-first: loud-on-absent pin + no-sentinel/mismatched-sentinel silence guards + a
hook-writes-the-sentinel pin. 25 pass / 2 skip, daemon tsc 0. Completes R5 (stale-firing
was b2fa9b4e; foreign-content was P5). Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
R5 marker-lifecycle, stale-firing-premise-false face: deliver-once was the ONLY
fire guard, so a not-yet-delivered marker written for a DIFFERENT compaction (an
old/leftover marker for the same seat but a different session) fired stale restore
context on the next compact-start. Bind the fire to EVENT+IDENTITY: the marker
already records the identity of the compaction it answers (transcriptPath/sessionId,
stamped by the PreCompact writer from the same Claude input); the bridge now fires
only when the current start MATCHES that premise. Recency is deliberately NOT the
gate (a clock window misfires both directions). A legacy marker with no recorded
identity falls back to the seat-key + deliver-once behavior (transitional).
RED-first (identity-mismatch pin) → GREEN; deliver-once / wrong-seat / precompact-hook
bridge behavior unchanged (21 pass / 2 skip). Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OPR.0.5.0 scope-honesty changed a session-scoped `ps --nodes --json` (default,
>1 rig on host) from a bare node array to a scope-DECLARED envelope
{ entries, totalNodes, scope } so the current-rig-only scope is visible. The 5
current-rig-default tests still asserted Array.isArray → failed. Shape-tolerant
nodesOf(logs) helper reads the node list from either shape (envelope .entries or
bare array); fleet-wide -A stays a bare array. Session-scoped intent preserved
(no -A); the FR-2 no-target refusal pin untouched. 18/18. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
ps-compact / ps-pl012-context-usage / ps-pl019-active delete OPENRIG_SESSION_NAME
(token-safety) then ran bare 'ps --nodes', but FR-2 requires an explicit scope
(--rig / -A) for --nodes — never an implicit fan-out — so ps refused with 'no
target' before the logic under test (downstream JSON.parse-empty / '' outputs).
These files assert FLEET-WIDE all-nodes output, so add -A (the explicit fleet
widener) to the session-less --nodes calls; --session is a FILTER and composes
with -A (verified). --rig calls already carry their scope (untouched). Production
FR-2 behavior unchanged/loud. The FR-2 no-target refusal keeps its dedicated pin
in ps-current-rig. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The workspace.root-default test asserted ~/.openrig/workspace but inherited the
seat's OPENRIG_HOME (.openrig-build-vm-*). config-store captures DEFAULT_WORKSPACE_ROOT
at MODULE-LOAD from OPENRIG_HOME, so a beforeEach set is too late (module-load env
capture). Fix: vi.hoisted points OPENRIG_HOME at a FRESH TEMP before the import graph
loads — never the seat's real home, and never unset-to-~/.openrig (foreign on-box
state, the seam-input hazard). Assertion now checks <temp>/workspace. 77/77.
First of the P6-1 38-file residue (config-store class). Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
readCrashCartDiscovery reproduces the daemon-side discovery facts from the copied
DB, daemon-down:
- FOUND ON THIS HOST: per-rig seat/running/resumable counts folded over the LATEST
session per node (max ULID id, the daemon's convention) + newest last_seen_at;
archived rigs excluded.
- WHERE WORK STOPPED: in-progress queue_items (display-only), newest first.
- HEADER: last-activity derived from the newest write-timestamp across the durable
tables (datetime() normalizes mixed ISO-Z / datetime('now') formats) + boot times
from self_host_identity. stopReason + priorUptimeMs are HONEST-NULL — verified at
source: NO shutdown/uptime record is persisted anywhere, so they are never
fabricated (a plan-vs-source gap, flagged for the planner).
loadCrashCartDiscovery composes it: fail-closed guard FIRST (no disk touched if a
daemon is live), copy-then-read into a disposable scratch dir, read, ALWAYS clean up.
ALL_MIGRATIONS: the canonical ordered 001->059 list, so read-model tests seed a
schema-faithful DB (not a stale hand-copied subset — perf-fixture-migration-parity).
Follow-up flagged: startup.ts:270 should import ALL_MIGRATIONS as the single source.
RED-first: 20/20 crash-cart tests (guard 5 / copy-read 5 / read-model 6 / load 4) +
tsc 0. All-additive (no existing file modified).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The safe-read foundation for the daemon-down discovery cockpit:
- assertDaemonDown: fail-closed guard — refuses if daemon.json pid is alive OR a
/healthz probe answers (honoring OPENRIG_URL); both negative to proceed. Mirrors
the daemon's own liveness logic; all probes injected (hermetic).
- resolveDaemonDbPath: prefer daemon.json.db (basename openrig.sqlite, NOT main.db)
over recomputing; flag a relative state-file path (daemon CWD unknown).
- snapshotDaemonDb: copy the {db,-wal,-shm} triple to scratch (db required, sidecars
if present); openDaemonDbReadonly: open the COPY read-only → WAL replays on the copy.
RED-first. Keystone demonstrated with REAL better-sqlite3 (crash-cart-copy-read):
last WAL-committed rows visible on the copy, main.db-alone omits them (proof they
lived in the WAL), originals byte-identical after the read (read-only / zero
interference). 10/10 + tsc 0. DB path confirmed openrig.sqlite at build base.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The daemon test suite has 20+ files that spawn REAL child processes (stub-runner-*,
precompact-hook, bridge, restore-from-jsonl). Under fold-gate contention (parallel
files + fleet load) a single node spawn can take 5-10s, exceeding vitest's 5s
default and flaking on TIMEOUT — not on any assertion (fail counts vary run-to-run;
uncontended the same tests finish in 1-4s). Set testTimeout/hookTimeout = 20000 so
these are deterministic under load. Fast tests still finish in ms; the only cost is
slower surfacing of a genuine hang. Validated: precompact-hook 16 pass / 2 skip
stable, stub-runner-compaction-e2e green. cli is unaffected (front-door ~360ms).
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The PreCompact hook read the global policy.messageFilePath verbatim into this
seat's marker.postCompactInstruction with NO seat-check, so a FOREIGN seat's
global extra leaked into this seat's marker (R5 marker-lifecycle family,
foreign-content-in-marker face; the rev1-r2 dcd95bd9 enforcer fix never reached
the hook path). Apply the SAME resolution the daemon enforcer's
resolvePostCompactExtra applies: prefer a per-seat compaction/post-compact-extra/
<seat>.md; fall back to the global ONLY when it does not declare a DIFFERENT seat
(a wrong-seat global is REFUSED); generic/undeclared/absent stay allowed. Regexes
byte-mirror the enforcer (declaredSeatOf, sanitizeSeat). No marker-format change.
Flips the two honest RED pins GREEN (precompact-hook.test.ts L365/L375). Proven
deterministically (manual hook runs: foreign global refused; per-seat preferred,
foreign excluded) + in-suite 16 pass / 2 skip (F2 restoreMapPath stays quarantined)
under an adequate timeout. The suite's default-timeout flakiness on real-process
spawns is pre-existing (pooled base-health residue), not this fix. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
D12 pre-existing base-failure family (verified at 261f4fcd, hermetic+uncontended):
- cli up.test.ts: 29 ambient -> 1 hermetic. The 28 are ENV-LEAK: getDaemonStatus
reads ambient OPENRIG_URL/RIGGED_URL (intentional prod override) + DaemonClient
env fallback, bypassing each test's injected mock daemon -> hitting the seat's
live daemon when the fold gate runs inside a seat. FIX: test/hermetic-env.setup.ts
(vitest setupFiles) scrubs the six connection vars before every test module.
Production env behavior untouched. Verified 29->1 under ambient env; strictly
non-regressive suite-wide (an adjacent 8-file family goes 45->38, never up).
- cli up.test.ts L822 (D12-F1, routed): startDaemon healthz poll is 80x250ms via a
hard-coded setTimeout (~20s, non-injectable, no early-bail on a fatal spawn log)
-> slow-fail past the 15s budget. it.skip with the named cause (product fix =
separate slice). NOT test-shimmed.
- daemon precompact-hook.test.ts L326/L335 (D12-F2, routed): the hook marker emits
no restoreMapPath field -> the per-seat post-compact-extra pointer is
unimplemented. it.skip with the named cause.
- daemon precompact-hook.test.ts L365/L375 (D12-F3, routed): LEFT RED as honest
pins per desk ruling. Integrity defect (R5 foreign-content-in-marker face): the
hook reads a FOREIGN seat's global messageFilePath into this seat's marker with
no seat-check. Desk excludes by finding id; I fix the hook seat-check as the next
increment (dcd95bd9 enforcer semantics) -> both pins flip green.
Out of D12 scope, flagged: ~38 pre-existing hermetic fails across 8 other cli
files (ps-*, destroy, config-store-extended, client, queue-update-metadata) — a
broader base-health sweep, not this increment. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Resolves the VM-side half of the stub census slot: authors the concrete zero-token
stub payload and populates docker/testbed/stub-assets.list (was empty => the build
verb refused, L0.2 FAIL). Base-image digest slot stays host-resolved (L0.1).
- docker/testbed/stub-assets/{rig.yaml, culture.md, agents/worker/agent.yaml}: a
runtime:stub, one-pod/one-seat topology. Shape grounded on the PROVEN 51-02 runner
fixture (test/fixtures/scenarios/topo-stub-baton.yaml) — member-key + top-key parity
MATCH — which the scenario runner settles headlessly in real tmux with zero LLM
tokens; both YAML files parse clean. Not guessed from memory (L3.3 grounding).
- stub-assets.list: exactly the trio's repo-relative paths (the census scope the
manifest hashes + the build verb stages; census-scope-match-code-path).
RED-first: scripts/testbed-stub-assets.test.mjs pins the L0.2 check (census non-empty,
names exactly the trio, every file exists, receipt each 64-hex) — RED on the empty
list (deriveStubAssetsHash loud-fails), GREEN after. 3/3 + full testbed suites 46/46.
L0.2 receipt stubAssetsHash: 3b24066fc0cc2e0a12ce03b32c7a18c170e62c394add583ffef0c105d8bd8349
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Founder Option-1 taste ruling (resolves the incr-2 bold/plain disclosure): the
who/what SUBJECT leads in BOLD, the detail plain after.
- needsRows splits q.summary on the ' — ' (U+2014) boundary the served summary
AFFORDS (the mock's own subject/detail convention): subject = before it,
claim = from it onward (plain, keeping the separator). A summary WITHOUT the
boundary renders whole-as-subject — honest, no synthesis the flat summary
can't support (a guaranteed subject for every row would need a served subject
field / authored-summary convention = a served-data question, not this change).
- The exception subject now carries the emphasis-ink token so its bold actually
RENDERS: a bold-only seg (no token) was drawn as plain by the segRows painter
(the same paint-attribute-dropped class as the earlier no-op finding) — caught
by a STYLIZED-layer pin (bold-SGR walk), not just the pre-stylize seg. Applies
to all exception subjects, per the mock's <b>.
RED-first: model split pin + stylized subject-bold/detail-plain pin (fail on the
old code) + green-at-base no-boundary degrade guard. 266/266, tsc 0, build 0.
Shipped-reads-only, zero deps. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Founder Option-B: PULSE renders as a content-pane view inside the NORMAL
explorer│content chrome — the sidebar STAYS (it is the founder's action path:
from a needs-you row, mouse to the sidebar and navigate). Supersedes the earlier
full-width form; the lanes truncate to the content width (trade accepted).
- renderPulseScreen now builds the normal split (sidebar + │ + lanes) reusing
computeExplorerRows/navigatorDisplay, so the founder's navigator behaves
identically to every other view. The full-width suppress-explorer special-case
is removed; the input special-case is removed too (←→ focus, ↑↓ per pane,
Enter drills — normal chrome input).
- Per-cell selection + fresh-output flash paint through the NORMAL split-pane
segRows path — the full-width stylize branch (and its bypass) is gone. F0's
no-op class simply does not exist in-pane.
- Selection stays incr-4's MOCK-BOUND per-cell accent-bg, walked column-major:
the approved mock's .sel row is a highlighted cell, an affordance the native
'›'-marker zone selection cannot express — documented in-package as the reason
parity-with-native yields here (the discriminator's mock-silence check).
- Lanes clip to the content width: a cell starting past the edge is not rendered
and not a target (no invisible-but-actionable cell).
- Width-INDEPENDENT and unchanged: F1 refresh-seam (load/rowFlashes/motionActive/
honest first-load 'loading'), F2 motion budget (NOW-only, window-bound, reduced
suppressed, same terminalActive onset the table flash rides), F3 reader-clock
(age/footer re-derive each render — pinned).
RED-first, all pins assert STYLIZED output (bg=48;2 content selection, inverse=
content flash via segRows; sidebar selection's own inverse is scoped out).
264/264, tsc 0, build 0. Shipped-reads-only, zero deps. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
L6 is the live proof of the step-3 mechanism the VM units injected: the runner's
container-mode drives a REAL scenario through the testbed image BY MANIFEST
IDENTITY (host-side reads -> published container port), stamps the manifest id
onto every ledger row, and carries two riders:
- L6.2 host-mode PARITY: the same scenario in host-mode must match container-mode
(the additive-opt-in fence, proven behaviorally where the VM units prove it
structurally; a divergence is a PM 51-02 gate, not a fold).
- L6.3 the L4 fail-closed STEP-3 FORM: a foreign OPENRIG_URL hard-refuses before
any container is created (the container is not an excuse to weaken the guard).
Wiring composes only already-tested helpers (spawnContainerDaemon, runScenarioFile,
withImageId); the only new-at-runtime piece is the real execFile docker invoker
(the container analogue of runRig). README index + sequencing updated (L6 after
L3); L4's grounding note now names L6.3 as its step-3 form.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
withImageId wraps the appendRecord sink so every results-ledger RunRecord in
container-mode carries the testbed image manifest id (digest) — runs comparable
across image versions. RunScenarioFileOptions.imageId threads it in; the wrap is
applied AFTER the ...opts.deps spread so it overrides the forwarded sink.
Host-mode byte-intact: with no imageId, withImageId returns the ORIGINAL sink
reference unchanged => ledger rows are byte-for-byte pre-51-04 (no imageId key).
The stamp is a COPY — the caller's record object is never mutated.
- RunRecord gains optional imageId (documented absent-in-host-mode).
- scenario-runner.ts + buildRealDeps UNTOUCHED (wrap lives at the pipeline layer).
RED-first: 4/4 new withImageId pins green (stamp-when-set, identity-when-absent,
undefined-when-no-sink, no-mutation) + tsc clean. Pipeline 9/9 + real-deps 9/9
unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The one opt-in branch: runScenarioFile:195 now stands the daemon up via
resolveScenarioDaemonSpawner(opts)(scaffold, opts). ABSENT opts.daemon =>
defaultHostDaemon = spawnScenarioDaemon(scaffold, { rigBin }) — the VERBATIM
pre-51-04 call, so host-mode is byte-intact (no 51-02 PM gate crossed).
Container-mode injects its own ScenarioDaemonSpawner (scenario-container.ts).
- new ScenarioDaemonSpawner type + defaultHostDaemon + resolveScenarioDaemonSpawner.
- RunScenarioFileOptions gains optional daemon?; every existing caller
(run-scenarios.mjs, the integration tests) sets none => host-mode.
RED-first: 4/4 green (mock-pinned: defaultHostDaemon forwards to
spawnScenarioDaemon with exactly { rigBin }; resolver returns the host default
when the override is absent, the override when present) + tsc --noEmit clean.
Pipeline suite 9/9 unchanged. All-additive to the host-mode path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
spawnContainerDaemon — the ScenarioDaemon-shaped sibling of spawnScenarioDaemon.
Docker-runs the testbed image BY IDENTITY, starts the shipped daemon INSIDE the
container, and points host-side rig reads at the published port; returns the
identical ScenarioDaemon contract so runScenarioFile's downstream binds unchanged.
- docker invoker INJECTED (mirrors runRig's node <rigBin> seam): unit-tests with
no real docker; the real-docker leg is the host-side L6 runbook.
- readEnv.OPENRIG_URL SELF-SET to the adapter's own published container URL
(scenario-daemon.ts:148 shape; the injectClockNow precedent — never foreign).
- fail-closed: refuses an inherited foreign daemon target BEFORE any docker runs
(the container is not an excuse to weaken the DAEMON_TARGET guard — L4 claim).
- lifecycle -> docker verbs: sigterm=exec rig daemon stop (container kept);
restart=stop+start (same container/port); stop=rm -f + scaffold cleanup.
- loud on run non-zero / empty id / in-container start failure (torn down, no leak).
RED-first: 11/11 green + tsc --noEmit clean. All-additive (2 new files).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Step-2 (plan §2): the L1-L4 live legs authored as host-executed, evidence-defined
runbooks (the 51-09 live-legs pattern; the VM seat has no runtime per the locus ruling).
Each leg = exact commands + real-byte capture + PASS/FAIL verdict-on-bytes (never from
memory) + hashed evidence dir (census-receipt discipline):
- L0 resolve inputs (base-image digest + stub-assets.list census — the two host-resolved slots)
- L1 PTY allocation (docker run -t + tmux capture real bytes + resize propagation)
- L2 tmux server lifecycle (detach-survive, multi-pane, exact send-keys/capture round-trip)
- L3 daemon-in-container (container-local sqlite, healthz on published port, rig up stub
topology settles — where stub-assets.list is grounded/finalized; zero tokens)
- L4 hermetic fail-closed (foreign OPENRIG_URL hard-refused INSIDE the container; grounding
note flags the packaged-vs-runner-mode form honestly)
- L5 multi-host (N named self-hosts on one docker network via the host registry, plan §3)
+ the 51-09 live-leg RIDER: executes the shipped collision/alignment RUNBOOK.md (LEG A
cross-host triple, LEG B founder-collision) against the testbed containers in one session.
Fences carried on every leg: no real-HOME mounts, network-only shared surface, scenarios
byte-unchanged, docker-gap-watch routes to PM.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review-r1's non-blocking nit (verdict 50974868): deriveStubAssetsHash digested via
JSON.stringify vs the manifest's canonicalJson. Deterministic here (fixed key order +
pre-sorted files) so not a defect, but export + reuse the one canonical sorted-key
serializer so the forgery-resistance doctrine is applied uniformly and the census digest
stays key-order-independent if the receipt shape ever grows. Green->green refactor
(no test pins the absolute digest value); 43/43 across the 5 testbed suites.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Completes step-1 (Dockerfile + build verb + manifest). The build verb is a thin
host-side shell (scripts/build-testbed-image.sh) over the tested node core:
- testbed-emit-manifest.mjs wires host-resolved base digest -> stub census ->
computeTestbedManifest -> byte-stable manifest.json + census receipt, with a CLI
shim the shell invokes (spawn-tested: emits on good input, loud non-zero on unresolved).
- The shell builds openrig-testbed:<git-sha> from the TREE (npm pack, never registry)
against the digest-pinned base, NEVER pushes; fences guarded by build-testbed-image.test.mjs.
- docker/testbed/base-image + stub-assets.list: documented HOST-RESOLVED slots (locus
ruling) — both refuse an unresolved/empty state, so the tree can never build floating.
docker build runs host-side (the VM seat has no runtime); all logic is VM-unit-tested.
RED-first throughout. 43/43 across the 5 testbed suites; bash -n clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
parseDigestPinnedBase enforces the byte-repro fence — only name@sha256:<64-hex> is
accepted; a tag-floating base, a malformed/non-sha256 digest, or empty fails loudly.
readBaseImage reads the committed docker/testbed/base-image slot (host-resolved per the
locus ruling), ignoring comments, requiring exactly one ref — an unresolved/ambiguous/
missing slot refuses, so the tree can never build against an unpinned base. The resolved
digest becomes the manifest baseDigest. RED-first, +7 node:test (16/16 file total).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The openrig-testbed Dockerfile (docker/testbed/) as the deliverable: digest-pinned
base parameterized via ARG BASE_IMAGE (the verb refuses a tag-floating ref), tmux+git
PTY substrate, pinned in-range node, OpenRig from the LOCAL pack tarball never the npm
registry, staged stub assets, a non-root openrig user, tini PID1 entrypoint that seeds
the 51-09 self-host-id adopt path. docker build runs host-side (locus ruling); the
plan-§1 fences are pinned as an executable contract (scripts/testbed-dockerfile.test.mjs
8/8, negative-asserts the registry-install fence), RED-first.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The build verb derives the manifest's stubAssetsHash (plan §1) from the EXACT
stub-asset file set the Dockerfile COPYs — a byte-reproducible census receipt
scoped to the code path's file list, never a recursive over-count. Digest is over
canonical {path,sha256} content (delimiter-forgery-resistant); loud-fail on a
missing file / empty set / a path escaping the asset root. RED-first, 9/9 node:test.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The testbed image's durable identity: base digest + node version + openrig sha
+ stub-assets hash + git-sha image tag (openrig-testbed:<git-sha>), plus a
content digest. REBUILD CONTRACT: same inputs => byte-identical manifest + digest
(census-receipt discipline; the runner/matrix cite the manifest per run).
The digest is taken over CANONICAL sorted-key JSON, never a naive field-join, so
a delimiter embedded in a value cannot forge another honest input set's digest
(hash-join-delimiter-forgery). Loud-fails on any missing/empty identity field —
never a silent partial manifest that looks build-clean.
Pure, dep-free helper (scripts/ house convention: <name>.mjs + <name>.test.mjs,
node:test), so the build verb (docker build) can compute inputs and call it while
this unit-tests hermetically. Docker-independent — lands ahead of the pending
container-runtime locus ruling (host-side vs seat-executed L1-L4).
RED-first: 5 tests (identity+tag+digest shape, rebuild determinism, per-field
sensitivity/no-collision, forgery-resistance, loud-fail) watched RED (module
missing) -> GREEN 5/5 via node --test.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
incr-4 of the 5.2 PULSE view, on the incr-3 lanes (4e675b6f). Three coupled
facets, all shipped-reads-only, reviewer-gated:
- COMPACT-SEAT lane form (r1 mock-authority ruling): NOW lane rows render the
seat's short logicalId (dev50.driver), EXCEPTIONS keep the full session. The
logicalId is served VERBATIM on the same nodes read (node.logicalId — the very
value TABLE renders as the agent name), carried into SeatActivitySummary — NOT
reconstructed from the session string (a hyphen split would be lossy). The
compact form also frees strip width for the work text.
- SELECTION parity: lane cells are content-selection targets walked column-major
(NOW → JUST FINISHED → UP NEXT) via the EXISTING contentSelection / content-
select / layout / contentTargetCount machinery — true parity, no invented 2D
cursor. The selected cell is painted PER-CELL (accent bg on its cell only),
never the whole zipped terminal row. pulseLaneTargets() owns the vertical
layout math so render.ts never re-derives offsets. A viewTab==="pulse" branch
in resolveKeyAction routes ↑↓ → content-select and Enter → the selected
target's action (no explorer to focus in the full-width view).
- DRILL-IN: every real lane row carries a drill Action to the AGENT it is about
(NOW = the active seat, JUST FINISHED = the seat that finished it, UP NEXT =
the destined seat), via the shipped session→agent drill — recovering the full
identity the compact label drops. An unresolvable seat degrades to a `notice`
revealing the full identity (honest, never a dead key). The "…" overflow marker
carries no action → not selectable. No invented qitem-detail endpoint.
Full tui suite 256/256 (+10), tsc 0, build 0. Firsthand frame probe verified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
NOW = active seats (terminalActive===true) joined to their in-progress work
(both already in-snapshot from 2b — no new read); idle→PARKED, null excluded.
JUST FINISHED = the shipped state=done,handed-off /list read (bounded recent
window), re-sorted by tsUpdated DESC (finish time) since the route serves
ts_created; HH:MM times. UP NEXT = the shipped state=pending read, unclaimed
only, carried in the SERVED order verbatim (no client priority synthesis — the
route serves ts_created DESC, priority/tier ordering is not served); overflow
past the display cap shows a '…' marker with the TRUE total count. Footer counts
are derived FROM the built model (active=NOW · parked=PARKED · waiting-you=NEEDS
YOU — label==referent), 'updated Ns ago' from a new TUI-local hydratedAt stamp
(honest '—' before first hydration). New client wrappers queuePending /
queueRecentlyFinished on the same /api/queue/list route (not new endpoints).
Bundled r1 incr-3 carry-ins: footer mixed-truth RESOLVED (live counts); PARKED
age-unknown belt (defensive 'idle (age unknown)'); D4-gap comment reconciled
(info token is resolved, not a routed gap). Live-data render-contract fix:
laneCell now TRUNCATES an overflowing label with '…' so long real session+work
labels can't shove the fixed lane columns out of alignment (the static short-name
demo hid this).
RED-first; tui tsc0 + build0; full tui suite 246/246 green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The final seeded behavior. `emit restore` fires the REAL restore reader
(compaction-restore-bridge.cjs) — arch R3: TRIGGER, never fabricate. On the
step the runner reads THIS seat's keyed restore-pending marker (written by a
prior `emit compaction`), the bridge injects ONE hookSpecificOutput.additional-
Context restore directive, stamps the marker deliveredAt/deliveryCount (one-shot),
and the runner mirrors the delivered directive verbatim to the pane. A restore
with no pending marker legitimately no-ops (honest, not an error).
New stub-restore.ts (fireRestore, symmetric to stub-compaction.ts) + a
resolveStubRestoreBridgePath resolver + the StubRunnerIO.fireRestore seam. With
all four STUB_BEHAVIORS now wired, the executor's deferral no-op becomes an
exhaustiveness throw (loud on an out-of-union behavior, never a silent drop).
RED-first: a hermetic dispatch test (executeStubScript emit restore -> fireRestore
called + directive mirrored) + a real-spawn e2e (compaction THEN restore ->
real additionalContext delivered to the pane + marker deliveredAt stamped under
the injected clock, deliveryCount=1). Watched RED (3 fails) -> GREEN.
Stub area 44/44; daemon tsc 0. Lights #2's seat-relaunch (a relaunched seat
with a pending marker restores).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The third seeded behavior. `emit mid_turn_death` emits a partial-turn pane line then
dies via a new die() IO effect: the runner records the exited sidecar (synchronous,
so the daemon never false-greens a stale ready), prints the EXIT marker, and
process.exit(STUB_MID_TURN_DEATH_EXIT_CODE=137, i.e. 128+SIGKILL). The executor
RETURNS on mid_turn_death — halting before the trailing Stop — so hooks CEASE
(UserPromptSubmit fired, Stop never does), the production-identical signature of a
turn that never completed. Later steps do not run.
Fire-and-forget activity POSTs may be truncated by the exit — which is faithful: a
real death loses in-flight hooks. So the reliable death signals are the sync exited
sidecar + exit code + the never-sent Stop.
RED-first: hermetic executor (die recorded, no Stop, later steps skipped) +
real-spawn death e2e (process exits 137, exited sidecar code 137, sink never receives
Stop).
daemon: 47/47 across the stub area, tsc 0. Remaining: restore (heaviest, real resume path).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The second of the four seeded behaviors. `emit slow_output` renders "paced output at
the scripted rate" (PRD §4.2) DETERMINISTICALLY — a fixed multi-part chunk sequence
(SLOW_OUTPUT_CHUNKS) to the pane, mid-turn, with NO wall-clock/real delay (§5) and NO
new leak-vector pacer env var.
Design ruled by orch (2026-08-06): the scenario verb set (match/contains/equals,
poll-until-match) has NO temporal assertion, so no scenario can express a real-pacing
claim; chunked multi-part pane output IS the assertable "paced" observable. A real
pacer would mint a second leak-vector var to produce behavior nothing in the format
can assert — it returns only as a future grammar extension (a timing verb) with its
own ruling.
RED-first: slow_output moved from the not-yet-simulated deferral set to a wired
chunking assertion (executor hermetic) + a real-spawn pane observable (the wired
runner renders the full chunk sequence in order to a real pane). Fits the R3
determinism pin (fixed text, deterministic by construction).
daemon: 45/45 across the stub area, tsc 0. Remaining: mid_turn_death, restore.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review-r1 escalated a real production-SAFETY concern: OPENRIG_TEST_CLOCK_NOW is
read in the SHIPPED precompact hook, so a LEAKED var silently FREEZES production
createdAt. Orch ruled a rider (name stays OPENRIG_TEST_CLOCK_NOW — already
test-namespaced, and the .cjs/.mjs hooks are general production hooks): when the
var is ACTIVE, both the stub runner AND the precompact hook emit one loud stderr
line ("OPENRIG_TEST_CLOCK_NOW active — timestamps are injected"), making any leak
visible in seat logs. Absence stays silent — production path + behavior unchanged.
The line is stderr-only + constant, so it disturbs no stdout/determinism pins
(R3 double-run, compaction-clock-injection, stub-runner-clock all still green).
STUB_CLOCK_ANNOUNCEMENT (stub-runner.ts) and the hook literal are kept
byte-identical; the pin asserts PRESENT under injection, ABSENT without, for BOTH
emitters (real-spawn).
daemon: 43/43 across the stub area, tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
PRD §5 ("no wall-clock/RNG in the stub's OWN behavior") pinned at the WHOLE-SCRIPT
level: running the same script twice under the same injected clock produces a
byte-identical observable — pane transcript (per-run temp path normalized), the
activity-event sequence (hookEvents + occurredAt=clock + identity), and the
compaction asset stamps (createdAt + packet-dir stamp). Asset stamps alone were
already pinned (compaction-clock-injection); this guards the surface the stub itself
emits against any raw new Date()/Math.random() leaking into the runner's behavior.
Real-spawn double-run against a throwaway activity sink. The pin has teeth: the
event sequence is non-empty and occurredAt is compared byte-for-byte, so a
wall-clock stamp would diverge the two runs.
This completes the compaction behavior (R1 loop-wiring + R2 activity events + R3
determinism). daemon: 34/34 across the compaction/stub area, tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The runner now POSTs the canonical /api/activity/hooks event set (§4.3) as it runs
its script: SessionStart on ready (running), then the scripted turn framed by
UserPromptSubmit … Stop (running → idle) — the observable state transition the
51-02 scenario harness reads off the shipped surfaces. Payloads carry the SAME
field shape the real runtimes POST ({runtime:"stub", sessionName, nodeId, hookEvent,
subtype?, occurredAt}), so they drive agent-activity-store state identically.
Compaction is NOT a hookEvent — it rides the precompact seam (R1); this is the
turn-lifecycle event set that accompanies it.
- StubRunnerIO gains postActivity; stubActivityPayload builds the canonical shape;
executeStubScript frames the turn (UserPromptSubmit … Stop).
- resolveStubActivityEndpoint mirrors pi-runner (env OPENRIG_URL+token, else
<OPENRIG_HOME>/activity-endpoint.json); postActivity is best-effort fetch with a
1.5s abort, never blocking the loop.
RED-first: the emission sequence via the hermetic executor test (fake IO); the
best-effort fetch mirrors pi-runner's already-tested impl. A real-spawn wire proof
(stub-runner-activity-e2e.test.ts) stands up a throwaway http sink and confirms the
wired process resolves the endpoint from env, Bearer-authenticates, and hits
/api/activity/hooks with the canonical sequence — closing the in-memory-hides-real-
spawn gap.
daemon: 33/33 across the compaction/stub area, tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The pane-hosted stub-runner no longer only idles: it LOADS a scenario-resolved
behavior script (<cwd>/.openrig/stub/script.json, else DEFAULT_STUB_SCRIPT) and
EXECUTES its steps against a StubRunnerIO seam (mirror pi-runner's RunnerIo).
`say` steps mirror to the pane; `emit compaction` fires the exact shipped
precompact seam via fireCompaction (arch R3: TRIGGER, never fabricate); the
three not-yet-simulated behaviors mirror an honest deferral (never a silent
no-op). now() honors the injected clock (PRD §5).
The runner authors its OWN session transcript (buildStubTranscript) and passes
it explicitly to the seam. This is a correctness requirement, not test
scaffolding: restore-from-jsonl's findLatestJsonl falls back to
~/.claude/projects, so without an explicit own transcript the stub would
non-deterministically compact whatever FOREIGN transcript is latest on the box.
RED-first, two cycles:
- hermetic executor + resolver dispatch (stub-runner-exec.test.ts, 7)
- real-spawn e2e: the wired runner authors its transcript, fires the real seam,
writes a seat-keyed marker+packet; asserts marker.transcriptPath == the OWN
path (kills the foreign-discovery false-green) (stub-runner-compaction-e2e.test.ts, 3)
daemon: 31/31 across the compaction/stub area, tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The priority sub-unit of the four stub behaviors. PRD §4.3/§4.4 (arch R3, binding): the
stub TRIGGERS the exact shipped compaction seam, never FABRICATES its outputs. Add
fireCompaction — on an `emit compaction` step the runner spawns the REAL precompact-hook.mjs
(the same product asset a real Claude seat runs at PreCompact), which writes the seat-keyed
restore-pending marker the real compaction-restore-bridge later delivers. The observable is
production-identical (a real keyed marker + a generated packet on disk) and deterministic
under the shared injectable clock.
Loud failure, never a silent phantom compaction: fail FAST if the shipped hook is absent
(HIGH-6 existence contract), on a non-zero hook exit, or if the seam produced no marker.
The key sanitizer matches the hook/bridge character class so the resolved marker is the
one the seam wrote.
RED-first, real-spawn: stub-compaction.test.ts (module-absent -> GREEN) spawns the REAL
hook under an isolated OPENRIG_HOME + a controlled transcript and asserts a real seat-keyed
marker + generated packet, byte-identical stamps across a double-run under the injected
clock, and fail-fast on an absent hook. 3/3; daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The compaction behavior (and the other three) needs a deterministic driver: a per-seat
script that says WHAT the pane-hosted stub does (pane output lines + named behavior
emissions), carrying no assertion logic (PRD §4.2). Add stub-script.ts — a pure,
side-effect-free module (behavior vocabulary + step model + parser + built-in default)
importable by both the pane runner and the daemon adapter without dragging daemon deps
into the pane.
TWIN-PARITY: STUB_BEHAVIORS is the canonical production-owned repertoire
[compaction, slow_output, mid_turn_death, restore]; 51-02's scenario emit verb
(EMIT_BEHAVIORS) is a copy of the same shared contract. A byte-parity guard test fails
loudly if they drift. usage_limit is refused as real-runtime-only (loud, never a silent
no-op), mirroring 51-02.
RED-first: stub-script.test.ts (module-absent -> GREEN) covers the parity guard, valid
say/emit parse, every locked behavior, and loud rejection of malformed JSON, non-object,
missing steps, unknown kind, textless say, unknown behavior (naming the repertoire), and
usage_limit. stub-script + scenario-schema 22/22; daemon tsc 0. 51-02 byte-untouched.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
PRD §5 guarantees "no wall-clock/RNG in the stub's OWN behavior", and the compaction
determinism pin (item 65) requires a byte-identical double-run. The runner stamped its
readiness sidecar (updatedAt, exit.at) with plain new Date(), which drifts run-to-run.
Point the runner's nowIso() at the same injectable seam the compaction assets use —
OPENRIG_TEST_CLOCK_NOW when set, real wall-clock otherwise (absent = production).
RED-first, real-spawn: stub-runner-clock.test.ts spawns the REAL runner under an
isolated cwd, reads the sidecar it actually wrote, and asserts updatedAt == the injected
instant (RED on wall-clock -> GREEN) plus a no-clock fallback control. stub-runner
clock+liveness+adapter 7/7 GREEN; daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The shipped compaction hook scripts stamped wall-clock time with new Date()
at four sites (restore-from-jsonl packet-dir path, precompact-hook createdAt,
compaction-restore-bridge postCompactAt + deliveredAt), none injectable. Add a
nowIso() to each asset reading the shared hermetic env-var OPENRIG_TEST_CLOCK_NOW
(ISO instant) with a real-wall-clock fallback (empty/absent = production, the only
unguarded path, on purpose). This realizes the A3-R3 clock/stamp injection seam so
the compaction determinism pin holds: the same script sequence twice under the same
injected clock yields byte-identical asset stamps.
RED-first, real-spawn (the class-fix floor): compaction-clock-injection.test.ts
spawns the REAL hook + bridge subprocesses under an isolated OPENRIG_HOME, watched
RED (wall-clock differs run-to-run) -> GREEN. Includes the double-run byte-identical
determinism pin across all four stamps + a no-clock fallback preservation control.
5/5 GREEN; daemon tsc 0. Test-adjacent precompact-hook.test.ts's 4 restoreMapPath/
seat-safe failures are PRE-EXISTING branch drift (base-parity confirmed identical),
not introduced here.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The compaction-restore bridge (a general production hook) will read an
injectable clock (OPENRIG_TEST_CLOCK_NOW) for deterministic test stamps,
falling back to new Date() when absent. A leaked clock var in a REAL seat
would silently freeze production compaction stamps — the temporal edition of
the silent-retarget class this slice's env-discipline exists to kill. Add it
to the hermetic helper as an OWN-category ambient hazard (distinct from a
daemon target so the refusal message is accurate): TEST_CLOCK_ENV_VARS,
detectAmbientClockHazard, assertNoAmbientClock, AmbientClockHazardError.
prepareHermeticEnv now fails closed on a leaked clock var in baseEnv (the
leak pin, before any fs side effect), scrubs it (defense in depth), and sets
it ONLY via a new injectClockNow opt (self-set injection; omitted => unset =>
the bridge stamps real-time = production).
Tests (hermetic-clock.test.ts): leak pin refuses a base-env clock var;
injectClockNow self-sets; absence leaves it unset. No regression in
hermetic-env / hermetic-scaffold.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The pane-hosted stub-runner printed READY then exited immediately:
`await new Promise(()=>{})` does not keep the Node event loop alive (no
ref'd libuv handle, and signal listeners don't ref the loop), so every
runtime:stub `up` failed readiness (the pane fell back to a shell and the
daemon's liveness cross-check reported stale readiness). Add a ref'd
keep-alive timer (cleared on the exit path) so the runner idles until
termination; the exit path and clean SIGTERM behavior are unchanged.
RED-first pin (stub-runner-liveness.test.ts): spawn the REAL runner, assert
it is alive past t=5s and exits cleanly on SIGTERM. Surfaced by the 51-02
e2e — step-4's component gates used the in-memory path and never spawned
the real runner.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Populate the ◌ PARKED WITH BATON section from shipped reads, retiring the incr-2
"idle-age read pending" placeholder now the owner idle-age fact (arch 3a947fb1)
lands. IMPL-PLAN §exceptions join: in-progress qitem ∧ idle owner ∧ no handoff —
queue (state=in-progress) ⋈ ps/activity (per-seat terminalActive + lastActivityAt).
- Gate = the SHIPPED terminalActive idle boolean (false); null (no signal) is
honest-unknown and EXCLUDED (never assumed idle). No client-side threshold.
- Idle-duration is a VIEW derived at the renderer from the owner's raw
lastActivityAt + the reader clock (buildPulseModel nowMs) — the TUI test-clock
seam, DISTINCT from the .cjs OPENRIG_TEST_CLOCK_NOW env clock.
- qitem rendered SHORT (label==referent — never the full pointer).
- Zero parked = silence (section omitted), like the other live joins.
Shipped reads only: queueInProgress() -> /api/queue/list?state=in-progress (mirror
of queueBlocked); seatActivity built from the SAME nodes read that feeds topology
(terminalActive + lastActivityAt), no extra fetch. daemon-client keeps the routes.
RED-first: 5 pins (population, active/handoff/null-owner exclusions, silence,
hydrate seatActivity+inProgress build). Full TUI suite 236/236, tsc 0, build 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
ARCH RULING 3a947fb1 (FR-7 additive; pulse PARKED-WITH-BATON owner idle-age).
pollSeat already reads the raw tmux window_activity epoch to derive active/idle
then discards it — surface it verbatim as `lastActivityAt` (ISO) on the
SeatActivity record and the per-seat NodeInventoryEntry projection so consumers
derive a fresh idle-age = f(fact, reader-clock).
- C1 additive: absent when no signal — pollSeat null-return leaves no record;
projection null when no obs, undefined when no service (the terminalActive
honest-absence ladder). Legacy record/projection bodies preserved.
- C2 raw fact: never clamped; skew may put it ahead of lastObservedAt (the
service already reads negative age as active) — renderers clamp for display.
- C3 one field: no ageSeconds sibling; age derivation is renderer-side.
Detail route serves it for parity with the list route (serves the entry wholesale).
RED-first: 6 pins (raw-vs-observed distinct, no-clamp skew, honest null/undefined,
verbatim projection, no-ageSeconds-sibling). Daemon-package increment on the
pulse-view lane; unblocks incr-2b PARKED population.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review50-r1 NOT-CLEAR, one bounded finding (the founder-caught label==referent
class): the ⧗ BLOCKED ON AGENTS rows rendered "blocked on qitem-20260805…" — a
qitem POINTER — where the mock contract names the AGENT ("blocked on review-r1").
RED-first (the new pin fails at a01eef08).
ROOT CAUSE + verify-at-source: blockedOn is POLYMORPHIC (queue-repository.ts:181
"qitem id, gate name"; the daemon's own human-park uses isHumanSeatSession(blockedOn),
so a SESSION for human-park, a qitem id for agent-blocks). The blocking agent is
that qitem's OWNER (destinationSession). My incr-2 fixture fed a SESSION in blockedOn
where real agent-blocks carry a qitem id — a fixture-realism miss that masked it.
- hydrate: resolve each non-human blockedOn via the shipped single-qitem daemon read
(GET :qitemId, queue.ts:864 — VERIFIED shipped, returns the qitem or 404; new
client method queueItem() is a per-id read on the existing route, NOT a new
endpoint) — a BOUNDED per-row lookup — and stamp the owner into QueueRead.blockerSession.
human-park (session in blockedOn) is skipped (belongs under NEEDS YOU). A miss
(gate name / closed blocker / 404) degrades QUIETLY to the raw blockedOn at render
(honest, never fabricated) — enrichment, NOT a load-bearing read, so it never
pollutes readErrors / the "reads failed" status line (the blocked LIST read stays
on safe()).
- render: "blocked on {blockerSession ?? blockedOn ?? '—'}" — names the agent, falls
back honestly. The human-exclusion filter now also drops a resolved-human owner.
- isHumanSeatSession is EXPORTED from pulse-model (ONE copy, reused by hydrate — no
second twin mirror).
Tests: fixture realism fixed (b1 blockedOn = a qitem id + resolved blockerSession);
pulse-live pins name==agent + NOT the qitem pointer + an unresolved blocker falls
back to raw; pulse-view locks it at the integration render; a new hydrate test locks
the resolution wiring (resolved owner / human-park skipped / 404 quiet-degrade).
Guard respected: no /api/ route literal added to a non-client file (types/hydrate
comments reworded).
Gates: full tui 234/234; tsc 0; build 0. Zero deps. Never push.
pulse-view increment 2 (RED-first; provenance vs 65f65cad). Wires the exception
sections to LIVE data from the snapshot the TUI already hydrates, and makes PULSE
render full-width. Scope LOCKED by dev50-planner (descope+escalate ruling); PARKED
deferred to incr-2b on the arch lastActivityAt ruling. NO new daemon surface.
- ▲ NEEDS YOU: from the shipped GET /api/queue/list?attention=1 (queueAttention()
pre-existed) — the daemon attention filter IS the human-facing set; map subject
from summary (fallback body head), age from claimedAt/tsUpdated.
- ⧗ BLOCKED ON AGENTS: from GET /api/queue/list?state=blocked (VERIFIED shipped —
/list reads c.req.query("state") and filters, queue.ts:744/753-771; new client
method queueBlocked() is a param-variant on the SAME route, NOT a new endpoint),
filtered to NON-human blockedOn (human-blocked already appear under NEEDS YOU).
Human predicate is a byte-identical mirror of daemon isHumanSeatSession (twin
comment); TUI has zero daemon dep so a local mirror is the only option.
- empty-strip-is-silence applies to the two LIVE joins only (a ran join yielding
zero is OMITTED — silence == zero). A FAILED read leaves the section empty but
is NAMED in readErrors and surfaced in the pulse status line (never hidden).
- ◌ PARKED WITH BATON: DEFERRED read (per-seat idle-age = ageSeconds dropped at
seat-activity-service.ts:77, not projected). Renders the NON-SILENT honesty-floor
placeholder per the planner ruling: header WITHOUT a (n) count + one dim
"— idle-age read pending" line; an omitted section would falsely claim all-good.
Populates in incr-2b on the arch lastActivityAt ruling.
- FULL-WIDTH seam: minimal early-return in renderScreen gated on viewTab==="pulse",
BEFORE computeExplorerRows — renders across full cols, no explorer sidebar, no ┬
split (pm-lead full-width ruling binds from here). Non-pulse views unchanged.
RED-first: new test/pulse-live.test.ts (7 cases) fails at 65f65cad (buildPulseModel/
snap.attention absent), passes here — NEEDS from attention, BLOCKED with the
human-blocked item EXCLUDED, PARKED honesty line, empty-join omission, full-width
no-sidebar + table-still-has-sidebar regression. pulse-view.test.ts's static
exception test rewritten to the live behavior (gutter/lanes/footer/tab preserved).
DISCLOSURES for the capture pass (deliberate, not defects): (1) live NEEDS rows
render the whole summary as the bold subject (claim=""); the mock's incr-1 two-part
bold-subject/plain-claim split isn't reconstructable from one summary field without
heuristic parsing (avoided — map+present, no synthesis). (2) failed-read vs
proven-empty are distinguished only by the global status-line readErrors, not
per-section. (3) renderPulseScreen carries its own minimal chrome (cmd bar/rule/
hints/status) rather than sharing renderScreen's — a full-screen-mode choice.
Gates: pulse-live 7/7; pulse-view 6/6; full tui 232/232; tsc 0; build 0. Zero deps.
Lanes+footer stay static (demoPulseModel) until incr-3. Never push.
review50-r1 NOT-CLEAR, one bounded region finding (padding fidelity vs the mock).
Both defects were the SAME shape: a gutter folded into the content pad instead of
reserved, so it vanished when content filled the column. RED-first (the new pin
fails at 118c4a9e).
- laneCell/renderLanes: COL=[30,26] are the mock's CONTENT widths; the 3-space
inter-column gutter (LANE_GUTTER) is now reserved SEPARATELY and laid between
columns by renderLanes — so it never collapses on a full-width row (the mock's
"✓ 14:02 slice-03 close-out" is exactly COL[1]=26; before, pad=0 → 0 gutter →
"close-out○ 51-02"; now "close-out ○ 51-02").
- renderLaneRule: dropped the padEnd().replace(/ +$/) that ate the space after
"(n)" into the dash run ("NOW (4)────"); the head keeps its trailing space and
dashes begin AFTER it ("NOW (4) ───…"), spanning COL[i]+gutter so the rule
aligns with the columns below (same stride, two methods).
Capture-truth pins added (plain segment text = width truth): the 3-space
JF→UP-NEXT gutter on the full-width row + "NOW (4) ─"/"JUST FINISHED (3) ─" +
NOT "NOW (4)─".
Gates: pulse-view 6/6; full tui 225/225; tsc 0; build 0. Zero deps. Region-scope
(full-width binds from incr-2). Never push.
51-09 fold-delta correction (RED-first; orch-authorized). Two my-lane gaps the
whole-lane fold gate surfaced (both fail at the lane tip, pass at base; the
desk-owned pre-existing base family is excluded — see receipt).
1. CLI fanout-contract TWIN MIRROR (production; the parity test is the pin):
incr-2a added the self-id block (setSelfHostId/getSelfHostId/resolvesToLocalHost,
+41) to the DAEMON hosts/fanout-contract.ts but not its byte-identical CLI twin
packages/cli/src/lib/hosts/fanout-contract.ts, so fanout-contract-parity.test.ts
("byte-equivalent across CLI and daemon") went RED. Byte-mirrored the block into
the CLI copy — the two files are now BYTE-IDENTICAL; parity RED -> 2/2 GREEN. The
lockstep-twin sweep family caught a real divergence and closed its own loop.
2. cross-host mock `exists` (fixture-only): cross-host-http.test.ts +
cross-host-commands.test.ts stubbed `lifecycleDeps: {} as never` (no `exists`);
incr-3's self-id fetch (resolveLocalDaemonUrl -> deps.exists) runs on the
cross-host path, so they went RED with "deps.exists is not a function". Added
`{ exists: () => false }` (the canonical send.test.ts stub form). RED -> 85/85 GREEN.
Fixture-only; the self-id fetch on the cross-host path is intended (the From:
origin triple needs the local self-id even cross-host).
client --version is GREEN at the lane tip (not our delta — a fold-worktree
composition artifact). GATES: parity 2/2, cross-host 85/85, send+client 66/66,
tsc cli+daemon 0, build 0. Fence: cli fanout-contract.ts (production) + 2 cross-host
test files; daemon fanout-contract + FR-8 session-name untouched. Never push.
51-09 increment 5 (the final increment). Envelope-consumer sweep (risk-2
obligation) + vendored-example fix + the live-leg fixtures/runbooks (proof item 5).
ENUMERATION INCREMENT (honestly no RED — no behavior change): every From: / ↩ Reply:
consumer was already 3-part-safe from increments 1-4b, so the sweep is an
enumeration + control test, not a code change. Consumers enumerated at source, all
3-part-safe:
- parseSessionName (FR-8 archetype, greedy fold a@b@c -> member a / rig b@c, never
throws; three copies byte-identical, C3-fenced);
- resolveQueueHostDestination (cli, atCount>=2 via lastIndexOf, 3-part-aware);
- resolveCrossHostTarget (incr-3 self-strip / reply-hint round-trip);
- the split('@').length<3 relay guards (send.ts, pane-envelope.ts) + stampSelfHostSuffix
(!==2, bare-only);
- the From:/Reply: emitters + queue sender surfaces render only (no parse).
Control test (self-host-envelope-sweep.test.ts) 3/3: parseSessionName greedy-folds a
3-part signature; 2-part unchanged; self-suffixed triple parses distinctly.
VENDORED FIX (the vendored-asset lesson): skills/.../cross-host-rig-commands/SKILL.md
(both copies byte-identical) — the block teaching the OLD wrong form ("a cross-host
message's From: is member@rig — the sender carries NO @host suffix") is rewritten to
the 51-09 always-suffix rule (From: member@rig@<originHost>; the reply hint round-trips
verbatim to the origin), with a supersession note citing the 2026-07-25 pm@your-rig
core-gap it closes + the C5 honest scope (the 2-part same-name ambiguity closes via
the --host envelope + sender-side stripping + the 4b teaching, NOT in-string magic).
LIVE-LEG RUNBOOKS (test/fixtures/self-host-live-legs/RUNBOOK.md — ship; the live runs
execute at the QA stage, not here): Leg A cross-host stamped-triple round-trip
(signature names origin not receiver; stored source_session == member@rig@origin;
verbatim reply lands on the origin host; fail-open control); Leg B founder-collision
(same-named rig on both hosts -> the triple disambiguates; verbatim reply lands on the
origin, not the local lookalike; the D10 2-part negative documented as not-killed-here).
ADVISORY (non-blocking, routed): the vendored SKILL edit leaves a skill-edge-digests
advisory drift under mirror-skills --check. It is NOT build-gating (tsc/build/
control-plane-json 9/9 all pass). Clean regen is the skill-authoring pipeline's
privileged, env-gated, tree-wide op (PM-ceremony) — NOT run from this build lane (it
would sweep pre-existing base drift at ceb9a3da that is not ours). Routed to the
skill-authoring pipeline owner as a follow-up.
Gates: sweep+envelope+teaching 14/14; control-plane-json 9/9; tsc 0; build 0; FR-8
session-name.ts untouched. Fence: 2 vendored SKILL.md + 1 sweep test + the runbook
fixtures. Never push.
51-09 increment 4b (RED-first; provenance vs 91401fa1). Arch ruling c9964404,
mechanism (ii): ADDITIVE TEACHING on the refusal the gate ALREADY makes — NOT a
new gate. 3-part destinations already refuse via the injected topologyValidateRig
(startup.ts:324-334, BR-1); 4b tells the sender WHY.
- destinationRigTeaching(session) (queue-repository.ts): greedy-parses the
rejected destination and returns ADDITIVE structured fields ONLY when the
parsed rig token contains '@' — { destinationSplit:{member,rig,host}, selfHost,
hint }. Non-self hint names the out-of-band path: "host does not ride in the
session string; use --host <host> with destination member@rig". C4 self case
(member@rig@SELFID): the hint NAMES it ("the host suffix '@<host>' is THIS host
— resend as member@rig") — the daemon NEVER auto-strips/routes-home (self-strip
is option (i), routes to arch).
- C1: the code stays `unknown_destination_rig` (stable consumers); the teaching
rides QueueRepositoryError.meta, which errorResponse already spreads — no
routes/queue.ts change. A plain 2-part unknown rig is byte-unchanged (no meta).
- C2: ONE helper consumed by ALL FOUR refusal sites (both create throws +
handoff + handoffAndComplete).
- C3: session-name.ts (FR-8 three-copy parse family) 0-line diff — detection
operates on the already-greedy-parsed reject.
- C5 HONEST SCOPE: the 3-part shape now TEACHES; the D10 2-part same-name
ambiguity (a local same-named rig) still validates+mints and is closed ONLY by
the --host envelope + sender-side stripping (incr 3), NOT this gate.
Proof exercises the INJECTED topologyValidateRig (startup.ts:324-334 verbatim
over a seeded RigRepository) — the production predicate, not the admit-everything
default (:417). RED-first coherent: RED1/2/4 fail at parent, RED3/5 pass as
controls -> GREEN 5/5. Regression 132/132; tsc 0; build 0. Fence: queue-repository.ts
+ 1 new test; session-name.ts + routes/queue.ts byte-identical. Never push.
51-09 increment 4a (RED-first). The qitem sender identity carries the ORIGIN
host, matching the envelope's always-suffix rule — correctly on BOTH the local
and the cross-host FORWARDED path.
- stampSelfHostSuffix (queue-repository.ts): the stored source_session IS the
origin triple <member>@<rig>@<selfHostId>; fail-open to 2-part when no self-id
/ not a bare member@rig / already a triple. ONE stored convention → every read
surface renders the triple with no render-time logic.
- LOCAL create: queue-repository.create() stamps the LOCAL self-id (the local
daemon IS the origin). Correct.
- CROSS-HOST FORWARD (routes/queue.ts): cross-host writes are forwarded and the
row is created by the DESTINATION daemon — so the FORWARDING (origin) daemon
now stamps its own self-id onto sourceSession when building forwardBody (both
the create-with-hostId and the handoff-successor sites); the remote create()'s
not-bare guard then leaves the already-origin-stamped triple untouched. This
fixes an origin-forgery hole (a forwarded bare sender was being stamped
member@rig@RECEIVER — replies routing to the wrong host, the exact class this
slice exists to kill). An already-triple forwarded sender is preserved verbatim.
from-host tag byte-pin (mini-req 5): the from-host:${osHostname()} tag LINE is
byte-identical (not in the diff); the pin is tag-line-untouched + behavioral
(routes/queue.ts is touched elsewhere for the forward-stamp). FR-8 session-name.ts
untouched (C3).
RED provenance: stampSelfHostSuffix absent at parent 1f77c4e8; the forward-stamp
test fails against the pre-correction stamp-at-write-only (receiver would forge).
GREEN 106/106 (self-host + queue-repository + cross-host create/handoff/br1);
tsc 0; build 0. 4b (E3 teaching) is arch-ruled (c9964404) and follows. Never push.
51-09 increment 3 (RED-first; provenance vs parent 38512e2d). Ruling cb19867f
Q2 (always-suffix, unconditional) + 2e1b737f (reverse dead-letter, option A).
The sender signature carries the ORIGIN host ALWAYS; the reply hint is
verbatim-usable including for LOCAL replies.
- Byte-identical LOCKSTEP TWIN: cli wrapSendBody + daemon wrapPaneEnvelope gain a
selfHostId param, render From: <member>@<rig>@<selfHostId> + the same reply
triple when the self-id is known; fail-open to today's 2-part when absent (C1).
Relay guard on BOTH twins: a sender already carrying a triple is preserved
verbatim (origin never re-forged).
- Reverse-dead-letter self-strip (cross-host-target.ts): a suffix === the literal,
case-sensitive self-id strips-and-routes-home (2a's CLI-edge twin); a non-self
UNREGISTERED suffix still loud-fails (rider a — never any-unknown->local);
C1 fail-open when the self-id is absent; C3 single home (FR-8 parse family
untouched).
- Additive daemon read: /healthz gains selfHostId (FR-7 additive-stamp — absent
before boot reconcile, legacy body preserved, NEVER ownName). ONE identity
source (rider b).
- CLI-direct send: one best-effort loopback GET <same-resolved-url>/healthz for the
self-id (fetchSelfHostId), reusing HEALTHZ_PROBE_TIMEOUT_MS (250ms, one
convention), CEILING=1 with NO retry, fail-open to undefined -> 2-part. --from
relay carries the origin triple. Daemon-side wiring (queue-repository +
session-transport pass-through getSelfHostId) renders the triple for
daemon-originated envelopes.
- R8 SUPERSEDED (merge-desk sanction + the 1-GET trade-off ruling): the
zero-probe pin now discriminates MECHANICALLY — exactly ONE self-id /healthz GET
(by path, no retry), and the 5-attempt status-probe burn stays 0.
Constraints C1-C4 + riders (a)/(b) + the timeout-leg (CLI-edge C1 negative)
proven. Inventory adds sanctioned: queue-repository, session-transport (pass-
through), daemon-lifecycle (fetchSelfHostId), R8 amendment. Gates: cli 67/67,
daemon 101/101, tsc 0 both, build 0, FR-8 parse family byte-identical. Never push.
51-09 increment 2b (RED-first). Arch ruling dfa65bfc: interpretation (ii) —
there is NO registry self-row (type-incoherent against the closed HostEntry
union). The alignment property is that the minted self-host id must be a VALID,
NON-RESERVED registry id so a REMOTE host can adopt it as a key
(transport-carries-host). Validated at boot, fail-closed.
- isRegistryValidSelfId / assertSelfHostIdRegistryAligned REUSE the canonical
validateHostRegistry (the CLI/daemon lockstep twin, parity-pinned in
hosts-registry-parity.test.ts) via a single valid-transport/target probe — no
divergent copy of the id regex/reserved-set.
- reconcileSelfHostIdentity: fail-closed assert on the final id in BOTH branches
(the ruling's escalation of increment 1's loud-conflict to a boot assert).
- Seed hardening: a host.name candidate is adopted ONLY when it is also a valid
registry id — a CONSISTENT extension of increment 1's blessed
adopt-unambiguous-else-generate guard (which already falls back to generated
for RESERVED seeds; now also for format-invalid ones). So a format-invalid
operator host.name (display-only, DP4) falls back to a generated valid id
rather than bricking boot.
Twin: 2b reuses validateHostRegistry, so the existing CLI↔daemon parity pin
transitively covers the alignment check; the 2b test adds self-id-shape fixtures
fed through both twins (agree verdict-for-verdict). RED 5/6 (twin-parity already
green) -> GREEN 78/78 (2b + incr-1/2a family + hosts-registry-parity + startup);
tsc 0; build 0. Fence: seat-identity-reconciler.ts + new test only. Never push.
51-09 increment 2, LEG (a) only (RED-first). Arch ruling cb19867f Q1(a): the
daemon's own self-host id must be locally resolvable — a read/route addressed to
this host's own id serves LOCAL, exactly as the 'local' sentinel / empty do,
without overloading the sentinel (two DISTINCT spellings, ONE route home).
- hosts/fanout-contract.ts: setSelfHostId/getSelfHostId boot accessor (single
shared host-contract module) + resolvesToLocalHost(hostToken, selfId) — the
pure convention (undefined/""/LOCAL_HOST_ID/self-id all route home; self-id is
a distinct spelling, never aliases 'local'; case-SENSITIVE self-id comparison,
matching increment 1's candidate-vs-stored per review50-r1's note).
- hosts/read-through.ts: local-route check uses resolvesToLocalHost(hostParam,
getSelfHostId()) — a read to this host's own id serves local, never dials.
- startup.ts: capture the increment-1 reconcile result + setSelfHostId at boot.
- test/self-host-alignment.test.ts: the predicate + the E1 read-through fixture
(a ?host==selfId read routes home, no dial — the unknown_destination_rig /
389ec01d killer at the read seam; queue-validator wiring is increment 4).
LEG (b) registry-alignment assert is DEFERRED — routed to arch-lead: the ruling's
"registry row for self under a different key" has no substrate at ceb9a3da (the
registry is structurally others'-view-of-me with no self-row; a self-marking
implies a fenced registry schema change). Awaiting the alignment-mechanism ruling.
RED 6/6 (resolvesToLocalHost/setSelfHostId absent) -> GREEN; leg-a regression
29/29; incr-1 43/43 intact; tsc 0; build 0. Additive to fanout-contract (no
AggregatedPayload contract touch). Never push.
51-09 increment 1 (RED-first). Arch ruling cb19867f Q1: canonical self-host
identity is NET-NEW — no authoritative self-record today (host.name is
display-only/DP4, "local" is a positional sentinel, the registry is
others'-labels-for-me). Establishes the daemon's own durable, unique,
stable-across-restart self-host id, co-located with the seat-identity substrate
(extend, not a parallel lifecycle).
VERIFY-AT-SOURCE (mini-req 1, confirmed @ceb9a3da): no durable daemon self-id
existed — LOCAL_HOST_ID="local" (fanout-contract.ts:14) is a positional
sentinel; zero self_host/host_identity records.
- migration 059_self_host_identity: singleton self_host_identity(host_id,
minted_at, reconciled_at) — next-free after 058.
- SelfHostIdentityStore (co-located in seat-identity-store.ts): thin defensive
DB seam (get/mint/touchReconciledAt).
- reconcileSelfHostIdentity (seat-identity-reconciler.ts): mint on first boot
(seed from an unambiguous operator host.name, else a generated collision-safe
id), reconcile thereafter (advance reconciled_at, keep the id — restart ⇒ same
id). never 'local'/'kernel'/'host'/'localhost' (assertNeverReservedHostId,
fail-closed); reserved RESERVED_HOST_IDS imported canonically (no divergent
copy). host.name conflict ⇒ keep the stored id + LOUD conflict naming both,
never silent re-key (the general mechanism increment 2 escalates to the
registry-alignment assert).
- startup: reconcile once after migrate, seeding host.name from settings.
REDs 6/6 (SelfHostIdentityStore/assertNeverReservedHostId absent) → GREEN 6/6;
regression seat-identity-reconciler + startup + self-host 43/43; tsc 0; build 0.
File inventory in-plan (+ test-app.ts migration parity). Never push.
A5 item 5 (ContextMonitor GAP-1/GAP-2): let a runtime:stub seat's context
usage be observed by the real monitor without provisioning any Claude-specific
context collector into the stub's cwd.
- GAP-1 getEligibleSessions: admit `n.runtime = 'stub' AND s.status = 'running'`
in the WHERE inner-OR, symmetric to the claude-code branch (a stopped/exited
stub stays ineligible; inherits the existing tmux-attachment outer guard).
- GAP-2 readContextUsage: guard `ensureContextCollector` behind
`session.runtime === "claude-code"`; `readAndNormalize` stays unconditional so
the stub's self-provided sidecar is still consumed (byte-equivalent claude
path; stub gets no collector write).
readinessCheckers / provider-collect / context-usage-store untouched; no
provider/usage-limit/config-writer widening.
Reviewed (review-r1 stage CLEAR, verdict 53397f73) at patch-id
35094f2c183b7a6b32354b1a0ec471c9e88c6aa7.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Promote the claude-stub seed to a first-class `runtime: stub`. Adds the
StubRuntimeAdapter (5-method RuntimeAdapter contract), a pane-hosted
stub-runner (Pi-shaped node-script, self-invocation guard, atomic readiness
sidecar), and its pure protocol; registers "stub" in BOTH createDaemon
literals (instantiator adapters + runtimeAdapters).
checkReady reads readiness from the runner sidecar when fsOps is wired
(production) or an in-memory launch record when hermetic — the hermetic path
never touches tmux, so a resumed stub reaches ready and stale pane bytes never
prove liveness. launchHarness fail-fasts on a missing packaged runner and
carries a byte-observable launch posture (fresh+resume). project/deliverStartup
run the real (Pi-shaped) projection + startup-file delivery.
Turns the composition + readiness pins green: STEP2/STEP3 (dual registration
dispatch), FACT3 (ordered startup ride to ready), FACT4 (pod-aware restore
resume via the real adapter), FACT5a-d (readiness honesty). ContextMonitor
GAP-1/GAP-2 (ctx%) is a separate later increment.
A5 authority: pre-edit packet 2f2a6039 (CLEAR dd7d0591); step-4 build guard
CLEAR review-r1 verdict 1eb6d505. context-monitor.ts + readinessCheckers
untouched. RED-order item 4.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add "stub" to SUPPORTED_RUNTIMES (rigspec-preflight.ts) so a modern-pod
runtime:stub member passes the canonical preflight gate alongside the real
runtimes, carrying the accepted REV2 RED layer for the stub-runtime adapter
(OPR.0.5.1.1; pre-edit CLEAR 15eb17b4; A4 terminal descope).
RED->GREEN (rerun + decomposed over the full accepted matrix, Node v22.22.1):
FACT1 (preflight accepts runtime:stub) and FACT2 (instantiate dispatches to the
injected stub adapter) are both GREEN; FACT3, FACT4, and FACT5a-d remain SIX
intended REDs (absent StubRuntimeAdapter). Preflight 28/28, pod-instantiator
35/35, five-file matrix 211 total = 205 green / 6 intended RED, daemon tsc 0.
Isolated worktree off pinned base 1280b602; no fold, no push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Response-integrity hardening. Distinguish three failure classes with typed
errors so the CLI tells the truth about what it knows:
- DaemonResponseError (bad-response: truncated/unparseable body, carries
status + bounded snippet) — was a raw SyntaxError bubbling to a cryptic
(--json) or silent (human) exit;
- DaemonTimeoutError (slow-response; subclass of DaemonConnectionError so
existing callers still catch it);
- DaemonConnectionError (no-connect).
requestJson reads the body text then JSON.parse (well-formed non-2xx
unchanged; 204 guarded). A shared renderDaemonTransportError at the
runProgram entry emits a 3-part fact/consequence/action error with an honest
nonzero exit in both json and human modes, and NEVER daemon-not-running
language for a bad or slow response.
Guard CLEAR: review-r1 verdict bf2ca4c3 (strict-improvement bar; reachability
walk clean, no swallow classes). Universal 3-part coverage across interior
catches ruled out as a separate pool atom. Built and reviewed on base
8d41d395; committed as reviewed (not rebased onto 0c3743fa — a restack would
need a fresh gate).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Guard PRE-EDIT CLEAR (4a04c1f7) — bounded recorder-local correction of a
parent-carried defect (QA signal bbd28852). A Worker reply ok=false marked
health degraded but resolved the pending write without latching lost durability,
so a later flush marker resolved, close resolved, and drainSlowOpRecorderOnShutdown
returned exit 0 despite a known-lost record.
slow-op-recorder.ts only (index.ts byte-identical):
- sticky acknowledgedWriteFailure latch set in the ok=false message branch BEFORE
resolving the waiter (one-shot degraded signal + waiter resolution preserved;
no hang, no duplicate emission, no unhandled fire-and-forget rejection);
- flush() completes its marker round-trip then rejects the new named
SlowOpRecorderWriteError when latched — distinct from the worker-terminal
SlowOpRecorderTerminatedError; close() still terminates finitely + rethrows;
- the production drain (unchanged) maps that rejection to exit 1.
RED-first (compiled Node v22.22.1): ENOTDIR-class real Worker ok=false -> flush/
close reject, snapshot recorder_write_failed, one emission, drain exit 1 + loss
log; clean recorder still exit 0. 40/40 focused (default + maxWorkers=1), 73/73
adjacent, typecheck + build. Prior lifecycle/non-interference legs preserved.
Author-excluded from later gates.
Guard checkpoint-2 NOT-CLEAR (5cbf17d3) — runtime correction green; one stale
JSDoc contradicted the fix. index.ts drainSlowOpRecorderOnShutdown JSDoc still
said the finite timer was "cleared + unref'd", contradicting the deliberate
referenced-timer fix (lines 46-51 keep it referenced — it is the only handle
that enforces the bound after servers + Worker are unreferenced; adjacent
comment already says do NOT unref it). Following the stale JSDoc would
reintroduce the silent-exit-0 defect caught at checkpoint 1.
Comment-only; no behavior/test change. index.ts sole file.
Guard checkpoint NOT-CLEAR (daa0f034) on b58f8793 — three findings, all in the
shutdown drain. Confined to index.ts + slow-op-recorder-shutdown.test.ts:
- Blocking 1: the finite drain timeout was unref'd, so once servers + the
recorder Worker are unreferenced Node exits before the bound fires (a hanging
close returned 0 with no log). Keep the timer REFERENCED (Guard withdrew the
unref sub-binding); it is still cleared after settlement.
- Blocking 2: `if (drainError)` misclassified falsey rejections (undefined /
null / false / 0 / "") as success. Track rejection with an independent boolean
so every rejection value logs and returns nonzero.
- Proof gap: the real-signal tests installed a parallel SIGINT handler and kept
an unrelated setInterval (masking Blocking 1). Replaced with a test that
crosses the ACTUAL startServer shutdown handler via a real SIGINT (exit 0 on a
clean drain), plus a clean-handles hanging-close pin (only the referenced drain
timer keeps the loop alive) that logs + exits nonzero, and a falsey-rejection
discriminator.
Compiled discriminators against dist now: hanging close -> timeout log + exit 1;
reject(undefined) -> {code:1}. 38/38 focused GREEN, 73/73 adjacent regression,
typecheck + build clean. Author-excluded from later gates.
Terminal 51elv2 correction (base 05fe13c4). One lifecycle/non-interference
class across four production files:
- slow-op-recorder.ts: one terminal-failure transition for Worker error,
unexpected exit (code 0 included), messageerror, and synchronous postMessage
failure — degrades once (one-shot high-urgency), settles every pending waiter
exactly once, makes all future posts/flushes finite. A lost drain rejects
with the named SlowOpRecorderTerminatedError (never a false durable success);
fire-and-forget measurement internalizes it. Normal close() stays healthy.
- server.ts: isolate a throwing request observer at the middleware boundary so
a successful route keeps its exact status/body (no instrumentation-induced 500).
- startup.ts: guard both setDegradedHandler registration and the streamStore.emit
callback body so neither aborts createDaemon or later wrapped work.
- index.ts: bounded shutdown drain (drainSlowOpRecorderOnShutdown) after services
+ HTTP servers close, before exit — one finite timer (cleared/unref'd); clean
drain keeps exit 0, timeout/rejection/terminal failure logs + exits nonzero.
RED-first: 11 discriminators RED pre-edit -> 37/37 GREEN. Nine sync-site,
rotation, schema, mode, secret exclusion, begin barrier, one-shot degradation,
and SessionTransport semantics preserved. Author-excluded from later gates.
Terminal-review NOT-CLEAR of b48c2242: WorkflowRuntime.route recreates the successor WITH the
source packet's summary/evidence (create-side carry) and then REDUNDANTLY re-supplied them in the
repark update. For a NON-human blocker that repark is a non-park metadata update, which the D2 guard
correctly rejects — rolling back a valid workflow route and surfacing HTTP 500.
Fix (Guard PRE-EDIT CLEAR e5feb850, option a): remove the redundant summary/evidenceRef from the
route repark only. The successor keeps them from createWithinTransaction; validateHumanPark uses
EFFECTIVE values (input ?? item), so a human re-park still validates from the carried fields. D2's
public/raw hard rejection is UNTOUCHED (no exemption, no viaWorkflowVerb bypass). Also corrects the
stale QueueUpdateInput JSDoc (non-park metadata is REJECTED, not silently ignored).
RED-first (workflow-route.test.ts): a handler-gated metadata packet projected waiting on the
non-human 'external-gate', then a REAL POST /api/workflow/:id/route — RED=500 pre-fix, GREEN=200 +
successor owner rebound, still blocked on the exact non-human blocker, summary/evidence retained,
frontier rebound, no orphan. Preserved: the human-blocked route positive (existing test) + all D2
public rollback-negatives. Gates: 33 workflow suites 355/355; D2/human-route/queue/CLI 76/76; tsc 0
both. Exactly 3 files; queue-repository.ts JSDoc-only; routes/queue.ts byte-identical to b48c2242.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
rig queue update accepted --summary/--evidence-ref on any transition, but the daemon persists
them ONLY at a human-seat park (state=blocked on a human seat); on every other transition they
were SILENTLY dropped — the operator believed metadata was saved when it was lost. Guard-bound
design: HARD REJECT before any mutation (not a post-commit marker, which would report a partial
write / data loss).
- daemon queue-repository.ts (before any UPDATE/log/event): a non-park transition carrying
summary/evidence_ref (null/undefined=absent, ''=present) throws
QueueRepositoryError('summary_evidence_not_persistable', { invalidFields }).
- route routes/queue.ts: the code maps to HTTP 400; err.meta.invalidFields rides the body.
- CLI: NO change — printResult already exits nonzero on 4xx (no warning branch, per the binding).
- human-park + metadata-free updates byte-for-byte unchanged; NO schema change (+23/-0 production).
RED-first: queue-update-metadata-reject.test.ts (reject + zero-mutation across state/log/event,
null-absent/empty-present, human-park control, route 400; migration-faithful with 044+048).
CLI acceptance is a REAL, HERMETIC loopback boundary: queue-update-metadata-reject-cli.test.ts drives
an actual queueCommand -> real DaemonClient -> @hono/node-server loopback -> real queueRoutes/
QueueRepository (migrations 044+048). It neutralizes ambient OPENRIG_URL/RIGGED_URL/OPENRIG_PORT (saved
+ restored even on failure) so status resolution binds to the fresh loopback state file, and a
clientFactory guard refuses any non-loopback URL BEFORE constructing a client (no accidental canonical-
daemon contact). Covers normal + --json reject (nonzero exit), UNCHANGED item/transition/event state
after the HTTP call, and the successful human-park control — passing with ambient OPENRIG_URL present.
Flipped the one existing pin that encoded the old silent-drop (human-route-enforcer.test.ts). tsc clean
both; queue regression green. Guard PRE-EDIT CLEAR artifact sha256 728690e1.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Mission control in the terminal, context library, permission
guarantee test-pinned, provider usage observability, plan amendment
done right, honest CLI output, build discipline.
Mission control in the terminal. Typing `rig` (or `rig tui`) opens
the TUI: file-tree navigator, dense agent detail (cwd, runtime as
text, context %), topology graph with the whole fleet on one screen,
honest status/activity language, motion design, working scrolling,
and honest width-clip/scope indicators throughout.
0.5.0 contains 0.4.8 in full. One lineage, no divergence, no
dual maintenance: 0.5.0 = 0.4.8 (the permission-policy release) +
the slices below. The 0.4.8 code + shipped skills all reside on the
0.5.0 lineage via the back-merge that reconciles the 0.4.8 release-
artifact history with the 0.5.0 working lineage.
Permission guarantee, now test-pinned. 0.5.0 upholds the 0.4.8
promise — OpenRig never writes permission entries into your Claude
`settings.json` — and pins it with a permanent guard test plus an
empty-writer sweep. The one sanctioned exception is the
project-local `acceptEdits` floor that 0.4.8 itself defines. Warning
ordering in permission-policy-discovery emits pre-existing
main-floor warnings first, then the permission-policy attachment
warning; presentation-only, semantic fence unchanged.
Context library. `rig context` stores and composes context; `rig
walk` delivers it paced; `--context` / `--body-context` ride
send/broadcast/queue-create with snapshot + provenance. Grammar is
strict: nouns store and compose (`rig context` never delivers);
verbs deliver (`rig send`/`broadcast`/`walk`/`queue` via `--context`
/ `--body-context`). The 0.4.x context-window usage viewer is
removed; the `rig context` name belongs to the library.
BEHAVIOR CHANGE (0.4.8 → 0.5.0). `rig.yaml` startup `context_pack`
entries are no longer delivered at instantiation. They are rejected
with a teaching error pointing at compose + delivery verbs. Users
who adopted them on 0.4.8 (shipped days ago; tiny exposure) should
compose the pack via `rig context compose` and deliver via a
dedicated delivery verb (`rig send --context`, `rig broadcast
--context`, `rig walk`, or `--context` / `--body-context` on queue
create). The bundle router populates the library store without
delivering (delivery-free-noun ruling applied at the startup
surface).
Provider usage observability. The daemon tracks account-level usage
per host — the "am I about to hit a usage limit" question — exposed
via `GET /api/provider/usage` and `rig provider status`. Honest
explicit-unknown and conflict-shows-both-facts; Codex account-switch
flows preserved.
Plan amendment done right. `rig scope slice approve --re-approve
--reason` re-stamps a locked plan with an append-only audit trail,
killing the old already_approved Status-note workaround.
Honest CLI output. `rig ps` says when it is showing one rig of many;
`rig send --json` returns structured errors as `{fact, consequence,
action}`; the activity-hook fix ends the fleet-wide "producer link
stale" advisories (operator-facing quality improvement worth
naming).
Build discipline. Contributor gates/lanes SSOT lives at
`docs/reference/developing.md` (encoded by the slice-12 gates
policy). The web UI formally in maintenance mode — CLI/TUI primary,
never "deprecated".
Two new bundled skills join the shipped set alongside the 0.4.8
skills: `applying-a-permission-policy` (Tier-A: the agent-driven
translation invoked at `rig setup --policy` for Locked/Standard/Open
policies) and `delegating-work` (Tier-A: every-agent distribution).
The `openrig-user` SKILL.md is updated with the 0.5.0
context-library + paced-delivery + `--context` / `--body-context`
awareness. Two additional 0.5.2-lane maturation-gated skills
(`retiring-and-inheriting-a-seat` and `oversight-team`) sit in the
oracle at draft stage and are correctly deferred to a later release
per the maturation-gate contract.
Formal CHANGELOG entries for 0.4.7, 0.4.8, and 0.5.0 plus
`docs/releases/v0.4.7.md` + `docs/releases/v0.4.8.md` +
`docs/releases/v0.5.0.md` follow as a combined post-cut docs pass.
Regenerates the product-repo public-skill manifests from the shared
authored sources and rewrites the shipped skill edges (plugin + spec)
per the oracle-declared membership and layout. Ceremony sits on top
of the back-merge commit that reconciled v0.4.8's release-artifact
history into the 0.5.0 lineage.
The primary content change from v0.4.8's mirror is the openrig-user
SKILL.md update carrying the 0.5.0 context-library + paced-delivery
+ --context / --body-context awareness authored per the locked spec
(agent-driven translation for how the rig context / rig walk verbs
compose with send/broadcast/queue/walk delivery). Complementary role-
SOP updates land alongside for queue-handoff,
seat-continuity-and-handover, session-compaction-and-restore,
watchdog, and mission-slice-sop. The dev-planner mission-slice-sop
proportionality-edit restoration (deferred post-0.4.8 per the single-
writer rule) is in the mirror as well.
Inputs pulled from the shared conventions layer (SHAs pinned in the
release ledger):
- Oracle membership (product-public-skills.yaml)
- Denylist rules (internal-tokens.yaml)
- Edge layout (skill-edge-layout.yaml)
- Skill canon root (the skills/ directory of the shared conventions layer)
Discipline applied by the mirror per each shipping skill:
- Layer-1 provenance-frontmatter strip.
- Layer-3 internal-fence strip.
- Deterministic leak-scan against the denylist; the script throws
on any post-strip content leak, so ship-set writes are only
reached when every shipping skill passes the scan.
MATURATION-GATED SKIP (recorded honestly): two skills sit in the
oracle's ship_after_fix section with CONDITION = MATURATION (not
sanitization) — retiring-and-inheriting-a-seat and oversight-team.
Both are body-clean of denylist tokens, but neither has met its
maturation condition (retiring-and-inheriting-a-seat awaits pilot
graduation from draft to provisional; oversight-team awaits real
oversight-rig usage before graduation). Per the oracle's own YAML-
comment intent + the release-management ruling for this cut, both
skills SKIP mirror application in 0.5.0 — likely landing in 0.5.1
after their conditions are met.
The mirror script's ship-set logic does NOT yet distinguish the two
condition kinds (sanitize: script-verifiable via leak-scan; matura-
tion: stage-field-verifiable). Both conditions currently share the
ship_after_fix section but only sanitize is script-enforced; matura-
tion lives in YAML comments (documentation, not enforcement). The
skip was applied MANUALLY at ceremony time per the just-added play-
book rule that every ship_after_fix skill's named condition must be
verified MET before its files ship. The class is banked as a 5.1
scanner-hardening candidate: the oracle gains a machine-readable
condition field (e.g. gate: sanitize|maturation + stage check
against canon frontmatter) and the mirror script enforces it. Until
then, the manual check stands as the enforcement mechanism.
The 3 shipped-tree files that the script would have produced for
these two skills were removed from the commit set post-mirror-run,
leaving the generated.json manifest entries as a documented
statement of oracle membership (accurate to intent) while the
tree-side files honestly reflect the maturation-gate skip.
No product-code changes; version fields untouched. This commit is
the canon->edges mirror only; the Release 0.5.0 version-bump commit
sits on top.
Standing pattern per pm-openrig 2026-08-05: every release's public
tip back-merges into the working lineage promptly post-release. This
is the reconciliation point for the divergence between origin/main
(v0.4.8 release commit 94e04abd + mirror commit 864cea6b) and the
0.5.0 working lineage (which restacked the 4.8 source code but not
the release-metadata commits).
Incoming from v0.4.8:
- Version strings bumped 0.4.7 -> 0.4.8 in the 4 package.json files
+ package-lock.json sync (canonical release-metadata).
- Two new shipping SKILL.md files at
packages/daemon/assets/plugins/openrig-core/skills/:
applying-a-permission-policy + delegating-work.
- Corresponding generated manifest updates:
scripts/product-public-skills.generated.json,
scripts/skill-edge-layout.generated.json,
scripts/skill-edge-digests.generated.json.
- Assorted skill-file body updates + edge deletions per the 0.4.8
mirror-pass canon-to-edges application.
Three conflict resolutions per pm-openrig pre-rulings 2026-08-05:
(1) packages/daemon/src/domain/rigspec-preflight.ts: take HEAD.
HEAD is the intended strict superset of the shipped 4.8 side per
the leg-6 §6 reconciliation (deliberate combination of shipped 4.8
policy-discovery + unshipped 0.5.0 activity-hook delivery).
Mechanical superset check applied: 364/365 non-blank incoming lines
present in HEAD literally; the 1 non-literal line is the warning
seed/append reshape (v0.4.8 assigned warnings from a single call;
HEAD seeds warnings empty and appends the same call's result via
push, so main-floor warnings emit first per §6 warning-order ruling).
Call-presence + result-incorporation verified; order pin
(permission-policy-discovery.test.ts) is the semantic proof.
(2) packages/daemon/test/permission-policy-discovery.test.ts: take
HEAD. Main-floor-first, policy-appended warning order per the §6 PM
ruling (fold-order = emission-order; RED-by-revert proven).
Presentation-only per the standing semantic fence — no compatibility
implication.
(3) packages/daemon/src/domain/rigspec-instantiator.ts: take HEAD.
The v0.4.8 imports (mkdirSync/writeFileSync from node:fs, assembleBundle
from context-packs/bundle-assembler.js, ContextPackEntry type from
context-packs/context-pack-types.js) served the OLDER shape where the
instantiator directly assembled context-pack bundles at startup (the
context_pack -> send_text delivery coupling). Slice-03's terminal
review verified this removal as intended per team-locked spec:
the coupling is DELIBERATELY gone from rigspec-instantiator.ts;
startup-validation.ts now REJECTS context_pack startup kinds with a
teaching error (compose with rig context compose + use a dedicated
delivery verb); the bundle router populates the library store WITHOUT
delivering (delivery-free-noun ruling applied at startup surface).
So v0.4.8's behavior is not silently dropped in 0.5.0 — it is
EXPLICITLY REJECTED with guidance, per team-locked spec.
Obligation-1 grep-proof for take-HEAD on rigspec-instantiator.ts:
`git grep 'assembleBundle|ContextPackEntry' packages/daemon/src/domain/rigspec-instantiator.ts
packages/daemon/src/startup.ts packages/daemon/src/domain/startup*.ts`
returns EMPTY at HEAD — the two imports are genuinely dead in the
instantiator path; the intended refactored usage lives in
context-packs/bundle-assembler.ts itself, HTTP route
packages/daemon/src/routes/context-packs.ts, and the library-service.
startup-validation.ts line 25 confirms the context_pack rejection.
BEHAVIOR CHANGE DISCLOSURE (deliberate; carries into release notes
per pm-openrig ruling): rig.yaml startup context_pack entries are no
longer delivered at instantiation. They are rejected with a teaching
error pointing at compose + delivery verbs. Users who adopted them
on v0.4.8 (shipped days ago, tiny exposure) should compose the pack
via `rig context compose` and deliver via a dedicated delivery verb
(`rig send --context`, `rig broadcast --context`, `rig walk`, or
`--context/--body-context` on queue create).
All other paths auto-merged cleanly.
TRUTHFUL ORIGIN (independently byte-verified from git history):
- 9da75d6f introduced packages/tui AND correctly synchronized its then-current lock state (added the
root "packages/tui" workspace-list member, the node_modules/@openrig/tui link, and the packages/tui
lock entry). It did NOT leave the lock unsynced.
- 4f78f931 later added the CLI "openrig-tui": "tui/dist/main.js" bin to packages/cli/package.json
(SOURCE); it did not itself touch package-lock.json. That CLI bin was synchronized INTO the lock
shortly after by 2156f530. (Refines the R2-summary phrasing, which attributed the CLI-bin lock sync
to 4f78f931; the lock sync is byte-proven to be 2156f530 — both pre-removal.)
- e9b25891 ("feat(policy): surface lifecycle discovery") is the ACTUAL REMOVAL COMMIT: it removed all
four effects from package-lock.json — the root workspace member, the workspace link, the packages/tui
entry, and the CLI openrig-tui bin — while the source manifests (root package.json workspaces,
packages/tui/package.json, packages/cli/package.json) RETAINED them. This is a source/lock desync,
not a missing-add.
- The current one-file package-lock.json +22/-1 tree mechanically RESTORES exactly those four effects
and nothing else (each mirrors the current source manifest verbatim).
- The desync reproduces on clean checkout at pre-repair base bdc30412 AND at exact parent 51209941
(npm ci --ignore-scripts => Missing @openrig/tui), so Slice-03 did NOT introduce it; ceremony-grade
clean-checkout probing found it.
Created via git commit-tree from the exact regenerated tree (no amend/rebase/cherry-pick; no source or
lockfile mutation). SUPERSEDES c49277b9 (technically-correct tree, but its body falsely blamed 9da75d6f
for the desync — the true removal is e9b25891).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Correct the R2 terminal NOT-CLEAR at frozen candidate b9d5006a in one
bounded successor:
- HIGH-1: 'rig context add' now walks every destination namespace ancestor
segment (lstat, reject symlink/non-directory, break on ENOENT) before the
copy, mirroring the daemon compose containment. A lexically-safe path-like
ref whose parent segment is a symlink can no longer escape the store.
- HIGH-2: the bounded, delimiter-free version predicate (isSafePackVersion)
is now enforced at the parseManifest ingestion chokepoint (covering the
library-service scan/index path) and at the CLI install boundary. Colon-
bearing and overlong versions are rejected live at scan and before install,
closing the FIXED-IN-BUILD id-forgery + ENAMETOOLONG classes.
RED-first; 5 new pins; broad context-pack + roster suites green; tsc daemon
and cli clean. Local-gated only; not product CLEAR.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
QA (review50-r1) caught a HIGH: the early --context cross-host rejection reads
opts.host BEFORE resolveCrossHostTarget folds the agent@rig@host sugar host into
it, so a sugar-form cross-host send with --context slipped the guard and reached
the remote argv — shipping a literal null with no message, or silently dropping
the context with one (the downstream text! assertion was false on that path).
Root cause fix: repeat the rejection AFTER the sugar fold. The late guard sees
the final opts.host (explicit OR sugar) and rejects --context immediately before
the sole cross-host call site — so cross-host argv cannot carry --context by
construction, and the downstream narrowing is sound because the guard it trusts
no longer has an upstream hole.
RED-first on both reproducers (no-message → never reaches argv/no null;
with-message → context never dropped). cli + daemon tsc read by direct compiler
exit. Everything else in 6b unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Completes the founder §4 delivery composition. All three flags share ONE
context resolver and the SAME all-or-nothing honesty contract rig walk
established: a missing/unreadable pack member aborts BEFORE any delivery,
names the member, exits non-zero — never a partial/silent context.
- shared resolver (cli/context-resolve.ts): resolveContextRef fetches the
pieces route's whole-content text + enforces the missing-member abort;
walkSizedWarning surfaces the §4 "walk-sized" advisory.
- pieces route: adds text/bytes (whole plain content) via the sealed
assemblePlainFiles, read-only — additive, walk ignores it.
- rig send --context <ref> / rig broadcast --context <ref>: resolve the ref
to its whole content and deliver it; oversized → walk-sized advisory;
single-seat/local in v1 (fan-out + cross-host --context are follow-ons).
- rig queue create --body-context <ref>: the snapshot rule — the RESOLVED
content becomes the qitem body (a snapshot, so a later library edit never
rewrites the handoff's history) and the ref rides as a body-context:<ref>
provenance tag. No queue-schema change; mutually exclusive with
--body / --body-file.
Consumes sealed getByRef/assemblePlainFiles read-only; zero sealed-domain
bytes changed. compose/send/broadcast/walk/queue now share one contract
end to end.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The pacing primitive from SPEC-rig-context-rig-walk-composition §3: "walk the
seat through it." Its own top-level verb (not an extension of rig send).
rig walk <seat> --through <ref | file...> --pace 10s
- Paced push-delivery: one /api/transport/send per piece; --pace elapses
BETWEEN pieces only (never a trailing pause); aborts on the first send
failure (no pacing into a broken pane).
- --through is a raw file list OR a single context ref (a pack → its ordered
member pieces via the new GET /library/by-ref/pieces route). A mix is
rejected.
- All-or-nothing honesty contract: any missing input — a local --through file,
or a pack member (reported by the pieces route's missingFiles before the
first send) — aborts BEFORE any send, names the missing member, exits
non-zero, sends ZERO pieces. One contract for both input forms.
- --pace parses 10s / 500ms / bare-seconds (default 10s).
The pieces route consumes the sealed getByRef + resolveFileWithinPack
read-only (ordered by manifest, deterministic) — no sealed-surface change.
Atom-6a of the §4 delivery composition; 6b adds --context (send/broadcast)
and --body-context (queue create) with the whole-content snapshot rule.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Retire the legacy colon-id compatibility that Atoms 2/3/4 deliberately
kept. A context pack is now addressed only by its path-like ref; the
opaque entry id becomes context-pack:<ref> (unique per pack — fixing a
latent shadowing bug where two refs sharing a manifest name+version
collapsed to one id).
BREAKING: colon-id addressing (context-pack:<name>:<version>) and the
/library/:id[/preview|/send] routes are removed.
- store: contextPackId(ref); remove parseContextPackId, idIndex, get,
getByNameVersion; id/relativePath share the ref as one source of truth.
- routes: remove /library/:id[/preview|/send]; add getByRef-backed
GET /library/by-ref/preview + POST /library/by-ref/send.
- cli: remove the resolvePack colon-id branch; preview/send by ref;
actionable migration error for a legacy colon-id input.
- ui: preview/send hooks + review page address by ref; the
/specs/library/$entryId round-trip is verified for a slash-containing
id (TanStack navigate/useParams round-trips natively — new test).
- rigspec (R1): startup_files kind: context_pack addresses by ref: with
an actionable migration error; expandContextPacks core extracted to an
exported expandContextPackStartupFiles resolving by getByRef, driven by
the real production helper in tests (two-refs-same-name materializes the
correct bundle).
- docs: as-built updated to ref addressing.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Complete the path-like verb set (list/show/add/rm). rm is the genuinely
new capability — list/show/add over refs landed in Atoms 2/3.
- store: removeByRef(ref) — additive after getByRef; unsafe ref is a
structured fail-visible error BEFORE any fs op, absent ref is
pack_not_found, a shipped builtin pack is refused (pack_not_removable,
mirroring add's operator-writable contract — never rmSyncs shipped
assets). Sealed scan/getByRef/compose + ref-safety.ts untouched.
- routes: GET + DELETE /library/by-ref (ref-primary read/delete surface;
?ref= query since refs carry '/'; registered before /library/:id so
by-ref is never captured as a colon-id).
- cli: `rm <ref>` verb (server-authoritative delete) + resolvePack
re-point of path-like refs through the sealed getByRef by-ref surface.
RED-first: store 5, routes 7, cli 4. Colon-id strip / rig walk /
--context / viewer-kill remain later atoms.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Guard NOT-CLEAR at cfc3e4cd (qitem 9e3865a4, artifact 58cdcb26): deriving
the compat index from nextByRef.values() assumed insertion order equals
discovery order — false after a same-ref override, since Map.set on an
existing key keeps its first-seen position. Legacy shared-id resolution
then pointed at the wrong physical pack.
Bounded fix (guard's smallest-fit option): delete-before-set at discovery
time moves an overridden ref to the map's end, making insertion order the
ACTUAL last-discovery order the derived legacy index depends on — the
pre-Atom-2 compat behavior restored. Primary ref semantics and all Atom-2
tests unchanged; the three-write discriminator added RED-first (1/21
failing at cfc3e4cd).
Gates: recursion+family 109/109 (daemon) + 44/44 (cli) · repo lint ·
daemon + cli builds. No Atom 3+, no colon-id strip, no widening, never push.
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard NOT-CLEAR at 8ab2d4fa (qitem b74e4576, artifact 617723f1): the ref
index aliased legacy colon ids, so distinct refs with identical manifest
name/version collapsed onto one entry, and same-ref root precedence held
only for resolve while list/count kept duplicates.
Bounded correction per the contract: entriesByRef (ref -> ContextPackEntry)
is now the PRIMARY index — getByRef, list(), and scan().count all observe
it; same ref across roots is last-root-wins EVERYWHERE (one row, count one,
resolve last); distinct refs with identical manifests stay independent by
construction. The legacy colon-id map is DERIVED from the final ref entries
after each scan as a compatibility-only index (strip = later atom) and can
never collapse ref identity; list() gains the ref as a deterministic sort
tiebreaker. Both guard probes added RED-first as canonical regressions
(failing 2/20 at 8ab2d4fa before the fix). All discovery/resolve/add
validation, structured rejection, no-write, symlink, recursion, and
colon-id coexistence controls preserved.
Gates: recursion+family 108/108 (daemon) + 44/44 (cli) · repo lint ·
daemon + cli builds. No Atom 3+, no widening, never push.
Co-Authored-By: Claude Fable 5 <[email protected]>
Slice-03 rig-context ATOM 2 (STORE), fresh-context build on sealed Atom-1
969034b8 (orch CALL=A, qitem ed7db558; audit BUILD-MAP 47ae4b0a §B):
- discovery is RECURSIVE and path-addressed: a pack is any nested dir
containing manifest.yaml; its ref is the relative path from the root
(spec §2 refs, e.g. packs/compaction-restore). Packs are leaves — a
manifest below a pack belongs to that pack's subtree. Symlinked dirs
stay untraversed (pre-existing lstat-dirent semantics carried in).
- the sealed assertSafePackRef is consumed at ALL THREE boundaries:
DISCOVERY (unsafe on-disk ref -> structured fail-visible scan error +
skip, never indexed), RESOLVE (new getByRef: unsafe -> structured
ContextPackError 'unsafe_ref' before any lookup; safe-but-absent ->
honest null; ref index keeps the same last-wins root precedence as the
id index), and WRITE/ADD (CLI add validates the install ref BEFORE
mkdir/cpSync via the CLI-side mirror of the sealed contract — the
established cross-package mirror pattern; unsafe names never touch
disk). ContextPackError's closed code union gains 'unsafe_ref'.
- colon-id addressing is UNTOUCHED and coexists (id strip = later atom).
- pins (RED-first at the sealed base: 17/18 + 6/7 failing): nested
discovery incl. the spec's own example refs, pack-leaf containment,
per-clause VERBATIM reject matrices at resolve (10 cases) and add (6
cases) asserting actual reject + no-write behavior per spec §2 and the
sealed contract (PM watch), last-root-wins ref precedence, coexisting
colon-id, symlink non-traversal, store-root-never-created no-write pin.
Gates: new suites 18/18 + 7/7; context-pack regression family 106/106
(daemon) + 44/44 (cli); repo lint; daemon + cli builds. No later atoms,
no widening, no colon-id strip, never push.
Co-Authored-By: Claude Fable 5 <[email protected]>
Slice-03 rig-context v1 (OPR.0.5.0.3), atom 1 of the founder-locked build. The §2
addressing contract ("the ONE thing that must be right") + R2 closure, RED-first.
Salvaged from the frozen candidate (assertSafePackName @ 37972eb6 charset;
isSafePackVersion @ b10c1618 verbatim) with the mandated BEND: single-component name
validation becomes PER-SEGMENT validation over the same charset, enabling path-like
multi-segment refs (packs/compaction-restore) while still banning traversal ('..'),
absolute paths, empty segments, and whitespace/YAML/id injection. The version token
stays bounded + delimiter-free (fixes R2 (a) ENAMETOOLONG + the store-id half of (b)).
New module src/domain/context-packs/ref-safety.ts: isSafePackRef/assertSafePackRef +
isSafePackVersion. 23 RED-first pins (valid path-like refs; traversal/absolute/empty/
injection/over-cap rejections; version bounded/delimiter rejections). Consumed by the
store discovery/write sites in the subsequent store atom. Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold-wave qitem 79159e6f LOW 2. The icon-fix QA (5a70e200, review50-r1) found the agent-detail
runtime field's `?? "— (not served)"` placeholder unreachable via the production API: the daemon
coerces a null runtime to the string "unknown" before the wire, so render.ts never sees null.
Repoint the defensive fallback to "unknown" to match that coercion — honest (what actually
renders), kills the misleading dead arm, and PINS the previously-unpinned null case (the icon-fix
guard's standing advisory). The sibling cwd field keeps its own "— (not served)" (a legitimately
servable value). RED-first: agent-detail-runtime-unknown.test.ts RED before the repoint (renders
'— (not served)'), GREEN after; render/icon/topology integration 51/51, tui tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold-wave qitem 79159e6f LOW 1. The bare-default table projection (ps.ts ~L942) and the
rigsOnHost scope count (~L1081) both applied the same `.status !== "stopped"` active-rig
check inline. Extract the ONE exported isActiveRig predicate they now share so the "1 of N"
scope label and the shown rows can never diverge (the derived-label-must-carry-liveness class
the LEG-2 QA flagged). Behavior-identical. RED-first: ps-is-active-rig.test.ts (import + 6 pins)
RED before the export (isActiveRig is not a function), GREEN after; ps + ps-scope-metadata
integration 53/53, cli tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold-wave qitem 79159e6f LEG 6. PM ruled ADOPT the 4.8 OPR.0.4.8.2 agnostic rip-out:
deliverStartup NEVER writes ~/.claude/settings.json permissions (provisionRigPermissions
/ CONVENIENCE_BASELINE C2 removed at 1c4e2db1). The source already carries the rip-out at
the restacked tip; this inverts main's contradictory 'provisions user-scope permissions'
pin into a rip-out GUARD (settings.json never written), citing the PM ruling, the 1c4e2db1
guarantee, and release-0.4.8/slices/02-agnostic-permission-rip-out (PERMISSION-POLICY-RESTEER).
Context delivery (statusLine + activity-hooks in settings.local.json) preserved.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold-wave qitem 79159e6f LEG 6 §6 reconciliation. The 4.8 permission-policy chain
(restacked from 0.4.7) and main's managed-activity-hook delivery (unshipped 0.5.0) both
emit READY-path warnings from preflightValidatedSpec. The auto-merge seeded policy first;
PM ruled activity-hook-first, policy-appended (fold-order = emission-order: main's
already-folded content is the floor, incoming restacked content appends). This seeds the
warnings array EMPTY so main's activity-hook/collision warnings ride first, and APPENDS the
permission-policy discovery warnings at the return. A warning-order pin freezes it
(RED-by-revert proven: policy-first fails). SEMANTIC FENCE pinned in the comment: the order
is PRESENTATION ONLY — first-warning-as-priority is a FINDING, not an ordering input.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
At the assembled 4.8 tip (909c33e2; Lane A amendment 05931d33 activating
permission_policy: none = deliberate_none), realize the previously-deferred
cross-lane pin at the SANCTIONED ALTITUDE SPLIT: the CLI WRITE half asserts
`--policy none` emits the ruled deliberate_none token; the daemon PARSE half
(none -> deliberate_none, floor==absent, never reads a file) is proven in
deliberate-none-amendment.test.ts. The CLI never imports the daemon (P6 fence
the QA verified); forcing the parse assertion CLI-side would breach it.
Also scope the least-destructive byte-survival claim (in-code comment + pin
name) to comment text/order/quoting/structure preserved; pre-# padding may
normalize — an honest API limit of yaml parseDocument (QA-recommended,
S17 honest-split; text-only, no behavior change).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add `rig setup --policy <name> --spec <path>`: a comment-preserving,
least-destructive edit recording a deliberate permission-policy choice into
an EXISTING RigSpec only (RULING-C) — locked/standard/open/yolo ->
builtin:<name>, none -> deliberate-none. A new install has no spec, so
nothing is written; the usability floor holds by absence. Render the frozen
founder-picked onboarding menu copy (permissionPolicyMenuLines).
The write path uses yaml parseDocument + doc.set + String(doc), preserving
comments (top + inline), key order, and formatting; pinned by a
discriminating byte-survival test (RED-by-construction against parse+
stringify). The `permission_policy: none` parse round-trip is deferred to
Lane A's amendment (it.todo at the assembled 4.8 tip).
Invariants pinned: P3 write-on-explicit-selection-only, P1 no
absent->deliberate_none, P2 honest render; anchors 1/2/3/5. No config
widening (P6); scaffolds untouched.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Lane-A carry (qitem 4b805845; PM scope ruling option A — the user-authorable
path gets a worked example; authored by review-r1, delivered via acting-orch,
byte-verified 1ebf873b / resulting docs fb03335f).
- packages/daemon/policies/examples/my-cautious-dev.policy.md: placement
policies/examples/ is the censused minimal-sibling convention (no prior
examples convention existed; PM has path-flexibility if they rule otherwise
at acceptance). Ships via build-package's recursive policies staging
(cp -r), and is structurally NOT materialized as a builtin — the
materializer's known-four allowlist admits only locked|standard|open|yolo
(builtin suite re-run green beside this carry).
- docs/reference/rig-spec.md: the delivered touchpoint patch applied;
resulting file hash matches the delivery manifest.
Co-Authored-By: Claude Fable 5 <[email protected]>
Lane-A hop 2 (qitem 4b805845), the RULED FORM OF RECORD
RULED-FORM-deliberate-none-2026-08-04.md sha256
5f37e40fbcdcf220dd103783d64fcac2e52945e96f736918abc2d8e84c768dc0
(chain: dev50-driver proposal -> acting-orch -> dev50-planner CLEAR ->
PM CONFIRM 2026-08-04 ~20:55Z):
- 'none' = the recorded deliberate choice: third origin deliberate_none,
NEVER resolves to any file (no resolvedTarget/declaringDir — the A3
no-squatting fence holds by construction), launchPosture floor —
IDENTICAL to absent; the entire change is record/provenance.
- The one vocab site (PolicyRefOrigin) extended; tsc-total consumers
widened at the rig-repository provenance setters (6 sites). policy-spec
PolicySource is a different vocabulary and is untouched.
- Discovery renders the three-way distinction: absent stays verbatim
'permission_policy absent; launch_posture=floor'; deliberate none
renders 'permission_policy: none (deliberate choice — recorded);
launch_posture=floor'.
- The sealed A3 'none'->error pin flip is this amendment's ONE sanctioned
sealed-surface change, cited by ruled-form hash in the pin (S19
precedent). New pins: never-resolves-by-construction (throwing reader),
near-miss custom-ref distinction, P1 HONESTY CRUX (absent never
rewritten/upgraded/reported as deliberate_none — discovery + persisted
rows), P2 POSTURE-IDENTITY (byte-equal floor claims, record-only
difference; provenance rows carry the choice). P3 is Lane-B's.
RED-first from assembled tip 9ed3b41a (5 pins failing pre-edit). Gates:
policy family 67/67 · daemon tsc/lint · daemon build. Never push.
Co-Authored-By: Claude Fable 5 <[email protected]>
QA NOT-CLEAR at 311e7257 (qitem 03a6194b): createDaemon() fires the
background kernel bootstrap by default, so an authenticated standalone
verifier run could race kernel apply into shared tmux — the probe was not
isolated as claimed. Bounded verifier-only correction (product bytes
unchanged): set OPENRIG_NO_KERNEL=1 before importing compiled startup,
tee construction output, and PIN both the deterministic skip line
('skipping kernel auto-boot') and the absence of 'kernel-boot: booting' —
a regression now fails the probe loudly.
Co-Authored-By: Claude Fable 5 <[email protected]>
OPR.0.4.8.3 (guard-sealed immutable plan v2 eea3c778, base 4694e86d):
- canonical repo source packages/daemon/policies/builtin/{locked,standard,
open,yolo}.policy.md — VERBATIM copies of the authoritative slice-03
content (full sha256 pins in the tests)
- build-package.sh stages $DAEMON_DIR/policies into the assembled package
(specs-style block) and reports a policies file count
- new materializeBuiltinPolicyReference(): known-four allowlist, byte-
identical mode-0444 read-only inspection copies at
$OPENRIG_HOME/reference/policies/builtin/, unlink-then-write refresh over
read-only copies, mode reconciled on the content-skip path, named skips,
never throws; wired best-effort in startup beside the reference-docs block
(../policies/builtin resolves from dist in repo-run AND assembled layouts)
- pins: builtin-policy-reference.test.ts (inventory, verbatim hashes, byte+
mode materialization, tamper-restore idempotence, stranger exclusion,
missing-source named skips) + check-packing-policies.test.mjs (staging
block + source hashes, ADDED BESIDE the packing floor)
- scripts/verify-packaged-builtins.mjs: repeatable D5b probe — assembled
build-info identity (full SHA, dirty=false), assembled policy hashes, and
the compiled assembled startup boundary via exported createDaemon() (no
listen) under a temp OPENRIG_HOME + isolated DB
No resolver, no writer surface, no builtinPackageTarget(), no Seam-B/CLI/
enforcement surfaces; fences per the sealed plan.
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard SEAM A NOT-CLEAR: validatePolicySpec returned ok:true for a spec with NO description
and for a config-surface spec omitting all four action-list fields — an advisory false-green
for incomplete custom specs, contrary to the schema (description required; config requires
default_posture + allow/ask/deny/destructive_class all present). Minimal correction (RED-first):
require description as a non-empty string; require all four config action-list fields present
as string arrays (use [] for none). 3 new RED pins proven (RED before fix); all four
authoritative built-ins still validate (each carries description + its required fields); parse/
body/serialization/A-only scope/fences unchanged. 14/14 GREEN; daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The permission-policy SPEC primitive (Seam A of the guard-cleared plan FINAL2 ca6b3b6d).
NEW permission-policy/policy-spec.ts:
- parsePolicySpec: parse the WHOLE contract from YAML FRONTMATTER, PRESERVE the Markdown body
verbatim; never throws (no-frontmatter / bad-YAML / non-object → structured ParseError).
- validatePolicySpec: ADVISORY, fail-open {ok, errors[]} — policy_schema_version===1, source/
surface enums, surface-appropriate EXCLUSIVITY (flag ⇒ launch_posture, no action sets; config
⇒ default_posture + string[] action lists, no launch_posture), scalar/list shapes. Does NOT
validate the semantic-action vocabulary (taxonomy owns it) nor resolve to any harness format.
- serializePolicySpec: deterministic canonical serialization; round-trip = serialize-stable +
semantic-equality + body-preservation (NOT byte-identity).
FENCES held: NO enforcement, NO config-writer, NO CLI, no flag-resolution (that is Slice02
yolo-mode / the skill). 11 RED-first pins; version-check discrimination proven (break ⇒ RED);
daemon tsc 0. Base 80336ff0 (Slice02 v6). VM-only; never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
QA NOT-CLEAR on dd61b137 was solely for v6-banned causal comments (all runtime flag
behavior, Claude/Pi/Leg-B, tests, lint, package, protected-state controls green). Remove
every claim that the unflagged Codex default does not grant managed --add-dir roots or fails
before readiness (yolo-mode.ts, codex-runtime-adapter.ts fork+fresh, native-resume-probe.ts);
comments now state ONLY the deterministic product contract — OpenRig launches OFF with
-s workspace-write and YOLO ON with -s danger-full-access; named profile passes through.
Empirical version-stamped unflagged behavior belongs solely in the append-only matrix.
ZERO runtime/flag/config/test behavior change (comment-only). Sweep: struck assertion absent
from source; diff bounded to 3 files, every changed line a comment; affected tests 117/117;
daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The no-flags OFF/no-profile floor is broken on the installed managed runtime: an
interactive Codex launch with required --add-dir writable roots and no -s flag is
rejected ("effective permissions do not allow additional writable roots") and falls
to shell before readiness (dev-qa firsthand, Codex 0.120.0). The clean-home codex
doctor default never exercised the --add-dir writable-roots interaction.
Centralize the fix in codexPostureArg (yolo-mode.ts), applied on ALL managed paths
(fresh / resume / native-fork):
- OFF, no profile -> EXPLICIT " -s workspace-write" floor flag (was empty)
- ON (YOLO) -> " -s danger-full-access" (was --dangerously-bypass-...-sandbox)
- OFF, named profile -> " -p <profile>" unchanged
Remove all "harness default" floor wording per PM amendment 1c; the floor is the
explicit workspace-write flag. RED-first: codex OFF/ON pins flipped across yolo-mode,
codex-runtime-adapter, codex-resume, native-resume-probe, node-inventory, and
resume-metadata suites. Source-only; no live host config touched.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Guard re-seal bounded corrections on 6581dbaf: (1) add a REAL PiResumeAdapter restore call-site
ON/OFF command pin (OFF -> --no-approve, ON/YOLO -> --approve) so pi-resume.ts cannot regress while
the pure piTrust helper stays green; (2) reword yolo-mode.ts shared-decision comment: the ON posture
is "maximally-permissive", not uniformly "full-bypass" — Claude/Codex = permission bypass, Pi =
RESOURCE TRUST (--approve), not a permission policy. Behavior unchanged. Daemon bounded suite green;
daemon+cli tsc 0. VM/source-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Guard NOT-CLEAR on 7f6b28f6: the "every seat" contract missed three managed paths the fresh-only
YOLO wiring did not exercise. Centralize the decision in yolo-mode.ts (claudePostureFlag /
codexPostureArg / piTrust) and route every fresh/resume/fork path through it:
- Claude restore (claude-resume.ts): was `claude --resume <token>` — now carries the posture flag
(the unconditional acceptEdits floor when OFF; --dangerously-skip-permissions when YOLO ON).
- Codex native fork (codex-runtime-adapter.ts fork branch): now codexPostureArg(profileArg) — OFF
keeps the profile/floor, ON forces --dangerously-bypass-approvals-and-sandbox. Fresh + resume
also routed through the same helper (behavior unchanged when OFF).
- Pi restore (pi-resume.ts) + launch: piTrust(configured) — ON forces `approve`, OFF the configured
posture. Pi's --approve/--no-approve govern RESOURCE TRUST, NOT a permission policy — wording
corrected in source comments/helper doc/test descriptions accordingly (PM/Leg-B framing). The
zero-permission-config-write property is scoped to Claude/Codex permission policy.
Test-first: yolo-mode.test.ts adds ON/OFF pins for Claude restore, Codex fork, and the
codexPostureArg / piTrust decisions; flipped the 3 claude-resume assertions (floor now present).
Daemon bounded suite 303/303, cli setup 24/24; daemon+cli tsc 0. VM/source-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Seam 4. Removed the ~/.claude/settings.json disclosure (the global allow-list C2 is deleted —
OpenRig no longer writes that file); made the settings.local.json purpose honest (statusLine +
the acceptEdits floor fragment; OpenRig bakes no allow/ask/deny policy). setup test flipped
(disclosure 6->5, settings.json entry absent). 24/24, cli tsc 0. HELD pre-guard-review.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Seam 3. New yolo-mode.ts yoloEnabled(env) reads the opt-in OPENRIG_YOLO setting (default OFF),
launch-flag surface only, zero config writes. Wired into all three harness launches: Claude
(--permission-mode acceptEdits floor / --dangerously-skip-permissions on YOLO), Codex fresh+resume
(harness-default floor / --dangerously-bypass-approvals-and-sandbox on YOLO, overrides -p profile),
Pi (this.trustPosture / approve->--approve on YOLO). Default OFF = floor unchanged. 209/209 across
8 suites, daemon tsc 0. HELD pre-guard-review (guard was at an interactive prompt).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Agnostic rip-out, seam 2 (Codex floor). Runtime-grounded: clean-home `codex doctor` (v0.144.4,
disposable CODEX_HOME) reports the no-config default as approval=OnRequest + filesystem sandbox
restricted + network restricted — i.e. Codex's own default IS the founder-selected workspace-only
floor. So OpenRig stops FORCING it.
- codex-runtime-adapter.ts (fresh launch) and native-resume-probe.ts buildCodexResumeCore (resume)
drop the forced `-a on-request -s danger-full-access`; Codex now launches with NO OpenRig sandbox/
approval flags = maximally agnostic ("setting nothing" = the floor). A named `-p` config profile
branch and the -C/--add-dir/-m workspace plumbing are unchanged.
- node-inventory.ts recovery guidance rewritten: no longer claims posture flags preserve approval/
sandbox; now states Codex uses its harness default unless a named config profile is present.
Tests: flipped 13 pins across 5 files (RED first — they pinned the forced flags — then GREEN), the
node-inventory summary assertion, and two misleading native-resume test names. 180/180 across the
codex+rip suites; daemon tsc 0. Guard-reviewed. VM/source-only (no live-host mutation); never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Founder-locked agnostic permission rip-out, seam 1. Rip-list = ASSESSMENT sha 5d450fdd. Base f81018fb.
OpenRig goes agnostic on permissions: it bakes NO config-file permission policy beyond the
usability floor. Two-surface rule — launch flags (stable) may be deterministic code; config-file
writes (chaotic) are removed.
- C1b/C1c: claude-settings.fragment.json stripped to ONLY permissions.defaultMode=acceptEdits
(+ enabledMcpjsonServers). The 13-entry allow-list and the rig up/down ask gates are gone.
- C2: provisionRigPermissions (the global Bash(rig:*) allow + _openrig_provenance writer into
~/.claude/settings.json) is DELETED — call, method, CONVENIENCE_BASELINE, and the now-dead
readStringArray. No retro-scrub: the new code never touches settings.json, so pre-existing
provenance-marked user files stay byte-identical. Trust/onboarding (C3/C4) plumbing kept.
- Floor kept byte-identical: fragment defaultMode acceptEdits + the --permission-mode acceptEdits
launch flag (unchanged).
Tests RED-first: agnostic-rip-out.test.ts (fragment has only defaultMode; fresh startup authors no
settings.json; no-retro-scrub byte-identical; launch-floor flag pin). Flipped the pins that
asserted the ripped rows (slice16-hygiene item 1, spec-library-starters, and removed the 7-test
provisionRigPermissions section with a rip-pointer). 201/201 across six suites; daemon tsc 0.
Guard-reviewed. VM-only, source-only (no live-host mutation); never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Additive host-usage section on the EXISTING `rig provider status` verb (PM: NO new verb):
renderStatusHuman gains a HOST USAGE block over the S-A rollup rows that already arrive
verbatim via /api/provider/status. Honesty AT THE RENDER: explicit_unknown reads 'unknown'
(never blank/ok), C3 window granularity kept, the conflict anomaly shown both-facts-visible,
the deployment-invariant provenance label surfaced, NO account identity emitted (rows carry
none). --json parity is FREE (verbatim res.data — values ARRIVE); human is the parity surface.
RED-first CLI pins: HOST USAGE section, explicit_unknown-as-unknown, conflict
both-facts-visible, provenance label, --json parity, no-account-id belt. + folds the free
guard advisory (svc-undefined ?? [] absent-key branch) into the S-B route suite. Strictly
additive (+84/-0, no verb restructure); base = S-B tip ae13fb64.
Verify note: provider.test.ts drives DaemonClient which is pre-existing-broken in the
reviewer worktree env (res.json() 'position 4' — the Atom-5 CLI-harness flake class); the
render was verified GREEN via a stub-client bypass + cli/daemon tsc clean; the pins run in
the gate env. provider-usage-route.test.ts (Hono-direct) 7/7 green here.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The external status site's entire contract: a thin ADDITIVE route over the S-A host rollup.
GET /api/provider/usage serves model.hostUsage (rollupHostUsage rows) VERBATIM from the one
read model — no derivation here (state/windows/resets_at/anomalies/provenance are built in
S-A; rows carry NO account identity by construction, only (host,provider)+topology seats).
Absent block -> honest empty array; unwired service -> loud 503.
RED-first route pins (provider-usage-route.test.ts): contract shape served verbatim,
conflict-anomaly rendering (both-facts-visible), explicit_unknown passthrough, the
no-account-id BELT at the route altitude, empty-rollup honesty, and the 503. Additive;
sealed S-A layers byte-untouched; provider-routes regression + S-A rollup suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Guard 6b2e84d3: the 120s conflict epsilon was falsifiable from below only.
Both boundary fixtures now anchor to the ABSOLUTE ruled value (119s ok /
121s conflict) plus a direct value pin — my first eps-relative fixture
tracked the drift and stayed green under the guard's own 30x probe, so it
was falsified and corrected to absolute anchors. Also folds the guard's
two invited hardenings: the account-leak belt now plants accountRef on the
claude lane too, and the optional-field narrow in the composition test.
Co-Authored-By: Claude Fable 5 <[email protected]>
One honest state row per (host, provider) — ok | nearing | limited-until-
resets_at | explicit_unknown — aggregated from the sealed C3 seat rows and C4
reactive signals. The three PM honesty conditions hold: (i) provenance-labeled
one-account-per-host deployment invariant, keys (host, provider) only, no
account identity emitted; (ii) conflicting seat reset schedules = first-class
anomaly + explicit_unknown, never a silent merge; (iii) granularity rides only
what C3 rows carry — codex has no meter yet, so absence is an explicit unknown,
never ok-by-silence. Additive hostUsage block on the read model (assembler
byte-untouched); the one empty-shape floor pin updated citing the A2 amendment.
Co-Authored-By: Claude Fable 5 <[email protected]>
Bounded correction of the two HIGH truth failures review50-r2 held C4
`4ce55996` on (verdict 82268f5d). Same five-file fence; sealed
provider-collect.ts / provider-signals.ts byte floors untouched.
HIGH 1 — reactive eligibility now follows the event, not just the seat:
- the PERSISTED activity runtime must itself be Codex, so a claude-code
activity attached to a Codex inventory/registry seat is never relabeled
as a Codex provider row;
- the account ref must name a KNOWN auth profile (present in auth-profiles/),
so a registry row pointing at an absent profile yields no actionable row
(explicit-unknown honesty, never a fabricated account).
HIGH 2 — classification binds to the structured provider-interruption
producer CLASS (the event class, rawEvent) matched against the declared
interruption vocabulary. The managed relay maps a tool_name into rawSubtype,
so a PermissionRequest with a rate_limit tool-name subtype is a permission
block, never exhaustion; rawSubtype and the derived reason are no longer
treated as producers. An unproven producer yields no row (fail-visible).
Tests: reproduce review50-r2's three failed probes as public-altitude pins
(POST /api/activity/hooks → store → getReadModel) plus a positive control;
re-point the exact-token/near-token/freshness/eligibility floors onto the
honest event-class producer path; drop the removed multi-field precedence.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OPR.0.5.0.4 C3 ONLY. claude-usage-reader.ts: an atomic tmp+rename cache writer
(writeProviderUsageCacheAtomic — writes a tmp sibling then renames over the target, so a
concurrent daemon read is never torn) + a daemon-local reader (collectClaudeStatuslineSignals)
that normalizes each Claude account provider_usage cache into provider_statusline rows via the
existing claudeStatuslineSignals. Explicit unknown+unknownReason for api_key
(api_key_no_subscription_windows), absent cache/pre-first-response (no_statusline_cache_yet),
and empty/malformed reading (empty_reading). Never throws; malformed → present-no-windows.
Wired as the Claude lane collectSignals source in ProviderServiceImpl.getReadModel (new optional
collectClaudeSignals dep); startup.ts discovers accounts + readings from the env-rooted
<OPENRIG_HOME>/provider-usage cache dir. RED-first: 6 reader pins (atomic tmp+rename discrimination
proven; the 4 unknown/rows cases) + 1 impl wiring pin. Provider suite daemon 65/65 + cli 8/8;
both tsc 0.
SCOPE: C3 only. NOT touched — C4 reactive tap, C2 Codex app-server, activity-accurate precheck,
D switch execution, BR-1 store (their own later seams). C1 stays sealed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Guard C1 NOT-CLEAR on 65fbbb62: collectFourBlockReadModel bound whenever a codex
auth-seat-registry row existed for the session name, ignoring the live seat runtime. A
STALE codex row whose session name was reused by a Claude seat wrongly bound that Claude
seat to a Codex account — contradicting the C1 contract (claude seats stay unbound with
seat_with_no_account).
Smallest fix: gate the bound branch on a real Codex runtime match (seat.runtime==="codex").
A non-codex seat stays unbound regardless of any same-session registry row. RED-first: one
discriminating pin (claude seat + stale same-session codex row => unbound). No C2/C3/C4/D
widening — only provider-collect.ts + its test. Provider suite 58/58; daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OPR.0.5.0.4 C1 done. ProviderServiceImpl wraps the collection seam: getReadModel is
fully wired (readCodexAuthMetadata + node-inventory across rigs via rigRepo.listRigs() +
clock); precheck composes the pure precheckSwitch gate (fails closed on unknown auth per
BR-3, assumes live conversation until the C2 activity seam); switchAccount precheck-gates
and returns an honest failed_safely (never fabricates succeeded — the rig-auth switch-exec
+ BR-1 action-record D seam is not yet wired).
DI: AppDeps.providerService + c.set in server.ts, constructed in startup.ts — the
/api/provider routes now return a real four-block (proof item 4) instead of 503.
4 RED-first impl pins (empty four-block; precheck fail-closed target_auth_unknown; switch
never-succeeds honest interim). Full provider suite green: daemon 57/57 + cli 8/8; both tsc 0.
Remaining for the slice (largely VM/QA + the D seam): live signal-collection lanes
(proofs 5/6), the switch-execution + action-record D seam (proofs 1/2/7), source-check
(3), BR-2 negative (8).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OPR.0.5.0.4 C1 resume. collectFourBlockReadModel(deps): the pure, injected collection
that getReadModel wraps — maps codex-auth profiles -> accounts[] (authState="unknown"
per BR-3 validate-at-use, never asserted active/needs_reauth from static disk), joins the
node-inventory seat universe to the codex seat-registry into rawBindings[] (unregistered/
claude seats -> unbound so assembleFourBlock emits seat_with_no_account), carries signals
(empty until the live collection lane), and delegates anomaly computation to the assembler.
Injected deps (readCodexAuth / listSeats / collectSignals? / now) keep the production
read-model path directly testable. 6 RED-first pins; BR-3 unknown-invariant proven to
discriminate (unknown->active => RED). daemon tsc 0.
Remaining C1: the production ProviderService wrapping this with real deps
(readCodexAuthMetadata + node-inventory across rigs + clock) + precheck (precheckSwitch)
+ switchAccount (D seam) + DI in server.ts/startup.ts (replace the 503 stub).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OPR.0.5.0.4 seam C1 first atom (paused mid-C1 per founder priority order — pre-guard-review WIP).
readCodexAuthMetadata(env): a DAEMON-LOCAL reader of the codex-auth on-disk contract
($CODEX_HOME||~/.codex : auth-profiles/*.json names + auth-seat-registry.tsv 6-col), so the
daemon never imports packages/cli. Reads profile NAMES + the TSV metadata ONLY — never the
profile *.json contents (token-class), verified by a test asserting no token/access_token bytes
surface (BR-6). Never throws; absent home/files -> empty; malformed registry rows skipped, never
fabricated. 4 tests RED-first; daemon tsc 0.
NOT YET DONE in C1 (resume): the getReadModel wiring (accounts authState=unknown per the BR-3
rail; bindings from node-inventory + this reader's seat join; claude -> seat_with_no_account) +
DI (server.ts/startup.ts). This atom is a self-contained reader unit; guard review of C1 pending.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.5.0.4 seam B (packet 3ffa3c22 §3). Thin Hono routes over one ProviderService read model.
- provider-service.ts: the ProviderService interface (getReadModel / precheck / switchAccount).
ProviderSwitchResult is a discriminated union that TYPE-ENFORCES BR-1: succeeded /
rebind_in_progress carry no reasons; failed_safely REQUIRES a non-empty [string, ...string[]]
reason tuple (a failed switch with no explanation cannot compile). reasons is string[] so
seam D can surface operational failures (auth-switch/action-record), not only precheck refusals.
- routes/provider.ts: status returns the whole model; accounts/bindings/signals are FILTERED
projections of the SAME getReadModel() (cannot diverge; bindings join provider via the accounts
map). Edge validation -> 400: bad provider enum, empty/whitespace account, missing or
whitespace-only seat/toAccount (trim for validation only; opaque refs passed through unrewritten),
non-object/null/array switch body, non-boolean forceUnsafe. Unsafe precheck is a 200 verdict;
switch business outcomes are 200 payloads (never transport errors). An unwired service returns a
loud 503 provider_service_unavailable -- never a fabricated four-block. Handlers are thin.
- server.ts: registered /api/provider (endpoints exist and 503 until seam C wires the service).
Tests RED-first: 12 (status, filtered projections, provider/account/whitespace 400s, null-body
400, unsafe-precheck 200 verdict, failed_safely 200 + fail-visible reasons, 503-when-unwired).
All provider daemon suites green; daemon tsc 0. Guard-reviewed vs packet 3ffa3c22. VM-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.5.0.4 seam A (packet 3ffa3c22 §3). The `rig provider` CLI verbs, daemon-backed via
DaemonClient (grammar per the auth.ts precedent), registered in index.ts.
- status | accounts | bindings | signals | precheck | switch, every verb --json-stable.
- Reads/precheck GET the daemon four-block routes (accounts/bindings/signals pass
--provider/--account as URL-encoded query params); switch POSTs the orchestration route.
- handleHttpError on every verb: prints the daemon error payload, exit 1 for 4xx / 2 for 5xx
(a 4xx is never accepted as success; a failed switch is never outcome-undefined exit-0).
- Human `provider status` renders the locked §3 projection (account rows, first-class dedup'd
binding-anomaly flags, freshest-signal age summary); --json stays verbatim; filtered blocks
are pretty JSON. Honest "Daemon not running" + nonzero exit when down.
- This seam does NOT itself compose rig auth — it only calls the route; the real precheck-gated
switch + rig-auth codex composition + durable action record are seams B/C/D (D owns the
CLI-local auth composition/package boundary). Seam A is not end-to-end until B-C-D land.
Tests RED-first: 8 (routing/params/json for the read+precheck+switch verbs, read-4xx and
switch-4xx error pins, human-status projection, daemon-down). CLI tsc 0. Guard-reviewed.
VM-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.5.0.4 sixth seam (packet 3ffa3c22 §1, proof item 4 unit shape).
assembleFourBlock({accounts, rawBindings, signals, asOf}) -> FourBlockReadModel. A PURE
assembler over already-collected inputs (no CLI/auth I/O — collection is a later seam):
- RawSeatBinding is a discriminated bound/unbound union so the collector never fabricates
account/binding data for an unbound seat.
- accounts[] and signals[] pass through unchanged (incl unmanaged claude profileRef=null).
- same_account_on_n_seats is computed only from repeated real account IDs across >= 2 DISTINCT
seat sessions (a Set, so duplicate identical raw rows for one seat cannot inflate the count),
attached deterministically (sorted seats) to each sharing bound row.
- each unbound seat becomes an unbound ProviderBinding carrying a required seat_with_no_account
anomaly (non-empty tuple). Anomaly asOf borrows the caller-provided read asOf; nothing invented.
Tests RED-first: 5 (mixed-provider preserve; same_account count+sorted-seats; explicit unbound
seat_with_no_account; distinct-seat de-dup so duplicate rows don't fabricate an anomaly; no
same_account from null-account seats). All 6 provider suites 32/32; daemon tsc 0.
Guard-reviewed vs packet 3ffa3c22. VM-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.5.0.4 fifth seam (packet 3ffa3c22 §1 precheck contract, proof item 7 unit portion).
precheckSwitch(input) -> { safe: true } | { safe: false, reasons[] } so the UI/automation
never offer an unsafe switch. Reasons: rebind_unsupported_for_runtime (claude target — rig
auth is codex-only); target_needs_reauth (authState needs_reauth) and the DISTINCT
target_auth_unknown (authState unknown — fails closed, never relabeled as re-auth);
would_strand_live_conversation (live turn); signal_unknown_or_stale (an automated trigger
whose shared BR-2 refusals intersect the unknown/stale subset — advisory_only is NOT
mislabeled). Reasons combine and dedupe deterministically.
Fail-closed: PrecheckInput is a paired union (manual = neither trigger nor now; automated =
both), so "trigger without now" is unrepresentable for typed callers; a runtime guard also
covers untyped/JS callers — a missing/unparsable now maps through the shared predicate to
stale, never a silent safe. Regression-pinned via a deliberate untyped call.
Pure predicate — no side effects. Tests RED-first: 9. All provider suites 27/27; daemon tsc 0.
Guard-reviewed vs packet 3ffa3c22. VM-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.5.0.4 fourth seam (packet 3ffa3c22 §2 reactive lane).
reactiveEventSignal() consumes at-limit / stream-failure / stop-error events immediately as
sourceClass=provider_event, authority=reactive_error — exhaustion evidence, not a remaining
meter, so usedPercent/remaining/resetsAt/window are all omitted. Closed ReactiveEventKind
union; provider/accountRef/asOf/staleAfter all required (staleness stays the shared BR-2
predicate's job). at_limit -> allow_switch_decision (a real exhaustion trigger per §2);
stream_failure/stop_error -> advisory_only. supportsNotification is OMITTED, not fabricated:
it is a per-account transport capability (true for Codex account/rateLimits/updated, false/
unknown for Claude statusline) that a generic reactive event does not establish. ProviderSignal
is not widened — event kind is input-side only.
Tests RED-first: fresh at_limit is BR-2-eligible; stream_failure and stop_error are refused
not_allow_switch_decision through the SHARED predicate (no duplicated stale coverage);
supportsNotification pinned undefined. All 4 provider suites 18/18; daemon tsc 0.
Guard-reviewed vs packet 3ffa3c22. VM-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.5.0.4 third seam (packet 3ffa3c22 §2 policy + BR-2 — the load-bearing negative).
Pure signalEligibleForAutomation(signal, nowIso) -> { eligible, refusals[] }. Automation
is eligible ONLY when the signal is known (source + authority), explicitly
allow_switch_decision, and provably fresh. Every failing condition adds an explicit,
fail-visible refusal reason: sourceClass_unknown, authority_unknown,
not_allow_switch_decision, no_freshness_bound, unparsable_freshness_bound, stale.
Fail-closed on freshness: a missing or unparsable staleAfter (NaN never compares fresh),
an inclusive expiry (now >= staleAfter is stale), and an unparsable now all refuse.
No side effects, no action execution, no orchestration — a predicate only.
Tests RED-first: 7 (eligible fresh/known/allow; unknown refuses 3 reasons; stale;
advisory_only; no-bound; unparsable-bound not-falling-through-to-stale; exact boundary).
All provider suites 14/14; daemon tsc 0. Guard-reviewed vs packet 3ffa3c22. VM-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.5.0.4 second seam (packet 3ffa3c22 §2 Claude lane + the 2026-07-31 research verdict).
claudeStatuslineSignals() normalizes the Claude Code statusline `rate_limits` object
(Pro/Max five-hour + seven-day windows) into signals[] rows:
- API-key account, before-first-response (no cache), and cache-present-but-empty all
become EXPLICIT unknown rows with distinct unknownReasons — usedPercent omitted (no
fabricated meter), automationUse=do_not_automate.
- Present windows -> provider_statusline rows: five_hour kept, seven_day normalized to the
§1 "weekly" enum; authority=account_cross_device (Claude.ai subscription windows are
account-level/shared); genuine 0/reset/asOf preserved; allow_switch_decision.
- supportsNotification always false (statusline has no push transport until OpenRig supplies
its own cache-update event — out of scope this seam).
Tests RED-first: 4 (api-key unknown, no-cache unknown, present-windows incl genuine 0,
empty-cache unknown). Both signals suites 7/7; daemon tsc 0. Guard-reviewed vs packet 3ffa3c22.
VM-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.5.0.4 first-seam follow-up (guard type-invariant). The unbound ProviderBinding
arm previously typed anomalies as BindingAnomaly[], so an empty or same-account-only
array compiled — leaving the dishonest "unbound seat with no seat_with_no_account
anomaly" state representable. Extract SameAccountOnNSeatsAnomaly / SeatWithNoAccountAnomaly
and require the unbound arm's anomalies to be a non-empty tuple
[SeatWithNoAccountAnomaly, ...BindingAnomaly[]]; the bound arm stays BindingAnomaly[].
Illegal state now fails to compile. daemon tsc 0; focused 3/3.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.5.0.4 provider-interruption-automation, first seam (packet 3ffa3c22 §1/§2).
- provider-types.ts: the §1 four-block read model. accounts[], bindings[] (a
discriminated bound/unbound ProviderBinding union over the same locked field
names — null for absent binding data, never a fabricated account; per-row
discriminated BindingAnomaly with exact locked variant fields), signals[] (the
research-verdict field set), precheck/switch result types, FourBlockReadModel.
- provider-signals.ts: codexRateLimitSignals() normalization. Feature-probe
negative and supported-but-empty reads become EXPLICIT unknown rows (usedPercent
omitted — never a fabricated zero; automationUse=do_not_automate, the BR-2 seed);
a real structured read yields one provider_structured_read row per native window
and preserves a genuine 0. Unknown DATA never erases KNOWN transport capability:
app-server absent -> supportsNotification=false, present-but-empty -> true.
- Tests RED-first: 3/3 (unknown-row honesty, genuine-zero preservation,
supported-empty transport capability). daemon tsc 0.
Guard-reviewed against locked packet 3ffa3c22 (two contract corrections applied:
transport-capability-vs-unknown-data, and the discriminated bound/unbound binding
without wire-shape drift). VM-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Founder defect: on a spec detail page reflexive Down keys produced zero
visible change — arrows drove the hidden explorer tree, body scroll lived
only on undiscoverable PageUp/Down + content-focus, the scroll hint was gated
behind the very state it teaches, and the "content ↑/↓" indicator named mouse
zones at keyboard users.
Fix (focus-model-preserving, one predicate): on a scrollable spec detail
(section specs + drill + contentMaxOffset > 0) reflexive ↑↓ scroll the body
regardless of which pane holds focus. Routing (input), the footer hint, and
the overflow indicator all read that ONE predicate, so the hint can never
promise a gesture the keys don't perform. The hint now surfaces on real
scrollability (ungated); the indicator is relabeled to a scroll control.
Topology tree-nav and link-hop on non-scrolling details are unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
slice-03 rig-context, SPEC §5/§7 STRIP + RENAME:
- STRIP: the context-window usage viewer command is removed entirely (file,
test, and index.ts wiring gone); `rig context` is no longer the viewer.
- RENAME: the `context-pack` verb is retired to `rig context` (one grammar,
no merged-noun ghost); `rig context-pack` is now an unknown command.
Pack storage contract is SALVAGED verbatim (kind/id/API/on-disk dir); the
context-usage telemetry subsystem is untouched; historical release records
preserved. The daemon instantiator's user-facing hint and the as-built docs
are reconciled to the new grammar.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review50-r1 QA finding @ fb8c8dcc: the honesty label's own count was incoherent with the
surface it cites. rigsOnHost = rigRes.data.length counted EVERY non-archived /api/ps entry
(stopped included), but `rig ps`'s bare-default header renders the ACTIVE-rig projection
(stopped rigs fold into a count line, not the field of view — the FR-1 contract at ~L942).
So "1 of 8 rigs shown; rig ps lists all" sent the operator to a surface that shows 6 — the
derived-labels-must-carry-liveness / width-clip-honesty class this atom exists to close.
Fix (ruling (a), primary): count the SAME active set rig ps shows —
rigRes.data.filter(r => r.status !== 'stopped').length — so '1 of N' matches what the
operator sees when they follow the hint. Chose the single active count over the dual-fact
form ('8 on host, 6 active'): the stopped rigs aren't reachable via the cited `rig ps`
surface, so naming them would invite a second, subtler incoherence; the active count is the
minimal directly-coherent signal (the atom's ~20-token ethos).
Pin (ps-scope-metadata.test.ts): a mixed 4-rig fixture (2 active, 2 stopped) asserts
rigsOnHost === 2 (not 4) and 'hint contains 1 of 2'. This is the LIVE-class the mocked pins
could not model. RED-first: reverting the count to data.length fails ONLY this pin (the
no-status fixtures in the other pins read as active, so they stay green). cli tsc clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The current-rig-only --nodes default is correct and STAYS; it just did not SAY it was
scoped, so a restoring agent could read a scoped list as the whole host (silent completeness
= silent loss — the width-clip honesty rule applied to CLI output). Exactly three parts,
nothing rides along: (1) when --nodes defaults to the session rig AND other rigs exist,
--json gains a machine-parseable scope:{rig, rigsOnHost, hint}; (2) one matching stderr line
on the human path; (3) NO default change, NO new flags — single-rig hosts and explicit
--rig/-A paths are byte-stable (nothing hidden -> no envelope, no hint).
RED-first pins (ps-scope-metadata.test.ts): scope object present+parseable on a multi-rig
host, the stderr hint on the human path, the no-default-change regression (single-rig
bare-array byte-stable + scoped node set unchanged). Verified via a stub client (the
DaemonClient http harness is pre-existing-broken in the reviewer env — res.json() 'position
4', the Atom-5/S-C flake class); cli tsc clean; pins run in the gate env.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
S19 follow-on founder ruling (bounds the marks ruling): where there is room,
write the runtime NAME as the value; an icon may accompany decoratively but never
substitutes. The agent-detail runtime field rendered mark-only (the ROUND-3/4
mark-as-value behavior); it now renders the name (honest placeholder when
unserved) with the mark trailing decoratively. Topology cards stay mark-only by
space (layout.ts/hatchet.ts untouched) — bounded, not reversed.
RED-first: new pin agent-detail-runtime-name.test.ts (2 cases) RED at 5de3b994,
GREEN after the fix. Two shipped-floor assertions in topology-view.test.ts that
pinned the reversed behavior ("the mark IS the value" / "spelled runtime is
dead") updated truthfully citing a4c9548a; every mark-styling-survival assertion
preserved. Full tui suite 214/214, tsc 0, build clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Permission-posture fast-follow: launch posture is configurable, the deny set is clobber-resistant, and dangerous ops are bounded by an honest prefix-gate.
Launch posture: the hardcoded `--permission-mode acceptEdits` launch
flag is replaced by a configurable posture defaulting to `dontAsk`. This
kills the ask-hang / freeze class that could park an autonomous seat on
a modal permission prompt with nobody to click through it. `dontAsk` is
the default because it matches what an autonomous seat can actually
respond to; `acceptEdits` remains selectable when a seat needs the
prior behavior; a deliberate `bypassPermissions` is preserved untouched
across writes.
Clobber-resistant deny set: the posture writes to user-level
`~/.claude/settings.json` instead of the project-local one-off approval
surface, so deny wins over project-local approvals. Writes are additive
(never destructive to sibling keys) and forward-migrate a legacy
`acceptEdits` value into the new schema. A deliberate
`bypassPermissions` value is preserved verbatim.
Bounded-dangerous deny set: `git push`, `gh pr create`, `npm publish`,
and `rig down` are the four bounded-dangerous ops gated by default.
`rig down` in particular is gated via the prefix superset
`Bash(rig down:*)` — harness rules at Claude 2.1.220 are prefix-only
(flag-only patterns provably fail to gate target-first forms such as
`rig down <rig> --force`, which were slipping through), so the prefix
gate is the only shape that actually holds. Plain `rig down` is gated
in 0.4.8; selective allowance (e.g. `rig down <rig>` for a specific
target) is deferred to 0.5.0 server-side enforcement.
`rig up` un-gated: the prior release's ask-gate on `rig up` is
superseded — `rig up` is a reversible op and does not warrant an
interactive gate.
DEPLOYMENT INVARIANT (documented): for the posture writes to reach
seats, the daemon's HOME must equal the seat's tmux HOME. This is a
2.1.220 settings-path class invariant that operators need to know for
the remote-host leg of any upgrade.
Formal CHANGELOG entries for 0.4.7 and 0.4.8 plus
`docs/releases/v0.4.7.md` + `docs/releases/v0.4.8.md` follow as a
combined post-cut docs pass.
Regenerates the product-repo public-skill manifests from the shared
authored sources and rewrites the shipped skill edges (plugin + spec)
per the oracle-declared membership and layout. Two new shipping skills
land — `applying-a-permission-policy` (Tier-A: the agent-driven
translation invoked by the shipped `rig setup --policy` verb for
Locked/Standard/Open policies) and `delegating-work` (Tier-A:
every-agent distribution) — with all others realigned to the current
layout.
Inputs pulled from the shared conventions layer (SHAs pinned in the
release ledger):
- Oracle membership (product-public-skills.yaml)
- Denylist rules (internal-tokens.yaml)
- Edge layout (skill-edge-layout.yaml)
- Skill canon root (the skills/ directory of the shared conventions layer)
Discipline applied by the mirror per each shipping skill:
- Layer-1 provenance-frontmatter strip (removes internal
source-evidence / assessment / qitem tokens before write).
- Layer-3 internal-fence strip.
- Deterministic leak-scan against the denylist; the script throws
on any post-strip content leak, so ship-set writes are only
reached when every shipping skill passes the scan.
No product-code changes; version fields untouched. This commit is the
canon->edges mirror only; the Release 0.4.8 version-bump commit sits
on top.
The duplicated CLI/daemon builders extract to ONE canonical source
(packages/cli/src/lib/scope/attestation-lineage.ts) carried across the
barred cli->daemon import boundary by the repo's mirror/codegen convention:
scripts/sync-scope-lineage.mjs emits the daemon's .generated mirror
(--check drift mode + node:test guard incl. the mutate-canonical->both-
surfaces-change demonstration). Both surfaces now import their package-
local module; the parity pin is reworked to hold canonical==generated
plus consumption, so the drift class dies structurally.
Co-Authored-By: Claude Fable 5 <[email protected]>
At the assembled 4.8 tip (909c33e2; Lane A amendment 05931d33 activating
permission_policy: none = deliberate_none), realize the previously-deferred
cross-lane pin at the SANCTIONED ALTITUDE SPLIT: the CLI WRITE half asserts
`--policy none` emits the ruled deliberate_none token; the daemon PARSE half
(none -> deliberate_none, floor==absent, never reads a file) is proven in
deliberate-none-amendment.test.ts. The CLI never imports the daemon (P6 fence
the QA verified); forcing the parse assertion CLI-side would breach it.
Also scope the least-destructive byte-survival claim (in-code comment + pin
name) to comment text/order/quoting/structure preserved; pre-# padding may
normalize — an honest API limit of yaml parseDocument (QA-recommended,
S17 honest-split; text-only, no behavior change).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add `rig setup --policy <name> --spec <path>`: a comment-preserving,
least-destructive edit recording a deliberate permission-policy choice into
an EXISTING RigSpec only (RULING-C) — locked/standard/open/yolo ->
builtin:<name>, none -> deliberate-none. A new install has no spec, so
nothing is written; the usability floor holds by absence. Render the frozen
founder-picked onboarding menu copy (permissionPolicyMenuLines).
The write path uses yaml parseDocument + doc.set + String(doc), preserving
comments (top + inline), key order, and formatting; pinned by a
discriminating byte-survival test (RED-by-construction against parse+
stringify). The `permission_policy: none` parse round-trip is deferred to
Lane A's amendment (it.todo at the assembled 4.8 tip).
Invariants pinned: P3 write-on-explicit-selection-only, P1 no
absent->deliberate_none, P2 honest render; anchors 1/2/3/5. No config
widening (P6); scaffolds untouched.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
QA-delivered drift pin (byte-verified 0603b97d): the duplicated CLI/daemon
lineage builders fail loudly the moment they diverge, until the shared-
extraction follow-on lands. Test-only.
Co-Authored-By: Claude Fable 5 <[email protected]>
A lock is a point-in-time attestation (ARCH-SHAPING 9d64ceb6 v2): re-approval
supersedes the prior stamp atomically (frontmatter-first, audit-second,
byte-restore on audit failure) with both attestations preserved in the
append-only log. The bare already_approved refusal now teaches the verb;
--reason is required; priors ride the frontmatter so the filesystem-local
scope audit shows amendment lineage.
Co-Authored-By: Claude Fable 5 <[email protected]>
Lane-A carry (qitem 4b805845; PM scope ruling option A — the user-authorable
path gets a worked example; authored by review-r1, delivered via acting-orch,
byte-verified 1ebf873b / resulting docs fb03335f).
- packages/daemon/policies/examples/my-cautious-dev.policy.md: placement
policies/examples/ is the censused minimal-sibling convention (no prior
examples convention existed; PM has path-flexibility if they rule otherwise
at acceptance). Ships via build-package's recursive policies staging
(cp -r), and is structurally NOT materialized as a builtin — the
materializer's known-four allowlist admits only locked|standard|open|yolo
(builtin suite re-run green beside this carry).
- docs/reference/rig-spec.md: the delivered touchpoint patch applied;
resulting file hash matches the delivery manifest.
Co-Authored-By: Claude Fable 5 <[email protected]>
`rig tui` is an explicit alias for what bare `rig` does (open mission control): it
delegates to the shared front-door openMissionControl (probe -> friendly degrade ->
resolveTuiPath + launch) — one launch path, no duplicated launch logic. Same
TTY-awareness on stdout and the same daemon-down friendly degrade; the front-door
usage lines gain a `rig tui` entry. RED-first: tui-command.test.ts pins the shared
code path via injected FrontDoorIo spies (call-graph pin, not a mirror), TTY + degrade
identical by construction, help registered.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Lane-A hop 2 (qitem 4b805845), the RULED FORM OF RECORD
RULED-FORM-deliberate-none-2026-08-04.md sha256
5f37e40fbcdcf220dd103783d64fcac2e52945e96f736918abc2d8e84c768dc0
(chain: dev50-driver proposal -> acting-orch -> dev50-planner CLEAR ->
PM CONFIRM 2026-08-04 ~20:55Z):
- 'none' = the recorded deliberate choice: third origin deliberate_none,
NEVER resolves to any file (no resolvedTarget/declaringDir — the A3
no-squatting fence holds by construction), launchPosture floor —
IDENTICAL to absent; the entire change is record/provenance.
- The one vocab site (PolicyRefOrigin) extended; tsc-total consumers
widened at the rig-repository provenance setters (6 sites). policy-spec
PolicySource is a different vocabulary and is untouched.
- Discovery renders the three-way distinction: absent stays verbatim
'permission_policy absent; launch_posture=floor'; deliberate none
renders 'permission_policy: none (deliberate choice — recorded);
launch_posture=floor'.
- The sealed A3 'none'->error pin flip is this amendment's ONE sanctioned
sealed-surface change, cited by ruled-form hash in the pin (S19
precedent). New pins: never-resolves-by-construction (throwing reader),
near-miss custom-ref distinction, P1 HONESTY CRUX (absent never
rewritten/upgraded/reported as deliberate_none — discovery + persisted
rows), P2 POSTURE-IDENTITY (byte-equal floor claims, record-only
difference; provenance rows carry the choice). P3 is Lane-B's.
RED-first from assembled tip 9ed3b41a (5 pins failing pre-edit). Gates:
policy family 67/67 · daemon tsc/lint · daemon build. Never push.
Co-Authored-By: Claude Fable 5 <[email protected]>
QA NOT-CLEAR at 311e7257 (qitem 03a6194b): createDaemon() fires the
background kernel bootstrap by default, so an authenticated standalone
verifier run could race kernel apply into shared tmux — the probe was not
isolated as claimed. Bounded verifier-only correction (product bytes
unchanged): set OPENRIG_NO_KERNEL=1 before importing compiled startup,
tee construction output, and PIN both the deterministic skip line
('skipping kernel auto-boot') and the absence of 'kernel-boot: booting' —
a regression now fails the probe loudly.
Co-Authored-By: Claude Fable 5 <[email protected]>
OPR.0.4.8.3 (guard-sealed immutable plan v2 eea3c778, base 4694e86d):
- canonical repo source packages/daemon/policies/builtin/{locked,standard,
open,yolo}.policy.md — VERBATIM copies of the authoritative slice-03
content (full sha256 pins in the tests)
- build-package.sh stages $DAEMON_DIR/policies into the assembled package
(specs-style block) and reports a policies file count
- new materializeBuiltinPolicyReference(): known-four allowlist, byte-
identical mode-0444 read-only inspection copies at
$OPENRIG_HOME/reference/policies/builtin/, unlink-then-write refresh over
read-only copies, mode reconciled on the content-skip path, named skips,
never throws; wired best-effort in startup beside the reference-docs block
(../policies/builtin resolves from dist in repo-run AND assembled layouts)
- pins: builtin-policy-reference.test.ts (inventory, verbatim hashes, byte+
mode materialization, tamper-restore idempotence, stranger exclusion,
missing-source named skips) + check-packing-policies.test.mjs (staging
block + source hashes, ADDED BESIDE the packing floor)
- scripts/verify-packaged-builtins.mjs: repeatable D5b probe — assembled
build-info identity (full SHA, dirty=false), assembled policy hashes, and
the compiled assembled startup boundary via exported createDaemon() (no
listen) under a temp OPENRIG_HOME + isolated DB
No resolver, no writer surface, no builtinPackageTarget(), no Seam-B/CLI/
enforcement surfaces; fences per the sealed plan.
Co-Authored-By: Claude Fable 5 <[email protected]>
PICKS-OF-RECORD v4 (sha256-16 14afeb74, founder amendment via pm-lead,
byte-verified; assignment 3c8e4665, qitem 266e4c14):
- clawd eyes become literally '><' — left '>' right '<', pointing INWARD,
squinty (image-cache 102): dark #181818 eyes on the #ad6755 terracotta
field, 2-cell form unchanged. Founder rationale: edge-eyes do not read
as eyes; inward brackets make it read as a FACE. Supersedes the round-4
outer-quadrant pair (kept in comment lineage).
- codex blue-hint pick (item a): CHEVRON-ONLY — codexMark's '>' now
carries the OFFICIAL sampled #6867aa (token codexBlue); '_' stays light
ink; text stays exactly ASCII '>_' (web identity); detail/topology only.
codexHintVariants stays as the decision-lineage record.
- items b (mono icons kept) and c (motion approved) require no code
change. Explorer mark-absence extended to ban the new '><' pair; every
▘▝-era pin updated citing the amendment; new chevron SGR pin on detail.
Gates: TUI 212/212 · repo lint · TUI/CLI/daemon builds. Front-door is
environment-flaky on this host today (PTY timeouts, counts vary 1-4 per
run, PARENT-REPRODUCED at the clean base via stash round-trip in this
worktree; the diff touches no packages/cli file). Never push.
Co-Authored-By: Claude Fable 5 <[email protected]>
S19 verification-capture atom, part 1 (QA LOCKED-SCOPE-CLEAR at 5348bb66,
qitem dfa0e13a, required successor item 1):
- explorer-absence.test.ts: for claude/codex/terminal agent rows the
explorer affirmatively proves — in the plain layer AND the compiled
styled stream — no runtime-mark glyph, no mark token/background SGR
class (clawd body bg/fg, clawd eye fg, terminal cell bg, mark ink fg,
codexBlue fg), and no spelled runtime word. Every absence matcher is
proven sensitive by a control-positive: the same snapshot's agent-detail
pages must contain exactly the glyph/SGR the explorer row lacks.
- superseded-comment cleanup named by the QA verdict (INFO): navigator.ts
header now names the locked ▦/≡ icons; theme.ts no longer calls the
shipped 16-color mark degrade values placeholders.
No product behavior change; taste fences untouched (codexHintVariants
still has zero production call sites — the three LOOK choices remain
open). TUI 212/212 · front-door 14/14 · repo lint · build.
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard round-7 correction (NOT-CLEAR at 1f78eaf2, qitem cc670c48), one
bounded fix: the selection marker no longer consumes the reduced-motion
acknowledgement. A SELECTED flashed row renders '»' in the marker slot —
still unmistakably the selection chevron, while visibly distinct from both
the plain '›' baseline and the unselected '≈' ack — so selection and the
fresh-output event are both observable under reduced motion and NO_COLOR,
with zero geometry/hit-map drift, and expiry returns the exact '›'
baseline. Compiled collision-matrix pin added for selected × fresh event ×
reduced motion (visible delta, marker-cell-only diff, hitMap equality,
NO_COLOR glyph, exact-baseline expiry); all unselected/inverse/ambient/
null/true→true/expiry cases preserved.
TUI 209/209 · front-door 14/14 · repo lint · build.
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard round-6 correction (NOT-CLEAR at 09aab69b, qitem 15623695), two
bounded fixes:
Finding 1 — the ROOT no-rig topology branch now consumes the refresh
owner's explicit load state like every other read surface: a real in-flight
cold start renders the braille spinner (line frames at 16-color, static
mark reduced) and marks motion-active; after settlement only a NAMED
rigs-summary failure or the proven no-rigs truth renders — never 'waiting'.
Pinned through the owner/render boundary composed from emptySnapshot() and
the default Topology view (start, two frame phases, settle-success-empty,
settle-named-failure, reduced motion).
Finding 2 — reduced motion now KEEPS the fresh-output state signal instead
of deleting it: the acknowledgement window ignores reduced motion and puts
a stable static '≈' glyph in the flashed row's marker slot in the PLAIN
layer (observable under NO_COLOR, zero geometry/hit-map drift; the SGR
inverse remains the full-motion animation and stays killed under reduced).
The ack expires with the same bounded window and schedules the one expiry
redraw. Selection keeps marker-slot precedence on the single selected row
(documented). The reduced-deletes-the-flash pin is replaced by positive
fallback + strip/geometry assertions.
TUI 208/208 · front-door 14/14 · repo lint · build. Event-source caveat
(onset-after-silence proof class) preserved verbatim for QA; all closed
branches and taste fences intact.
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard round-5 correction (NOT-CLEAR at b92c2a58, qitem e858ee70):
Finding 1 — the loading spinner now rides an EXPLICIT in-flight state owned
by the new refresh owner (live.ts): refresh start draws the loading frame,
settle (success or failure) clears it, rejection releases in-flight and keeps
the prior snapshot for retry. renderScreen takes load via RenderOptions
(omitted = settled); settled absence renders static honest truth — a NAMED
read failure or proven-empty — and never spins. main.ts draws the first
frame before the refresh starts instead of awaiting hydrate behind a blank
terminal.
Finding 2 — the one-shot activity row flash now keys per-seat FRESH PANE
OUTPUT: hydrate preserves served terminalActive verbatim (paneActive), whose
substrate is tmux #{window_activity} — it advances only when the window
receives output, so a false→true transition cannot occur without real new
pane output. The owner diffs per-agent after the first hydrate (first load
never flashes; null never flashes; the ambient rig-stream is not a source)
and renderScreen/stylize inverse-flash EXACTLY the flashed agent's explorer
row (flashRows), with expiry and reduced-motion honored. The ambient footer
ticker no longer flashes. Substrate note: the projection serves only the
derived boolean, so re-output while already active cannot re-trigger;
onset-after-silence is the observable event (documented in live.ts).
Pins cross the refresh owner (live.test.ts) and the render/stylize boundary
(motion.test.ts rewrite); hydrate settled-text pin updated citing this round.
TUI 203/203 · front-door 14/14 · repo lint · build. Three closed branches and
all taste fences preserved (codexHintVariants zero call sites; explorer
mark-absence intact).
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard NOT-CLEAR at 77d150e4 (qitem 29695aef), four locked-mechanics findings:
1. clawdSquareMark eyes were center-bunched (▝▘ = inner-right + inner-left
meet at the seam — the rejected form). Corrected to ▘▝: outer-left +
outer-right quadrants flank a real terracotta center gap; the pin now
asserts the actual quadrant geometry (U+2598 then U+259D), not a string.
2. Agent-detail runtime marks rendered as stripped plain text plus a spelled
runtime word. The runtime field is now a seg-channel row: the mark's own
fg/bg tokens survive to compiled SGR (clawd #181818-on-#ad6755, terminal
dark cell, codex ASCII >_) and no spelled runtime renders beside it.
3. spinnerFrame/flashActive were dead helpers. renderScreen now takes
nowMs/colorMode/streamFreshAt: the braille spinner (line frames at
16-color, static dot reduced) rides the three honest read-pending lines;
fresh footer stream output raises a one-shot inverse row flash; main.ts
supplies the clock, detects fresh stream tails, and keeps a bounded
redraw timer only while Screen.motionActive. Needs-page discipline: the
pending spinner degrades to the static dot while a ⚑ pulse is visible.
4. Explorer agent rows hardcoded an uncolored ●. rowStatusGlyph folds the
served AgentRow status vocabulary into the honest 4-glyph set + activity
roles; computeExplorerRows uses it and the navigator emits a status-badge
seg run (explorerMeta is now a run array) so the role paints in color —
active/idle/attention visibly distinct, unknown stays an honest ○.
Taste fences preserved: codexHintVariants still has zero selecting call
sites, explorer non-status icons stay monochrome, motion sign-off open.
TUI 191/191 · front-door 14/14 · repo lint (4x tsc) · build green.
Co-Authored-By: Claude Fable 5 <[email protected]>
LOCKED items only (the three LOOK choices stay UNFINALIZED candidates):
- Runtime marks OFF explorer rows → agent-detail (mark beside runtime) +
topology cards only; explorer meta = bare ctx% (name-first-untruncated and
no-spelled-runtime both hold).
- Clawd redrawn: clawdSquareMark ▝▘ — a square (2 cells at cell aspect) with
quarter-block eyes clearly APART on the terracotta field (wide-rect
center-bunch rejected); shipped claude mapping updated; minis remain
downsample candidates for the record.
- Rig glyph ▦ / pod glyph ≡ everywhere (explorer icon, rig container tab,
pod container tabs); explorer icons MONOCHROME (color for status only).
- Official codex blue token #6867aa (sampled provenance); the three
blue-hint variants exist as CANDIDATES (chevron/outline/none) — nothing
picked; the shipped codex mark stays the approved ASCII >_.
- Motion mechanics (motion.ts): reduced-motion kill-switch honored by the
cursor blink · braille spinner + 16-color line fallback + reduced static
dot · one-shot flash primitive · determinate bar (REAL fractions only,
wired to the detail ctx line) · needs-you ⚑ slow pulse (the only
persistent motion in its region, pinned) · max-one-persistent pinned for
the cmd bar.
TUI 185/185 · front-door 14/14 · lint · build. Superseded S19-MR2 explorer
pins narrowly relocated to card surfaces (terminal-bg + clawd-paint
discriminators preserved).
Co-Authored-By: Claude Fable 5 <[email protected]>
The cursor's locked purpose is telling the user they CAN type: the shell
accepts input from the empty state, so the honest readiness affordance is
one always-rendered ▊ at the insertion point (empty + non-empty), SGR blink
via stylize, zero hit-geometry effect, no new focus state. Empty-absence pin
flipped POSITIVE (cmd ▸ ▊ + blink); end-of-text pin retained. Guard's two
bounded strengthenings folded: the absent-cwd pin asserts the literal
'— (not served)', and the stale glyphs.ts one-green-bucket comment refreshed.
TUI 173/173, lint, build.
Co-Authored-By: Claude Fable 5 <[email protected]>
clawdFounderMark per the founder's round-2 design (flat #ad6755 block, two
#181818 eye bars, no limbs — exact source values); theme gains 16-color bg
codes so the red-block degrade is RENDERED-CONFIRMED, not assumed. Terminal
mark recorded LOCKED as-is. The Codex-blue sourcing record is honest: the
local source of record is monochrome for Codex (the only blue is pi's),
openai.com 403'd, mirrors ruled non-authoritative — sheet carries labeled
placeholder accents pending an authoritative sample. TUI 173/173.
Co-Authored-By: Claude Fable 5 <[email protected]>
MR3: the ● bucket splits by ACTIVITY into color roles (actActive/actIdle;
○→actDetached, ◐→actAttention) — glyph honesty unchanged, roles pinned
distinct, values are placeholders for the founder palette pick. MR4: the
agent detail renders the served cwd verbatim (full absolute path; honest —
when not served) — hydrate maps the existing NodeInventoryEntry.cwd field
(R7-clean). MR5a: a ▊ cursor cell renders while composing in the command
bar (the existing input state, no new focus mechanism) and stylize gives it
SGR blink — additive to the slice-11 lock, guard diff review owed. MR5b:
chrome one-step contrast bump (58,63,75 → 76,84,99); the text-only-highlight
pin held as the regression guard. Guard's non-blocking wording cleanups
folded. TUI 173/173.
Co-Authored-By: Claude Fable 5 <[email protected]>
(1) The layout NEVER truncates an agent name: the meta yields entirely when
the full name leaves no room (name-first); only the physical pane-edge clip
remains (pad()'s boundary ellipsis, pinned to the last column only).
(2) Explorer meta rides a seg channel (Screen.explorerMeta) so the runtime
mark's own tokens — including the terminal dark-cell bg — survive
stylization; discriminating pin: terminal >_ carries 48;2;12;10;9, codex
does not; strip-invariant held.
(3) stray packages/tui/_ removed.
(4) Mini marks are OUTPUTS of clawdDownsample (majority-vote quadrant
downsample of the transcribed grid — function-tied pin); the row token is
the LOCKED web >_ (❯ restyling = founder call); the eye-vanish fidelity
limit is stated in the regenerated mr7 sheet, not hidden.
TUI 169/169.
Co-Authored-By: Claude Fable 5 <[email protected]>
Marks derive 1:1 from RuntimeMark.tsx (rect grid transcribed + pinned):
faithful 16x8 half-block clawd (prototype scale), mini row candidates,
codex/terminal ❯_ family. Theme gains exact mark colors + bg support
threaded through canvas/segs/stylize. Explorer meta per the guard re-seal:
name-first untruncated + mark + ctx, no runtime word, no middle dot.
Superseded slice-17 pins narrowly updated; per-view pin recalibrated (its
clip premise was beaten by the density gain). mr7 taste packet banked.
TUI 168/168.
Co-Authored-By: Claude Fable 5 <[email protected]>
Card titles are MEMBER-only (confirmed-prefix strip, the navigator's rule;
identity stays logicalId in every zone/action — display only via the new
PlacedNode.title) and card meta drops the pod suffix: the pod is named ONCE
by its container tab. Superseded slice-17 display pins narrowly updated per
the guard re-seal language (fingerprint/meta locators to member form —
assertions' intent unchanged). Density at the fixed 140x34 proof viewport:
BEFORE 10 → AFTER-MR1 12 visible nodes (interim; the runtime-mark leg
shrinks cards further). TUI 164/164.
Co-Authored-By: Claude Fable 5 <[email protected]>
The prior unquoteShellArg stripped outer quotes without verifying the argument is ONE
shell token, so a user command with multiple quoted args whose LAST arg ends in the relay
suffix (e.g. node 'user-arg' '/tmp/.openrig/hooks/scripts/activity-relay.cjs', or the
double-quoted form) decoded as owned and was DELETED on reconcile — user data loss.
Fix (bounded): ownership now requires a CANONICAL one-token round-trip — decode the single
shellQuote token, then re-encode; it is owned only if shellQuote(decoded) === the argument
verbatim AND decoded ends with the owned relay suffix. Multi-arg / non-canonical commands
re-encode differently and are rejected (preserved). Apostrophe-cwd owned entries still
round-trip. No other scope.
RED-first: preservation pin (single- + double-quoted multi-arg user commands ending in the
suffix survive reconcile) failed pre-fix, passes post-fix. Delivery 20/20 (apostrophe +
all prior ownership pins intact), focused 73/73, daemon tsc clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
isOwnedRelayCommand wrote owned hooks as node ${shellQuote(relayDest)} but matched
with a naive quoted-arg regex that broke on shellQuote's '"'"' apostrophe escaping.
A cwd containing a legal apostrophe (O'Brien, O'Neil) therefore made the owned hook
UNRECOGNISED: re-enable appended a fresh copy every project() cycle (unbounded
accumulation x4 events) and disable never stripped it (dangling managed hook).
Fix (localized, M1 only): decode the node argument by reversing shellQuote (single-quote
unwrap with '"'"' -> '), tolerating a legacy double-quoted form, before the owned-suffix
check. User hooks still never match (only node <arg ending in the owned relay path>).
No adapter chmod / no other surface touched.
RED-first: new single-quote-cwd pin (enable x2 -> exactly one owned entry/event; disable
-> zero) failed at 8!=4 pre-fix, passes post-fix. Delivery 19/19 (all prior ownership
pins intact), focused 72/72, daemon tsc clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The projected relay must be 0755 and production PRESERVES the source mode (no adapter
chmod policy), so the committed/packaged asset itself must be executable. The tracked
asset was git 100644 -> preserved 0644 through tarball/install/projection, falsifying
the 0755 contract while reporting projected. Set the tracked asset to 100755 (mode-only,
bytes unchanged) so preserveMode yields 0755 end-to-end.
Regression stats the REAL committed asset mode (not a synthetic 0o755 fixture), so a
future 0644 regression fails the suite. Verified: git 100755; npm-pack tarball entry
-rwxr-xr-x; build exit 0; focused delivery/rip-proof/codex/resolver/up-warning/route 71 pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Test-only corrections on the fa71e83f chain; production/spec bytes frozen.
Finding 1 — reachability at SHIPPED altitude: replace the in-memory AgentSpec pin
with resolveAgentRef of the ACTUAL shipped development/implementer profile (which
selects shared:claude-activity-hooks) from disk, through the real resolveNodeConfig
-> planProjection -> adapter, asserting the claude_activity_hooks entry enables.
Finding 2 — warning crosses the real route: new /api/up route test drives an apply
of a claude_activity_hooks seat with the delivery assets unavailable (injected via
a new createTestApp claudeActivityAssets test-helper opt forwarded to the
PodRigInstantiator); asserts rc0 (status<400, rigId returned) AND the exact nonfatal
warning in the response body — i.e. it crosses the real /api/up boundary.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Guard NOT-CLEAR c8a8932a correction, one complete tip on the 18b21e38 chain.
Part 1 — prevalidate before mutation: the adapter now validates BOTH the relay
source AND a nonempty derived event set before any strip/copy/write. A missing/
malformed/zero-relay-event canonical manifest (or missing relay) preserves existing
managed hooks + settings bytes, copies nothing, and is reported skipped-not-projected.
Part 2 — ship + select through the real path: new shipped runtime_resource
claude-activity-hooks (type claude_activity_hooks) + marker asset, selected in all
15 claude profiles; a real resolver->planner->adapter integration pin proves enable
is reachable (not a hand-built plan).
Part 3 — nonfatal rig-up warning: ONE shared validation module
(claude-activity-hooks.ts) is consumed by BOTH the adapter reconcile and preflight
(no drifting parsers). Preflight emits a nonfatal delivery-gap warning (testable
asset-path DI); the READY-path preflight warnings are propagated into the successful
instantiate/materialize result alongside launch/materialization warnings, so rig up
stays rc0 while surfacing the exact warning through the route + CLI.
RED-first throughout; daemon tsc clean; rip-proof + Codex activity suites green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Corrects the aceed9e9 tip against guard's demonstrated defects:
- EXACT ownership: strip only the `node <quoted relay path>` command shape, not a
loose substring, so a user command that merely contains the path (e.g. echo) is
preserved. Changed-prefix still replaces (not duplicates).
- FAIL-CLOSED: an unparseable settings.local.json is left byte-for-byte untouched
(was clobbered to {}).
- MISSING SOURCE: enable with an absent relay source delivers NOTHING (no dangling
commands) and reports the entry skipped, not projected (no false success claim).
- Event vocabulary DERIVED from the canonical claude.json manifest (relay events
only); removed the parallel hand-maintained event/timeout constants.
- Use shared shellQuote for the owned command.
- Test header: COEXISTS with (does not supersede) the distinct-seam rip-proof suite.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Deliver OpenRig activity-relay hooks as a managed, reconciled projection driven
ONCE from the always-run ClaudeCodeAdapter.project() seam (r3): ENABLE when a
claude_activity_hooks runtime_resource entry is present (copy activity-relay.cjs
to <cwd>/.openrig/hooks/scripts/ 0755 + upsert the owned command for exactly the
4 relay events SessionStart/UserPromptSubmit/Stop/Notification); DISABLE strips
owned entries + prunes even when the profile removes the resource and no entry is
emitted (production-reachable durable disable). Owned entries recognised by the
relay script path (changed-prefix = replace, not duplicate); user hooks always
preserved; dedicated owned-entry reconcile (not additive mergeJsonFragment).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
config-store's DEFAULTS (module scope) resolve db/transcripts paths via
getDefaultOpenRigPath → getOpenRigHome → readOpenRigEnv("OPENRIG_HOME",
"RIGGED_HOME"), read at MODULE-LOAD time — so an ambient home under EITHER the
primary OPENRIG_HOME or the legacy RIGGED_HOME alias freezes the default paths at
import, before any beforeEach can scrub it. "resolve() returns defaults" then
fails (path becomes $HOME_ALIAS/openrig.sqlite, not ~/.openrig/…).
Neutralize BOTH aliases via vi.hoisted (runs before the module graph loads) and
restore them in afterAll. Test-only; production config resolution is unchanged.
RED reproduced for each alias (OPENRIG_HOME and RIGGED_HOME → "expected
'…/openrig.sqlite' to contain '.openrig/openrig.sqlite'"). GREEN 18/18 under all
three ambient conditions — primary set, legacy set, and neither — plus cli tsc
clean; diff is test-only. No push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Both local `rig send` transport-failure catches (single-seat and fan-out) called
printTransportFailure(err) then exited BEFORE their `if (opts.json)` branch, so a
`--json` consumer got empty stdout + exit 1 on the most important failure case
and had to shell-parse human stderr prose.
Fix (mirrors the established printDaemonNotRunning convention, daemon-lifecycle.ts):
printTransportFailure takes opts?:{json?}. Remediation values are defined once;
on --json it emits {"error":{fact,consequence,action}} on stdout and returns;
otherwise the existing three human console.error lines render byte-identical
(indentation added at render). Both call sites thread { json: opts.json }.
No discrete `target` field (a design call, out of this bounded fix); fact already
carries the resolved target + cause. Human stderr/exit behavior unchanged.
Tests (channel-separated, since captureLogs merges streams):
- R7c/R7d (RED→GREEN): single-seat + fan-out --json → exactly one parseable stdout
JSON envelope, empty stderr, exit 1.
- R7e/R7f: human path keeps stdout empty (stderr-only remediation), exit 1.
- R7/R7b human remediation pins unchanged (byte-identical).
Gates: R7 family 6/6 green; cli tsc --noEmit clean. (The 21 daemon-dependent
send.test.ts cases fail identically to baseline in this deps-symlinked worktree /
single-file run — an env limitation, not a regression; full-suite QA is dev-qa's
wholly-fresh exact-SHA pass.) No push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Atom 4 integration residue: the restack 81a123c2 restored the reviewed
precompact-hook.mjs, but scripts/skill-edge-digests.generated.json still carried
the pre-restoration digest (3b739a39…), failing `mirror-skills --check` in the
root test:repo gate.
Regenerated via the canonical generator (packages/daemon/scripts/gen-control-plane-json.mjs
through generateControlPlaneJson; NOT hand-edited). The only change is the
claude-compaction-restore/scripts/precompact-hook.mjs digest → be1161c8…, matching
the restored file. The other three control-plane manifests regenerate byte-identical
(authority YAMLs unchanged), so the diff beyond 81a123c2 is exactly one generated
digest line.
Gates: test:repo GREEN (52 pass/1 skip/0 fail, mirror-skills --check exit 0);
daemon tsc GREEN; diff-check = only the generated digest; clean identity.
No hook behavior edited; no mirror churn, source cleanup, fold, push, or lock.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Causal reconciliation, not a cherry-pick: main's hook was byte-identical to
reviewed 12f4be41 (via the 0.4.1 release squash) until the 4c6d8883 skills
asset regeneration replaced it with a stale pre-fix copy (its additions are
purely old shapes; no intended behavior lost by restoring). Restored to the
pre-clobber bytes exactly: restoreMapPath (per-seat extra path or explicit
null), foreign-seat global refusal, per-seat-extra preference — enforcer
semantics duplicated by design in the portable asset. Hook matrix 99/99
(precompact + core-plugin + bridge-reader + seat-scoped + enforcer); packaged
cli/daemon projection carries both behaviors; daemon residue now exactly the
4 atom-1..3 failures from the intake. Forensic note in the lane artifact dir.
Co-Authored-By: Claude Fable 5 <[email protected]>
Corrects the stale fail-closed claim at cli-reference.md:782-783 to match
shipped behavior (send.ts:174-182 help text; :147 --force back-compat no-op;
implementation verified at :78/:301/:345 by independent R1 review). The AFTER
wording reuses the shipped help text so the two surfaces cannot silently
drift again. Candidate artifact 5fe69a4fd159d490…, R1 APPROVE on record.
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard SEAM A NOT-CLEAR: validatePolicySpec returned ok:true for a spec with NO description
and for a config-surface spec omitting all four action-list fields — an advisory false-green
for incomplete custom specs, contrary to the schema (description required; config requires
default_posture + allow/ask/deny/destructive_class all present). Minimal correction (RED-first):
require description as a non-empty string; require all four config action-list fields present
as string arrays (use [] for none). 3 new RED pins proven (RED before fix); all four
authoritative built-ins still validate (each carries description + its required fields); parse/
body/serialization/A-only scope/fences unchanged. 14/14 GREEN; daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
6c7b9240 gave the CLI proof template the accepted two-code-span dual-context
sentence but malformed the daemon sliceProof() copy into a single combined
span, breaking root-PROOF byte parity. Minimal fix per the guard preflight:
only that sentence changes, to the CLI's exact bytes; the parity test gains
the three content pins (repo path span, installed-path span, never the
internal daemon/docs/reference staging path). Two-file boundary held.
Parity 34/34, adjacent CLI scaffold suites 14/14, lint, build:package,
packing + freshness checks green. NOTE for review: a grep for '(installed:'
still hits the daemon PRD-template sentence — that form is SHARED with the
CLI templates and byte-parity-proven; it is not the atom's defect.
Co-Authored-By: Claude Fable 5 <[email protected]>
The permission-policy SPEC primitive (Seam A of the guard-cleared plan FINAL2 ca6b3b6d).
NEW permission-policy/policy-spec.ts:
- parsePolicySpec: parse the WHOLE contract from YAML FRONTMATTER, PRESERVE the Markdown body
verbatim; never throws (no-frontmatter / bad-YAML / non-object → structured ParseError).
- validatePolicySpec: ADVISORY, fail-open {ok, errors[]} — policy_schema_version===1, source/
surface enums, surface-appropriate EXCLUSIVITY (flag ⇒ launch_posture, no action sets; config
⇒ default_posture + string[] action lists, no launch_posture), scalar/list shapes. Does NOT
validate the semantic-action vocabulary (taxonomy owns it) nor resolve to any harness format.
- serializePolicySpec: deterministic canonical serialization; round-trip = serialize-stable +
semantic-equality + body-preservation (NOT byte-identity).
FENCES held: NO enforcement, NO config-writer, NO CLI, no flag-resolution (that is Slice02
yolo-mode / the skill). 11 RED-first pins; version-check discrimination proven (break ⇒ RED);
daemon tsc 0. Base 80336ff0 (Slice02 v6). VM-only; never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The stale positional-timestamp call predates bd10fee6's contract change; the
sole runtime caller (routes/queue.ts) uses the options object, so the test
moves to the supported shape rather than the API regressing to an
undocumented positional form. Adds the scoped/bounded discriminators (rig
scope honored, limit honored). queue-repository + queue-routes 71/71, queue
CLI 77/77, daemon typecheck clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
QA NOT-CLEAR on dd61b137 was solely for v6-banned causal comments (all runtime flag
behavior, Claude/Pi/Leg-B, tests, lint, package, protected-state controls green). Remove
every claim that the unflagged Codex default does not grant managed --add-dir roots or fails
before readiness (yolo-mode.ts, codex-runtime-adapter.ts fork+fresh, native-resume-probe.ts);
comments now state ONLY the deterministic product contract — OpenRig launches OFF with
-s workspace-write and YOLO ON with -s danger-full-access; named profile passes through.
Empirical version-stamped unflagged behavior belongs solely in the append-only matrix.
ZERO runtime/flag/config/test behavior change (comment-only). Sweep: struck assertion absent
from source; diff bounded to 3 files, every changed line a comment; affected tests 117/117;
daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
FR-1 (A1-amended): test:workspaces enumerates the blocking set readably
(daemon + cli + tui); NEW advisory test:ui runs packages/ui vitest at will.
lint/build/build:package/test/test:repo byte-untouched — UI typecheck and
the shipped UI dist stay blocking.
FR-2: docs/reference/developing.md created — gates/lanes table, the no-UI-
sync norm verbatim (hooks mirrors unmaintained; a test:ui failure = moved
API contract signal, not a broken gate), the wording rule (experimental /
maintenance mode / best-effort / CLI primary — never 'deprecated'; zero
'deprecated' hits grep-proven).
FR-3: the diff is EXACTLY two files (root package.json + the new doc); no
doctor/bundling/ui.enabled/build-package/starter-skill changes.
Co-Authored-By: Claude Fable 5 <[email protected]>
Root: canvas.zone() recorded hit zones regardless of the viewport clip, so
the keyboard target list contained fully offscreen nodes the mouse could
never reach — Enter drilled invisible state (18/67 targets at 140x34, 35/67
at 80x34 per R2). Fix at the single source: zones now derive from the SAME
clipped truth as pixels (fully offscreen → no zone; partial → visible extent
only). PER-VIEW eligibility (PM concurrence): a pod drill scopes the graph
to that pod's containment subgraph, so nodes clipped at rig scale become
visible AND eligible in the drilled view and ineligible again when clipped —
always the current view's hit-zone truth, never a global filter. Selection
normalizes via the existing layout clamp (pinned for the resize class).
Pins: on-screen-intersection at 140+80, full-walk visible marker in plain
and truecolor with strip-invariant + matching Enter action, resize
normalization, per-view drill/zoom eligibility round trip. TUI 163/163,
front-door 14/14, lint, builds. Live proofs 23/24/25 at 80 cols.
Co-Authored-By: Claude Fable 5 <[email protected]>
HIGH-1 containment: layoutGraph is pod-clustered — podGroup nodes become
drawn pod containers (light box, ▾-name tab, header hit zone → pod drill)
inside a rig double-box container (tab zone → rig drill); agent boxes nest
inside their pods (the ║…│…│ nesting fingerprint is test-pinned on every
agent row); containers are unprotected backgrounds so edges route across
them while agent boxes stay opaque; under-route corridors treat pod
containers as obstacles and the rig keeps corridor slack rows. Nested hit
zones discriminate rig/pod/agent; keyboard reaches the same targets.
MR8 width honesty (spec pins 98c4a473/ab733e53 byte-gated before edit):
layout reports true extent; both renderers draw ' content clipped ▸ ' at the
right edge when the graph exceeds the viewport — indicator only, pinned
over-wide AND no-false-alarm.
HIGH-3 visible focus: the selected-zone › splice now patches the seg row too
(spliceMarkerIntoSegs) so truecolor stylization keeps the marker and the
strip-invariant on marker rows; pinned visible+truthful+action-identical.
TUI 159/159 · front-door 14/14 · lint · builds. Live proofs: 140x34
containment+indicator, truecolor marker, 80-col public-bin.
Co-Authored-By: Claude Fable 5 <[email protected]>
Live captures banked: genuine ○ on demo (orch1.lead detached beside 6 ●) and
implementation-pair (both seats detached); PIN-1 live pair (graph-node click
vs command drill, identical path) on v-openrig-build. ◐/✕ absent from the
live fleet — fixture-pinned honestly, never fabricated.
Co-Authored-By: Claude Fable 5 <[email protected]>
RED style-agnostic pin (corridor-crossing-intermediate-box, all three styles),
then renderBraille reordered to hatchet semantics: edge runs first, opaque
boxes clear pass-throughs, protected-aware blit + arrows last. 152/152.
Decision record re-trued: verdict unchanged (braille primary), claim now
structurally stronger.
Co-Authored-By: Claude Fable 5 <[email protected]>
Hybrid refinement: box-drawing for straight horizontal runs (mid-cell, flush
arrowheads), braille sub-cell ONLY for diagonals; boxes opaque+protected so
warping is impossible by construction. Default graphStyle=braille per the
founder's pre-ratified rule; residual TIER-2 portability caveat carried in
BRAILLE-DECISION-RECORD-2026-08-04 (routed via pm as a record, not a question).
TUI 151/151.
Co-Authored-By: Claude Fable 5 <[email protected]>
Atom 2 (INTAKE.md classification 2). operator.agent profile.uses.skills references
openrig-upgrade, but it was missing from specs/agents/shared/agent.yaml resources.skills,
so the kernel resource-pool containment gate refused it (kernel-rig-spec-validate.test.ts:
"operator.agent references skills not in shared pool: profile=default skill=openrig-upgrade").
Smallest coherent fix: register the skill in the shared pool at its existing on-disk path
specs/agents/shared/skills/core/openrig-upgrade/SKILL.md. RED reproduced first; +2 lines,
no unrelated pool churn. All three kernel variants (advisor.lead/operator.agent/queue.worker)
+ on-disk SKILL.md resolution: 11/11; broader spec/kernel/starter suites 173/173; daemon tsc 0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
review50-r1 honest-degraded money-leg QA on ac34ed75 found the packaged end-to-end
projection still lands 0644 at all four helper paths: PluginVendorService.ensureVendored
(assets -> ~/.openrig/plugins) is a SECOND readFile->writeFile hop UPSTREAM of the
already-fixed adapters, so the adapters faithfully preserved the staged copy s wrong 644.
The earlier driver spot-check projected repo assets directly, bypassing this hop.
Apply the identical preserveMode shape at the vendor site: optional statMode/chmod on
PluginVendorFs + a preserveMode() reconcile in ensureVendored AND ensureSkillGlobally, on
the fresh-write AND the content-identical hash-skip path (a previously-staged 644 repairs
on re-vendor). Wire statSync(.mode)/chmodSync into the production realFs in startup.ts.
Explicit auto-fetch decision documented (v0 writes no files; future extract must preserve
mode). RED-first real-fs pins (exec 0755 / non-exec 0644 / hash-skip repair) — RED showed
0o644 vs 0o755, then GREEN.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Both runtime adapters project directory-shaped plugin packages via text
readFile -> writeFile (writeFileSync utf-8), which creates dest files with the
process default mode and never reapplies the source mode. Nested executable
plugin helpers (claude-compaction-restore/scripts/*.mjs, 0755) therefore landed
as 0644 after real default-profile materialization, though byte-identical.
Add optional statMode/chmod fs primitives and a preserveMode() reconcile in both
Claude and Codex projection, on the fresh-write path AND the content-identical
idempotence-skip path (re-materialization of an already-present tree must still
fix mode). Wire statSync(.mode)/chmodSync into the production fsOps in startup.ts.
Optional primitives keep existing mock-fs callers unaffected. RED-first real-fs
pins: exec helper keeps 0755, non-exec neighbor stays 0644, both adapters.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Inside pod dev50 a served dev50.driver displays as 'driver' (nav-flow mockup
convention): only a confirmed `${pod}.` prefix strips; non-prefixed names
display unchanged; the full served identity stays in row key/action/selection/
detail. Locked full meta and guide geometry preserved. Pins: same-runtime+ctx
collision distinctness, honest non-prefixed fallback, selected-line visible
identity restored.
Co-Authored-By: Claude Fable 5 <[email protected]>
Meta is the locked form with the middle dot, including honest 'runtime · —';
the display name yields with … under the fixed pane width, never the meta.
The whole meta paints dim as one run. Pins: typical-name truncation,
null-context, short-name untruncated, extreme-name, whole-meta styling.
Display-truncation consequence (namespaced agents can render ambiguously at
depth 4 in the 30-col pane) flagged to guard/pm for the founder polish loop —
row identity remains exact in the row model, hit actions, and detail pane.
Co-Authored-By: Claude Fable 5 <[email protected]>
The no-flags OFF/no-profile floor is broken on the installed managed runtime: an
interactive Codex launch with required --add-dir writable roots and no -s flag is
rejected ("effective permissions do not allow additional writable roots") and falls
to shell before readiness (dev-qa firsthand, Codex 0.120.0). The clean-home codex
doctor default never exercised the --add-dir writable-roots interaction.
Centralize the fix in codexPostureArg (yolo-mode.ts), applied on ALL managed paths
(fresh / resume / native-fork):
- OFF, no profile -> EXPLICIT " -s workspace-write" floor flag (was empty)
- ON (YOLO) -> " -s danger-full-access" (was --dangerously-bypass-...-sandbox)
- OFF, named profile -> " -p <profile>" unchanged
Remove all "harness default" floor wording per PM amendment 1c; the floor is the
explicit workspace-write flag. RED-first: codex OFF/ON pins flipped across yolo-mode,
codex-runtime-adapter, codex-resume, native-resume-probe, node-inventory, and
resume-metadata suites. Source-only; no live host config touched.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Guard BLOCKING at 6e8f2e8c, all four findings closed:
1. bin-wrapper (the real public path) now owns bare-TTY invocations via the
re-exported runFrontDoor (feature-detected); compiled PTY pin via tmux +
an in-process forced-TTY belt.
2. spec: rows derive depth from the shipped label indent (6-space = foldered)
so an expanded folder's child renders one level deeper, never a sibling;
PIN-1 actions/keys pinned unchanged.
3. agent meta = runtime · ctx% (mockup's first-token display form) under an
IDENTITY-FIRST width policy: richest meta that fits beside the FULL name,
ctx-only fallback, name truncates only as last resort; pinned all three.
4. locked │ rails render; the pane border is located by its FIXED boundary
column in stylize and in the two floor call sites (guard-sanctioned
truthful floor update — assertions unchanged).
TUI 133/133, CLI front-door 14/14, full CLI green vs the 31 pre-existing,
lint, builds. Captures refreshed through the PUBLIC bin.
Co-Authored-By: Claude Fable 5 <[email protected]>
Navigator (mini-req 1): computeExplorerRows stays the ONE row model; the
renderer applies a display-only transform — ┊/├─/└─ branch guides, ⊕/▚ icons,
right-aligned meta (agent ctx% honest-—, pod counts), collapse glyphs only
where collapse exists (hosts/rigs lose the decorative ▾). Rail glyph is ┊
because │ is pane structure to the shipped floors. Selection-focus highlight
stays the existing render-only bar (arch ruling 1); drill/auto-expand
unchanged (ruling 2).
Front door (mini-req 7): bare rig with BOTH streams TTY probes the daemon and
launches the TUI (monorepo-first/bundled-fallback resolution, the daemon
resolver pattern); either stream piped → the unchanged commander usage path
(fast, no hang); daemon-down/init-fail → helpful usage naming 'rig up', never
a stack trace; args always fall through unchanged (byte-diff pinned).
TUI 131/131 (123 floors untouched + 8 re-skin pins), CLI front-door 10/10,
full CLI suite green vs the 31 pre-existing base failures, root lint, builds.
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard re-seal bounded corrections on 6581dbaf: (1) add a REAL PiResumeAdapter restore call-site
ON/OFF command pin (OFF -> --no-approve, ON/YOLO -> --approve) so pi-resume.ts cannot regress while
the pure piTrust helper stays green; (2) reword yolo-mode.ts shared-decision comment: the ON posture
is "maximally-permissive", not uniformly "full-bypass" — Claude/Codex = permission bypass, Pi =
RESOURCE TRUST (--approve), not a permission policy. Behavior unchanged. Daemon bounded suite green;
daemon+cli tsc 0. VM/source-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Guard NOT-CLEAR on 7f6b28f6: the "every seat" contract missed three managed paths the fresh-only
YOLO wiring did not exercise. Centralize the decision in yolo-mode.ts (claudePostureFlag /
codexPostureArg / piTrust) and route every fresh/resume/fork path through it:
- Claude restore (claude-resume.ts): was `claude --resume <token>` — now carries the posture flag
(the unconditional acceptEdits floor when OFF; --dangerously-skip-permissions when YOLO ON).
- Codex native fork (codex-runtime-adapter.ts fork branch): now codexPostureArg(profileArg) — OFF
keeps the profile/floor, ON forces --dangerously-bypass-approvals-and-sandbox. Fresh + resume
also routed through the same helper (behavior unchanged when OFF).
- Pi restore (pi-resume.ts) + launch: piTrust(configured) — ON forces `approve`, OFF the configured
posture. Pi's --approve/--no-approve govern RESOURCE TRUST, NOT a permission policy — wording
corrected in source comments/helper doc/test descriptions accordingly (PM/Leg-B framing). The
zero-permission-config-write property is scoped to Claude/Codex permission policy.
Test-first: yolo-mode.test.ts adds ON/OFF pins for Claude restore, Codex fork, and the
codexPostureArg / piTrust decisions; flipped the 3 claude-resume assertions (floor now present).
Daemon bounded suite 303/303, cli setup 24/24; daemon+cli tsc 0. VM/source-only; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Seam 4. Removed the ~/.claude/settings.json disclosure (the global allow-list C2 is deleted —
OpenRig no longer writes that file); made the settings.local.json purpose honest (statusLine +
the acceptEdits floor fragment; OpenRig bakes no allow/ask/deny policy). setup test flipped
(disclosure 6->5, settings.json entry absent). 24/24, cli tsc 0. HELD pre-guard-review.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Seam 3. New yolo-mode.ts yoloEnabled(env) reads the opt-in OPENRIG_YOLO setting (default OFF),
launch-flag surface only, zero config writes. Wired into all three harness launches: Claude
(--permission-mode acceptEdits floor / --dangerously-skip-permissions on YOLO), Codex fresh+resume
(harness-default floor / --dangerously-bypass-approvals-and-sandbox on YOLO, overrides -p profile),
Pi (this.trustPosture / approve->--approve on YOLO). Default OFF = floor unchanged. 209/209 across
8 suites, daemon tsc 0. HELD pre-guard-review (guard was at an interactive prompt).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Agnostic rip-out, seam 2 (Codex floor). Runtime-grounded: clean-home `codex doctor` (v0.144.4,
disposable CODEX_HOME) reports the no-config default as approval=OnRequest + filesystem sandbox
restricted + network restricted — i.e. Codex's own default IS the founder-selected workspace-only
floor. So OpenRig stops FORCING it.
- codex-runtime-adapter.ts (fresh launch) and native-resume-probe.ts buildCodexResumeCore (resume)
drop the forced `-a on-request -s danger-full-access`; Codex now launches with NO OpenRig sandbox/
approval flags = maximally agnostic ("setting nothing" = the floor). A named `-p` config profile
branch and the -C/--add-dir/-m workspace plumbing are unchanged.
- node-inventory.ts recovery guidance rewritten: no longer claims posture flags preserve approval/
sandbox; now states Codex uses its harness default unless a named config profile is present.
Tests: flipped 13 pins across 5 files (RED first — they pinned the forced flags — then GREEN), the
node-inventory summary assertion, and two misleading native-resume test names. 180/180 across the
codex+rip suites; daemon tsc 0. Guard-reviewed. VM/source-only (no live-host mutation); never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Founder-locked agnostic permission rip-out, seam 1. Rip-list = ASSESSMENT sha 5d450fdd. Base f81018fb.
OpenRig goes agnostic on permissions: it bakes NO config-file permission policy beyond the
usability floor. Two-surface rule — launch flags (stable) may be deterministic code; config-file
writes (chaotic) are removed.
- C1b/C1c: claude-settings.fragment.json stripped to ONLY permissions.defaultMode=acceptEdits
(+ enabledMcpjsonServers). The 13-entry allow-list and the rig up/down ask gates are gone.
- C2: provisionRigPermissions (the global Bash(rig:*) allow + _openrig_provenance writer into
~/.claude/settings.json) is DELETED — call, method, CONVENIENCE_BASELINE, and the now-dead
readStringArray. No retro-scrub: the new code never touches settings.json, so pre-existing
provenance-marked user files stay byte-identical. Trust/onboarding (C3/C4) plumbing kept.
- Floor kept byte-identical: fragment defaultMode acceptEdits + the --permission-mode acceptEdits
launch flag (unchanged).
Tests RED-first: agnostic-rip-out.test.ts (fragment has only defaultMode; fresh startup authors no
settings.json; no-retro-scrub byte-identical; launch-floor flag pin). Flipped the pins that
asserted the ripped rows (slice16-hygiene item 1, spec-library-starters, and removed the 7-test
provisionRigPermissions section with a rip-pointer). 201/201 across six suites; daemon tsc 0.
Guard-reviewed. VM-only, source-only (no live-host mutation); never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
QA c5b32f17: the 100-page-per-refresh budget was itself an arbitrary cap — a
10,001-item stream silently rendered item 10,000 with empty readErrors. The
walk now terminates ONLY on the served short/empty page; the sole other exit
is a non-progress guard (repeated/rewound cursor) that aborts with a NAMED
error. Cursor commits per page, so an abort or failed page keeps all reached
progress instead of blanking the tail. Pins: item 10,001 reached one past the
former boundary with readErrors empty; 25,050-item tail; non-progress abort =
2 calls, named error, progress kept, no hang. 118/118 + typecheck + build +
root lint.
Co-Authored-By: Claude Fable 5 <[email protected]>
QA blocker 02d3acde: /api/stream/list is chronological ASCENDING, so a fixed
limit returns the OLDEST rows once the stream outgrows it — the ticker froze
on item five forever. The hydrator now walks the served cursor contract
(afterSortKey pages of 100, instance-scoped cross-refresh cursor, rolling
5-item tail) so the newest item always reaches the snapshot; no fixed cap can
mask the tail again. Pins: QA's exact 6-item repro shape, 147-items-across-
pages, cursor persistence across refreshes (resume-after-key asserted), and
honest-empty/failed-page behavior. Live GREEN: QA's sixth item renders and a
freshly emitted seventh advances the ticker within one refresh (evidence 39).
115/115 vitest + typecheck + build + root lint.
Co-Authored-By: Claude Fable 5 <[email protected]>
Any padded cell, explorer label, or composed row that overflows its width now
ends in … (glance honesty — 'implementation-pai' reads as broken; 'implementa…'
reads as truncated). 111/111 + typecheck + build.
Co-Authored-By: Claude Fable 5 <[email protected]>
Focus-at-a-glance: only the focused pane's cursor carries the full accent
inverse; the other pane's bar drops to dim-inverse (k9s/editor standard).
Stylize reads focus from the bracketed pane title in the chrome — no second
source of truth. 110/110 + typecheck + build.
Co-Authored-By: Claude Fable 5 <[email protected]>
Remaining round-1 checklist items: the focused pane's title is bracketed in
the top rule (active-pane emphasis); content-pane selection renders as a real
inverse highlight bar instead of a bare glyph (strip-invariant intact); the
content spec-library page mirrors the explorer exactly — same folder grouping,
same expansion state, same filter-overrides-collapse rule. 109/109 vitest +
typecheck + build.
Co-Authored-By: Claude Fable 5 <[email protected]>
Founder direct-drive catch: a specs filter leaked into the topology table and
blanked it. Drills and cross-navs that change section now clear the filter;
same-section drills keep it. Three pins added.
Co-Authored-By: Claude Fable 5 <[email protected]>
Founder round-3 verdict items: (1) rig-spec topology tab renders the
established table treatment (NODE/LABEL/POD/RUNTIME aligned columns + edges
section) instead of unformatted rows; (2) navigation syncs the explorer
highlight onto the opened item and the cursor NEVER resets to top —
locationKey/syncSelection in the reducer, auto-expanding whatever level hides
the target (pods, spec folders); (3) SPECS defaults: rig specs fully listed,
agent specs collapsed to folder rows with counts (toggle-expand action, filter
overrides collapse so matches stay visible); (4) TOPOLOGY defaults: rigs+pods
visible, agents on pod expand — tighter visually. Stable row keys carry
identity. 103/103 vitest (8 new round-4 pins) + typecheck + build; touched
pages imaged and looked at per the visual addendum.
Co-Authored-By: Claude Fable 5 <[email protected]>
Founder round-2/3 directive: detail.ts is the shared vocabulary (field grid
with dim-label/bright-value columns at one fixed label width, titled section
rules, uniform (open ▸) affordance, listItem/alignedRow, one spacing rhythm —
the approved mockup's agent-spec frame recreated as the reusable primitive).
Applied everywhere text dumped: agent detail (seat/spec/actions sections),
agent spec (the mockup grid incl. wrapped skills), rig spec configuration
(per-pod sections, aligned member columns, cross-pod edges), NEW workflow-spec
page (roles/steps/status from the served library fields), overview (rig grid +
aligned pods), needs-you + hosts-down (fixed columns with in-row hierarchy:
amber kind · dim host · bright target · dim detail). Empty pods render
honestly instead of vanishing. Stylize gains the field-row + section-rule
rules; strip-invariant still pinned. Acceptance per the visual addendum:
every page rendered, imaged, and LOOKED at (before/after set banked).
95/95 vitest + typecheck + build.
Co-Authored-By: Claude Fable 5 <[email protected]>
Founder visual-identity directive (base 22cea911): theme.ts maps the LOCKED
mockup palette (teal accents #4dbdb2/#7bd3ca, amber #e6b56e, dim #6d7480,
bright on dark) to truecolor/256/16 with NO_COLOR/--no-color honesty;
stylize.ts is a segment-painted post-pass over the tested plain layer with the
strip-invariant pinned across every view and mode (color can never move a hit
target or shear a frame). Treatment: inverse-accent selection bar, semantic
STATUS colors (ok/warn/error/dim), teal links+acts+tabs, amber alerts, red
hosts-down, styled prompt/ticker/status. Plain-layer chrome per directive:
pane-titled box rules (EXPLORER ┬ <SECTION>) + contextual keybind hint bar.
Presentation only — resolver/mutation path untouched; scope fences unchanged.
95/95 vitest + typecheck + build + root lint.
Co-Authored-By: Claude Fable 5 <[email protected]>
ACTIONS = real drive-structure writes over the two existing contracts (web
parity): term ▸ → POST /api/terminal/open {view: pod:<rig>/<pod>}; run ▸ →
POST /api/up {sourceRef}, rendered ONLY for non-running seats (no per-seat run
contract exists — no false affordance). Acts execute in the driver loop and
never mutate view-state (PIN 1); outcome = verbatim daemon reply in a status
notice. Agent detail shows served tmuxAttachCommand. Specs detail binds the
LIVE /api/specs/library/:id/review route (the spec's /api/specs/review cite
404s at tip — QA-found); description/skills/guidance/startup/source rendered
verbatim, memoized by id@updatedAt. Rig lifecycleState surfaces: explorer
suffix + rig-down beside-group enriched by /api/rigs/:id/status. Renderer:
single-cell glyph fix (U+26D4 is emoji-width and sheared padded frames).
57 vitest + typecheck + build; live: term-click opened a real pod view,
spec detail + rigs-down rendered (captures 16-17); frames byte-stable
across refreshes.
Co-Authored-By: Claude Fable 5 <[email protected]>
hydrate.ts maps attention-aggregate/rigs-summary/nodes/specs-library/
rig-spec.json/review-rig into FleetSnapshot: STATUS = lifecycleState verbatim
(PIN 2, honest-unknown cells), Needs-You derived items verbatim (both stuck
legs stay distinct via served identity/threshold strings — no client-side
threshold exists to conflate), host-down composed BESIDE the items (hostsDown),
failed reads → named readErrors + honest-empty. Specs↔Topology join over
existing reads. main.ts: 5s hydrate loop. Fixtures mirror served shapes traced
at 5f3b5bd4. 35 vitest + typecheck + build GREEN; verified live against the VM
daemon (captures 09-13 in slice proof/).
Co-Authored-By: Claude Fable 5 <[email protected]>
18 node:test cases GREEN. Evidence + verdict in the slice folder's proof/
(SPIKE-VERDICT-2026-08-02-dev50-driver.md). Zero deps; outside the workspaces
so package gates are untouched.
Co-Authored-By: Claude Fable 5 <[email protected]>
Recovery honesty + starter bootstrap + skills wave + Slack connector + UI maintenance-mode.
Recovery honesty is the through-line: on a resumed restore, the startup
orchestrator now bundles the applicable after_ready send_text preload
action(s) in front of the first send_text post-launch file (role.md) and
delivers them as the single leading turn — sequencing (not timing)
guarantees the "load skills before doing anything" preload precedes the
role-triggered first turn (the restore analogue of
deliverInitialSessionPrompt's fresh identity+role.md bundle). Compaction
recovery and transcript ingest are honest — Claude transcript ingest is
fixed with explicit degraded signals; a post-compact restore/audit is
idle-gated so restore-sent actually means delivered. Consumers should
key seat liveness off lifecycleState (honest ~3s) rather than
sessionStatus (staleness cleanup tracked for a subsequent release).
Daemon rig ps and daemon-status report liveness honestly — a dead tmux
seat drops effective running to 0 with attention_required; send and
capture return session_missing when the seat is gone. CLI probes report
uncertainty honestly — unconfirmable status returns UNKNOWN with no
false start advice, and confirmed-stopped still FAILs with guidance.
Starter bootstrap: product-team starter bootstrap hygiene, plus
product-team send_text skill-preload on fresh_start and restore. Default
culture loads at startup, and rig specs are audited at load time. Rigs
instantiated BEFORE 0.4.7 need re-instantiation (or spec-level patching)
to pick up the starter fixes — the loader and audit changes apply
immediately, but starter-spec content lands at instantiation.
Skills wave: the bundled skill layer gains public routing + a default
projection, plus public-skill strip and mirror controls, plus a plugin
fix that keeps the documented skill count honest. The vendored skill
inventory (canonical, shared, and bundled plugin) grows from a handful
to broad coverage across core, PM, pod, and process families.
Slack connector + human queue: first-class connector shape (CLI
commands/slack.ts + supporting library), with hosted create crossing an
inconclusive local probe to the real configured-daemon result.
CLI contract honesty: json errors, flag validation, and scoped overdue
behavior tightened for machine-readable use.
UI: the web UI is frozen at this release in maintenance-mode
(CLI-primary wording — never "deprecated"), surfaced as a dismissible
UI banner plus a rig ui open stderr notice plus documentation
positioning.
Queue, topology, review polish: queue compact-list rows mark elided
fields so omitted is not empty; nested Approve posts a
missions-root-relative scopePath; mission review card composition is
polished; unverified delivered items read artifact-recorded rather than
"nothing delivered"; drawer FileViewer resolves inline C1-body images
via /api/files/asset; proof-of-work markdown links open in the in-app
drawer; docs guard encodes the ratified docs/DESIGN.md root placement.
Host sizing guidance: add swap and a sane per-box seat budget
(~2GB RSS per seat observed baseline).
Formal CHANGELOG entry and docs/releases/v0.4.7.md follow as post-cut
release documentation.
On a resumed restore the work-triggering guidance/role.md was delivered as a
post-launch send_text file (step 7) and started the seat's first turn at once;
the after_ready "BEFORE you do anything else, load skills" preload was delivered
later (step 9), so it landed AFTER non-skill work had already begun (QA's real
automatic Codex dual-trigger witness: role.md 23:05:22 -> tool call incl.
`rig whoami` 23:05:38 -> action 23:05:39). This broke the locked
action-before-work contract.
Causal fix (NOT a delay change): on restore, bundle the applicable after_ready
send_text preload action(s) in front of the first send_text post-launch file
(role.md) and deliver them as the single leading turn — the restore analogue of
deliverInitialSessionPrompt's fresh identity+role.md bundle. Sequencing, not
timing, guarantees the preload precedes the role-triggered work turn; bundled
actions are marked consumed so the after_ready phase does not re-send them (no
duplicate).
- startup-orchestrator: restore-preload bundling branch + deliverRestorePreloadPrompt;
executeActions gains a skip set for consumed preload actions
- test: RED-first pin asserting (shape-agnostically) the preload is delivered no
later than the role.md work-trigger, precedes role content when bundled, and is
not duplicated. Fresh path unchanged.
QA-BLOCKER qitem-20260802225900-ecfa4638. Correction qitem-20260802231132-e99624f7.
Contract unchanged; RED-first; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Durable fix for the copied-skills-don't-trigger audit item (Option A, config-only,
Guard-accepted plan r2 45bdbdca). Bootstrapped seats had their process skills on
disk but never INVOKED — the conversation-start trigger doesn't fire on --resume
(Codex resume re-sends only the resume command; same gap on Claude), so a TDD
factory freelanced. One runtime-uniform send_text closes both sides.
Change (config + wording, NO daemon code, NO hooks, NO demo-box touch):
- Each of the FIVE product-team AgentSpecs (orchestrator, implementer, qa,
product-designer, independent-reviewer) gains a startup.actions send_text entry:
type send_text, phase after_ready, applies_on: [fresh_start, restore] (snake_case;
the bug lives on resume), idempotent: true (else validation errors on restore /
the orchestrator skips it). The value imperatively tells the seat to load+invoke
its OWN process skills using its RUNTIME'S supported mechanism — runtime-neutral,
no hardcoded "Skill tool" (Codex has none); each payload names that seat's real
process-skill set from its own skills list (qa/product-designer differ from
implementer = the built-in two-differently-skilled-seats proof).
- openrig-start.md: passive skills line → imperative "load AND invoke ... before
proceeding, using your runtime's supported skill mechanism."
Proof: spec-contract regression (5 specs, raw snake_case fields + per-seat skills
named + clean validateStartupBlock + camelCase-false-green pin + differently-skilled
seats); carried startup-orchestrator both-sides coverage; compaction/restore
regressions 145/145; spec-library-starters 25/25; daemon typecheck/build + root lint
clean. The observed-invocation bar (RED-first, phantom-green-aware: send-ok is NOT
proof — QA observes actual skill invocation in transcript/JSONL on fresh AND restore
for Claude AND Codex) is QA's firsthand matrix.
Upgrade boundary (orch Option A): fixes newly-instantiated product-team rigs and
restores OF those; pre-change persisted rigs do NOT gain it on resume (RestoreOrch
replays captured startup_actions_json, not a re-resolved spec) — NO migration added,
per ruling. Built over main 25bdf742; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Founder-flagged flagship quality (OPR.0.4.7.16) — three fork-free fixes from
dev-driver3's YC-demo-box audit (items 2-4; item 1 copied-skills-don't-trigger is
a separate arch track, NOT folded here). The product-team starter is the rig we
hand strangers; it must work out of the box. Durable fix in the openrig repo;
demo box ovh-usw-01 left untouched.
1 (permissions) — agents/shared/runtime/claude-settings.fragment.json shipped only
Bash(rig:*), stalling git/npm/tsc/node on every dev command. Now allows the TDD
toolchain (git/npm/npx/pnpm/yarn/node/tsc/vitest/jest + Read/Edit/Write) on top
of acceptEdits; rig up/down stay behind the ask gate. The spec-library-starters
security-gate assertion is updated to this authorized set (further widening must
be conscious).
2 (culture seat-id staleness) — rig spec audit now flags stale seat ids: a
backtick `pod.member` reference in the culture whose pod exists but whose full
seat id is not a current seat in rig.yaml (a rename left the old id) → advisory
finding stale_culture_seat_id, so no stale id reaches a materialized
AGENTS.md/CLAUDE.md unflagged. File-path/version tokens are not false-positives.
3 (QA carries the skill it enforces) — agents/development/qa/agent.yaml + its role
guidance now include test-driven-development (the RED/GREEN guard loads TDD).
4 — regression tests: fragment content + qa-skill assignment (daemon
slice16-starter-hygiene) + rename-consistency (cli rig.test.ts spec-audit).
Verified: slice-16 CLI rig 8/8 + daemon hygiene 2/2 + spec-library-starters 25/25;
daemon+cli typecheck/build + root lint clean. Firsthand: rig spec audit flags a
renamed seat's stale culture id; clean when consistent. Built over main bd10fee6;
demo box hands-off; never pushed. (Fresh-bootstrap toolchain-runs-unprompted proof
is QA's firsthand matrix.)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Founder-directed 0.4.7 cut (OPR.0.4.7.15). One honesty contract: the CLI must
fail machine-readably under --json, reject what it doesn't support instead of
accepting-then-ignoring, and never emit unscoped cross-rig data.
1 (systemic anchor) — shared error/exit path (packages/cli/src/cli-error.ts:
runProgram): validation/usage errors (missing required option, unknown option,
invalid option argument) now emit {ok:false,error:{code,message}} on stdout with
a nonzero exit when the invocation asked for JSON (--json / -o json); plain-text
stderr preserved otherwise; --help/--version pass through. Both run-sites
(bin-wrapper + index direct-run) route through it. One class-fix, not per-command.
3 — queue list -o validated (enumArg["json"]); unknown formats rejected via the
shared path. (rig ps already rejects unknown -o — dispatch premise corrected.)
4 — queue list --limit validated (positiveIntArg): negative/zero/non-numeric
rejected. (rig ps already validated its --limit.)
5 — rig ps --active/--running: bare LOCAL rig-tier use (no --nodes) now fails
loudly (rig rows carry no per-node activity) instead of a silent no-op; help
text states the node-only scope. Remote (--host/--all-hosts/--hosts) paths
forward --active to the remote and are exempt from the local gate.
2 — queue overdue scoped + bounded + body-free by default: CLI gains --rig / -A /
--full / validated --limit (default 50, compact); daemon findOverdue({rig,limit,
compact}) + /overdue route mirror `list` (shared COMPACT_QUEUE_COLUMNS const;
no-arg findOverdue unchanged for the watchdog).
6 — regression tests: 14 CLI (slice15-cli-contract.test.ts) + 4 daemon findOverdue.
Verified: slice-15 CLI 14/14 + daemon findOverdue 4/4; adjacent queue/ps CLI
123/123, queue-list-compact/routes/deadline 52; daemon+cli typecheck/build + root
lint clean; full CLI suite = only the 4 PRE-EXISTING baseline failure files
(ps-compact/ps-pl012/ps-pl019/up), identical on clean base — zero new regressions.
Firsthand: JSON error objects emitted through the real binary across queue/send/
scope-audit under --json. Built over then-current main 5f3b5bd4; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Slices 13–14 QA blocker: `rig compact` surfaced compact-sent → restore-sent →
audit-sent, but the restore prompt was never enqueued or delivered. Root cause:
the three post-compact back-half sends (turn_boundary → restore → compliance)
called SessionTransport.send with NO options. Right after /compact, Claude is
busy auto-compacting; the restore was injected into the busy pane on the next
poll tick and dropped — yet send returns ok (busy is a non-blocking advisory on
the default path), so the stage advanced to restore-sent on an undelivered send.
The audit landed later and is what actually triggered Claude's reconstruction.
Fix (minimal, existing-shape): each back-half send is now idle-gated with a small
bounded `waitForIdleMs` (POST_COMPACT_SEND_WAIT_MS_DEFAULT = 10s, below the ~30s
ContextMonitor poll interval — the loop is sequential, so a longer block would
stall other seats). waitForIdle delivers only once the prior turn is idle; on a
busy timeout it returns not-ok, the stage does NOT advance, and the SAME stage is
retried on the next poll tick. So restore-sent is backed by one durable
normal-user restore delivery, and audit can never overtake restore. No new
scheduler; reuses the exact seam /compact already uses.
Added regression test (RED→GREEN): a busy tick retries the same stage without
advancing; a later idle tick advances once; restore is delivered before audit.
Existing back-half send assertions updated to the idle-gated 3-arg shape.
Verified: enforcer 39/39, compaction-routes + context-monitor + seat-scoped
adjacent 70/70 total, daemon typecheck + emit build clean, root lint (daemon/ui/
cli) clean. Corrected candidate over ca1a14e5 (base 5f3b5bd4); shared main
unmoved; never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Founder-ruled 0.4.7 fix. `rig queue list --json` (compact, intentional
projection) emitted body:'' + summary:null + evidenceRef:null — byte-
indistinguishable from a genuinely empty dispatch, so a consumer could not
tell not-loaded from author-sent-nothing (real assigned work rendered as an
empty-looking row).
Fix (additive, compact-only): compact list rows carry an optional
`fieldsElided: ["body","summary","evidenceRef"]` marker so omission is
structurally detectable. QueueItem.fieldsElided is OPTIONAL — existing
stable-field consumers are unaffected; --full list and `show` return complete
rows and never carry the marker. No CLI/UI change, no payload growth on the
full paths.
Guard-accepted candidate at base 151774d2 (exactly 2 files). Verified:
daemon queue-list-compact 9/9, queue-list-grammar 9/9, queue-routes 33/33;
CLI queue 77/77; daemon typecheck + emit build clean; root lint (daemon/ui/cli)
clean. Never pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Productize the two SHA-verified trusted-host reference connectors as the durable
source implementation + agent-assisted `rig slack` setup/config CLI. Connector
accesses the fleet ONLY via `rig queue`; secrets from a 0600 env file /
OPENRIG_SLACK_* env at call time (never in config/repo). Not a daemon worker
(preserves the item-10 secret-host axis: connector-host may differ from queue host).
Modules (packages/cli/src/slack/): state-store (durable seen + interruption-safe
dead-letter), message (Block-Kit + text fallback + secret redaction), slack-api
(fetch-based, timeout-bounded: fail-visible webhook, GRANTED-scope-from-header,
membership, Socket Mode open), queue-bridge (rig-queue runner + pure filter,
-A all-rigs + high --limit), outbound (poll→post→seen-after + enable-seed),
inbound (Socket Mode router: fast-ack, human-only + non-text ignore, dead-letter,
in-flight dedup), config (first-class), secrets (0600 + perm hygiene).
commands/slack.ts: setup|status|verify|enable|disable|outbound|inbound.
Incorporates the QA-BLOCKER corrections (QA of the prior build, SHA 3c77dbd6):
B1 inbound dead-letters retry periodically WHILE the socket stays connected
(not only on reconnect) — proven by a real runInboundLoop transport test.
B2 interruption-safe dead-letter: non-destructive readAll + atomic temp+rename
replaceAll — no truncate-before-success loss window.
B3 remote queue targeting via OPENRIG_URL (queueUrl), supported by ALL verbs,
instead of `queue list/show --host` (which those verbs reject).
B4 normal OPENRIG_SLACK_* aliases resolve (OPENRIG_+name, not doubled SLACK_SLACK_).
B5 enable-time backlog seeding passes an explicit high --limit (complete >100 set).
Plus a real fake-Socket-Mode transport test (runInboundLoop / open / onmessage /
ack path), closing the prior proof gap.
All 10 locked items + T1076 proven firsthand on a disposable daemon w/ fake Slack.
52 slack unit tests GREEN; CLI typechecks clean; only full-suite failures
(ps-compact/ps-pl012/ps-pl019/up) are PRE-EXISTING on clean 1a8e56ed — zero
regressions. VM-only; never pushed. Supersedes prior candidate dc060c2d.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Isolated candidate (detached, off base 1a8e56ed). Items 4-6 only; items 1-3
untouched. Four source files, four frozen RED/preserve test files.
D4 (item 4) — daemon-status false STOPPED while daemon healthy:
- daemon-lifecycle.ts: the whole status /healthz probe now lives inside ONE
~5s deadline (STATUS_PROBE_DEADLINE_MS). Immediate connection errors (the
post-restart bind window) may retry INSIDE the deadline; a probe timeout
means the deadline is spent and is never re-allocated per retry. A
slow-but-answering /healthz (~400ms) is observed rather than falsely
reported stopped; genuine refusal still fails within the window. The
single-shot callers (checkPid + start/stop guards + start poll loop) keep
their prior fixed 250ms per-probe timeout (HEALTHZ_PROBE_TIMEOUT_MS).
- doctor.ts: checkPort receives BOTH configured host and port; healthz +
cmux probes target the configured host:port, not hardcoded 127.0.0.1:7433.
D5 (item 5) — a dead disposable seat still read running after tmux teardown:
- ps-projection.ts: one isEffectivelyRunning predicate drives runningCount,
status, and the activeCount gate. It excludes ONLY seats with an applicable
session_missing identity verdict; pane_pid_gone / mismatch / tmux_unavailable
and stale/absent verdicts stay running (fail-open on ambiguity). The running
BASE stays the created_at-ordered SQL running_count (the locked
newest-session rollup); we subtract only the status-running-but-not-
effectively-running seats. No sessions.status mutation.
D6 (item 6) — a seat whose send AND capture both return session_missing is
never concurrently reported running:
- session-transport.ts: send() and capture(), on hasSession()===false, write
the SAME applicable session_missing verdict the reconciler would (via
SeatIdentityStore), so ps stops fabricating the dead seat as running without
waiting for the next reconciler poll. Only written when a binding pane exists
(null pane = tmux_unavailable, non-down-ranking).
Verified in this isolated worktree: 168/168 across the four frozen files
(CLI 103, daemon 65). Frozen test hashes unchanged from the accepted RED
freeze. Both packages typecheck + build clean. Known-unrelated baseline
getting-started narrative-parity failures are pre-existing and untouched by
these four files.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Stage-3 Lever C (Stage-1): the composer already returns plan.concise.text === null
iff the mini-requirements are not authored. The SliceReviewTab plan section now
names that non-compliance honestly — a static, non-interactive attention marker
(testid plan-mini-reqs-noncompliant, copy "Not compliant · mini-requirements not
authored", the existing amber attention styling) — in place of the terse
"— not planned yet" dash, in the SAME text===null branch.
Presentation-only: renders in every phase (no phase gate), suppresses no sibling
(media / locked set / plan-lock all render as before), adds no state/fetch/control/
abstraction, and changes no composer or data contract. "Visible does not block."
Tests (flagship-review-polish.test.tsx): the marker renders for post-intent and
intent-phase null-text (the no-phase-gate pin), stays absent for authored text
(Markdown renders), and — with media + locked siblings — renders alongside both
while the marker element itself carries no interactive descendants (scoped to the
marker; locked-set EvidenceOpeners remain valid controls).
Carried caveat (pre-existing, unrelated, NOT touched): the mission-status-reconcile
"named RED at base" failure (partitionProjectMissions bucketing) is independent of
this review-plan-section change.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Stage-3 Lever A (REV4): a slice SPEC approval now derives and co-serializes a
`locked-artifacts` frontmatter snapshot — the pinned plan set the Review surface
and audit read — in the SAME writeFrontmatterFields/writeFileSync as the approval
stamp. Mission/delivery approvals never create it; a delivery merge preserves an
existing list; the existing audit-failure path restores the verbatim original
README (no half-stamp, no orphan key).
New pure derivePlanLockArtifacts(readme, prd) (plan-lock-artifacts.ts): always
pins the Implementation PRD spec entry, then the selected proof-contract
plannedRefs (extractProofContractSelected — an authored README wins over a
pristine-scaffold PRD), then `## Intent visual` media (suppressed when N/A). Every
path is slice-relative: URI-scheme refs (http/https/data/...) are rejected up
front, absolute `/...` and `../`-escape via sliceRelativeMediaPath, deduped by
normalized path (first source wins), stable order (PRD -> plannedRefs -> intent).
Reuses the exported Review helpers directly (no shared extraction); no
route/CLI/reader/types change. scope-approve.ts adds a tiny tryReadPRD that fails
open to null on any missing/unreadable PRD.
Tests (scope-approve.test.ts): the five acceptance behaviors + gate pins are
asserted through the shipped ScopeApproveService + gather/compose (exact ordered
arrays; PRD-only/missing/unreadable singletons; README-wins parity;
escape/absolute/URL-scheme rejection + normalized first-wins dedup + N/A
suppression + determinism), plus mission/delivery no-create, delivery-preserve
(exact multi-entry list), and slice-spec audit-failure verbatim restore. The 9
pre-existing ScopeApprove tests are preserved.
Pre-existing unrelated caveat (NOT touched by this change): the two
getting-started-narrative-parity slice-21 CLI/daemon PROOF.md byte-parity test
failures are pre-existing and independent of this scope-domain change (the failing
test has no scope-approve/plan-lock coupling).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
PM firsthand HALF-PASS (qitem-20260723005752-5ee2eea4) on the retained lane: the
proof-directory browser (DELIVERED "see all proof" -> EvidenceOpener('proof/') ->
ArtifactsNavigator) listed guard.md/qa.md/rev1-*.md, but clicking an individual
proof file via the row or the tree item rendered no C1 content. Root cause
(read-only forensic + a11y evidence): the only clickable element per file was the
nested FileLink filename button; the right-pane row wrapper (badge/size/mtime) and
the tree-item indent were inert, so a click off the filename glyphs did nothing.
QA-local passed because it clicked the exact filename button; the DrawerSelection
provider chain, a folder drawer viewer, and the allowlist/read path were all ruled
out (QA read guard.md 200 on the same candidate).
Fix (ArtifactsNavigator.tsx only, reusing the existing FileLink / FileReferenceTrigger
/ drawer mechanism): the whole row is now one FileLink hitbox. Right pane: badge,
filename, size, separator, and mtime sit inside a single FileLink `group` button
(flex w-full), with group-hover:underline on the filename span so only the filename
underlines on hover (the pre-fix per-cell decoration is preserved). Tree: the depth
indentation moves off the inert li onto the full-width FileLink button. All
testids/data text/order/per-cell styles, the exact root/path construction, the lazy
list/read behavior, and the FileLink/drawer/FileViewer mechanisms are unchanged; no
li handlers, no duplicate selection logic, no nested interactive elements, no new
abstraction.
Tests (artifacts-navigator.test.tsx): R-right clicks a real inert visible row child
(the size cell) and R-tree pins the hitbox structure (the li owns no indentation
while the tree button owns the depth indent + full width), each paired with a
working nested-filename-button GREEN control; the shared assertion parses exactly
one /api/files/read (root + exact path) and exactly one file-viewer with five C1
labels+values plus the distinctive body.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
FOUNDER-fix blocker (qitem-20260723002125-9e4526a0): firsthand QA on the retained
lane caught a proof-file drawer body image (proof/qa.md ``)
resolving to a broken SPA-route-relative URL. The drawer's FileViewer Markdown
branch rendered <MarkdownViewer content={content}/> with no assetBasePath, so
relative body images (siblings of the file) fell through to the SPA route instead
of the canonical /api/files/asset. Exposed by the sibling founder fix (44ef16c1)
that first routed proof .md artifacts through the drawer.
Fix (FileViewer.tsx only): derive the asset base from the RESOLVED read target —
parentDir(target.path) (pure slash semantics; a root-level file anchors at ".") —
and pass fileAssetUrl(target.root, parent) as MarkdownViewer's assetBasePath in the
Markdown branch. Inline-only callers (no resolved target) keep an undefined base
(MarkdownViewer passthrough). Display path, root/readPath resolution, fetch, other
kinds, the parser, callers, and containment are unchanged; this is the same
canonical assetBasePath + fileAssetUrl mechanism ProofTab uses for inline PROOF.md
images.
Tests (drawer-primitives.test.tsx, real FileViewer + real MarkdownViewer): RED-1
pins the inline image resolving via /api/files/asset using the dirname of the
resolved target.path (not the display path), alongside the five-field C1 header +
body; RED-2 pins a root-level file anchoring the parent at "." (never a leading
slash); a passthrough pin keeps http(s)/data:/leading-slash srcs verbatim.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
FOUNDER UX fix (qitem-20260722234754-e8db7111): a proof-of-work file link in
the PROOF tab navigated the whole browser out of the SPA to the raw
/api/files/asset for guard/qa/rev1 .md verdicts. It now opens IN-APP in the
SharedDetailDrawer — rendering the C1 proof header (slice / candidate_sha /
artifact_type / verdict / money_evidence) + body — and closes cleanly back to
the review surface.
Fix (ProofTab.tsx only): the proof/ otherFiles list routes Markdown artifacts
(.md/.mdx, case-insensitive) through the existing FileLink primitive
(root=resolved.rootName, display path proof/<name>, readPath=<proofRel>/<name>)
instead of an <a target="_blank"> raw-asset anchor. Non-Markdown other files
(logs/video/binary) keep the browser-viewable raw-asset link; the image
gallery/lightbox, inline PROOF.md asset resolution, and lazy proof/ listing are
unchanged. The DELIVERED "see all proof" drill-in already opens in-drawer via
EvidenceOpener -> ArtifactsNavigator -> FileLink (no change needed there).
Tests (frozen pre-fix): proof-tab.test.tsx FX-1..FX-4 pin the in-app control,
the lazy slice-relative /read, the five-field C1 header + body, and
close-in-place with location/history unchanged; FX-5 preserves images/lightbox +
inline PROOF.md. artifacts-navigator.test.tsx pins the real delivered-see-all ->
EvidenceOpener('proof/') drill-in opening a C1 proof file in the drawer.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
REV6 render-honesty defect (qitem-20260722121455-3f43040c): a delivered item with
verified=unverified and no media (proof=[]) rendered "— nothing delivered for this
item" directly above its own "QA note: ..." — the media-absent fallback treated
every non-verified row as nothing-delivered. Per composeDelivered, unverified means
an artifact was delivered (covering.length>0) with no PASSING recorded comparison,
so the label was false.
Fix (SliceReviewTab.tsx only, two semantic changes):
- The unverified badge now reads "no PASSING QA comparison" (a non-passing
comparison may exist and produce the note), not "no recorded QA comparison".
- The no-media branch now reads: verified -> "verified by artifact; no media
attached"; unverified -> "artifact recorded; no media attached"; and reserves
"— nothing delivered for this item" for the missing state only.
proof.length>0 media rendering, the QA-note rendering, types, composer, and the
daemon are unchanged.
Regression (flagship-review-polish.test.tsx): unverified with and without a note
renders artifact-recorded/no-media and never "nothing delivered", keeps the QA
note, and the badge says "no PASSING QA comparison"; missing is pinned as the sole
nothing-delivered branch; verified-no-media is preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
REV6 control defect (qitem-20260722114922-2ea5442e): the slice Review NEEDS YOU
"confirm this proof is faithful" Evidence control opened HTTP 404 on an existing
PROOF.md because composeSliceReview emitted the derived confirm-faithful
evidenceRef as the full mission-relative <mission>/slices/<slice>/PROOF.md, while
EvidenceOpener — like every other proof ref — joins the current slice-directory
relPath, duplicating the slice path.
Fix (producer-side, one line): the confirm-faithful evidenceRef is now the
slice-relative "PROOF.md", matching the slice-relative evidence-ref contract
EvidenceOpener consumes (e.g. proof/qa.md). where: sliceRef stays mission-relative
(the mission Link target); raw queue-item evidence refs, EvidenceOpener, and the
slice-boundary containment are unchanged.
Regression: review-composer.test.ts pins cf.evidenceRef === "PROOF.md";
review-actions-and-evidence-containment.test.tsx pins that the opener joins a
slice-relative ref to the canonical slice path exactly once (never duplicated),
that ordinary proof/qa.md is preserved, and that absolute / parent-traversal refs
remain refused.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
REV6 control defect (qitem-20260722111916-e2575404): the mission Board row and
slice NEEDS YOU Approve controls posted a bare slice name to /api/scope/approve,
which resolves missions-root-relative <mission>/slices/<slice> — so a nested
slice 404'd ("not a declared slice") with no approval stamp/audit row.
Fix (UI only; daemon scope-approve contract unchanged):
- review-actions.ts: one shared pure derivation
sliceScopePath(missionId, slice) = missionId ? `${missionId}/slices/${slice}`
: slice. Single owner so the two callers cannot drift. approveSlice's first
param renamed slice -> scopePath (forwarded verbatim).
- MissionReviewTab (board): approveSlice(sliceScopePath(d.missionId, d.slice)).
- SliceReviewTab -> NeedsYouAccordion: threads data.missionId; approve composes
via sliceScopePath(missionId, slice); the bare slice is kept for display only.
- RigAgentsPage passes missionId={null} (rig scope, approve hidden).
- A bare name remains the legacy root-slice fallback (missionId null). The
absolute SliceDetail.slicePath is never used (host-path coupling / contract
violation, not a 404).
- Honest failure / no partial mutation preserved (post() unchanged).
Regression: review-approve-scope-path.test.tsx pins the exact POST scopePath for
the Board and NeedsYou nested controls, the legacy bare fallback for both, and
asserts the absolute slicePath is never sent.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
`rig queue handoff --body -` silently persisted the literal "-" as the qitem
body: a durable coordination-ledger verdict was lost, surviving only because
it was also in a scratchpad. Source verification found the same defect on
four write verbs -- handoff, handoff-and-complete, inbox-drop, outbox-record.
All four sent `opts.body` raw and never routed through resolveQueueBody, the
helper create already uses, so none consumed stdin on `--body -`, none
accepted `--body-file`, and none rejected a lone dash.
Route all four through the shipped resolveQueueBody -- reuse, not a
reimplementation. Each now accepts `--body <text>` (stdin-aware via `-`) and
`--body-file <path>` (stdin via `-`), mutually exclusive, with help that
documents both. The handoff pair resolves only when a body source is
supplied, so passing neither preserves today's source-body default (POST body
undefined); inbox-drop and outbox-record drop their `requiredOption("--body")`
for a plain option and always resolve, so neither and both reject before the
daemon is contacted. Exact bytes reach the POST on every file and stdin path.
The resolver's four error consequences said "rig queue create did not run" --
false once shared by the other verbs; they now read "The queue command did not
run", and the missing-body action drops the create-specific "qitem" wording.
The 3-part fact/consequence/action structure is unchanged.
No new seam, helper, or file: the existing lexical defaultStdinReader and
emitBodyResolveError are reused. B (create --host) and C (3aef8b2f) remain
source-verify-first and untouched.
Tests: queue.test.ts pins all eight file/stdin body transports as exact-byte
round-trips, the four help surfaces, the neither-contract split (handoff pair
undefined vs inbox/outbox reject), the both-source rejection, and all four
generalized error consequences plus the generic missing-body action. The
command-level stdin tests swap process.stdin for a PassThrough and restore the
descriptor and exit code in finally; the technique is calibrated against
create first.
Slice: release-0.4.7 / OPR.0.4.7.8 queue-body-input-parity (atomic A0+A)
Gate: qitem-20260721091854-e513c074 (Guard) / source verdict 7df65b46
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Slice-04 /api/ps event-loop stall. Three-layer fix:
- UI event-invalidation coalescing: useGlobalEvents is now the sole
150ms-debounced owner of ps + default-summary invalidations; useActivityFeed
no longer fires them per-event. This removes the SSE-replay-driven ps/summary
refetch storm (one ps + one default-summary per 150ms window), while rig
graph/nodes/sessions, discovery, attention, queue, package and the 100-event
feed cap stay direct/unchanged.
- Fetch cancellation: usePsEntries + useRigSummary forward the TanStack
AbortSignal to fetch, so superseded/cancelled fetches abort instead of piling
up.
- Returned-rig scoped 1+1 inventory fold: /api/rigs/summary uses the new
getNodeInventoryForRigs scoped to the rigs it actually returns (default
active-only; archived variants exact), sharing one batched builder with
getNodeInventoryForAllRigs — exactly one fleet startup-orientation and one
restore-outcome scan, no per-rig re-scan, live per request, no cache. The
per-node fold no longer widens to archived rigs.
- Shared setImmediate projection lane: /api/ps and /api/rigs/summary run their
whole compute + c.json as one job on a single process-local FIFO lane that
yields the event loop (native setImmediate) before each job, so a concurrent
burst keeps /healthz within the shipped 250ms responsiveness budget
(attempt-4: worst /healthz < 250ms vs pre-fix ~24s). No cache/worker/config,
no server-side cancellation; other routes untouched.
Regression pins added: D1 (summary 1+1 scans), D2 (N=24 real-localhost-child
burst keeps worst /healthz < 250ms), D3 (ps + summary archive membership
10/27/17 + lifecycleState + request-boundary freshness + fail-loud 500), U1 (SSE
burst coalesces to one ps + one default-summary), U2 (AbortSignal
forwarded/honored), U3 (agent.activity causes no ps/summary invalidation).
Caveats: two pre-existing, unrelated full-suite failures remain out of scope
(getting-started-narrative-parity scaffold byte-parity drift;
mission-status-reconcile "named RED at base"). The D2 acceptance asserts worst
/healthz < 250ms but the exact worst-ms is not emitted on PASS — terminal QA/PM
must record the actual numeric maximum under the equivalent burst.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The docs-path guard blocked docs/DESIGN.md, so `npm run test:repo` could not
pass on a clean tree. All 18 tests passed; the non-zero exit was entirely this
guard. A gate that is permanently red is worse than no gate, because every
lane learns to wave it through -- and this one runs at the root of all of them.
The guard was not wrong to exist and the file is not misplaced. Root placement
is ratified doctrine: docs/as-built/ui/library-specs-and-design-system.md §4
says "Q1 is ratified: DESIGN.md stays at root, byte-identical", explicitly
rejecting docs/as-built/. The guard simply never encoded that decision.
Nor could it be satisfied by relocating. Its own remedy text says to move
durable docs into one of the three allowed roots, and all three are wrong here:
docs/as-built/ requires the last-verified-against-source contract that a
non-source-derived brand spec cannot honestly carry (and Q1 rules it out by
name); docs/reference/ is staged into the published package and materialized
to $OPENRIG_HOME/reference/, so moving there would start shipping the design
spec to every operator -- a distribution change disguised as a lint fix; and
it is not a release note. The repo has a legitimate doc class with no allowed
home, and this file is its only member.
So it is named as an exact path rather than relocated, and the directory
allowlist is untouched. Exact-path matters: the policy being enforced is
"loose plans and notes stay untracked", and a directory hole would quietly
readmit exactly that. A sibling docs/OTHER.md is still blocked, and a test
pins that invariant so a future widening cannot pass unnoticed.
Tests: the allowance and the real script's exit code are both pinned, the
latter against the actual repository rather than a mock. The integration
harness uses process.execPath and fileURLToPath rather than ambient "node"
and URL.pathname -- pathname stays percent-encoded, so the naive form would
pass here and fail on any checkout whose path contains a space.
Slice: release-0.4.7 release-readiness
Gate: qitem-20260720110034-4a1e9e9b (Guard) / qitem-20260720105744-572bc477
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The conventions doc reaches readers by three paths, and the shipped teaching
surfaces only ever named one of them:
repo source docs/reference/sdlc-conventions.md
packed internal daemon/docs/reference/sdlc-conventions.md
installed stable $OPENRIG_HOME/reference/sdlc-conventions.md
Every pointer named the repo path. That is correct for someone reading the
repo and wrong for an agent on an installed package, where the file lives
where the daemon materializes it at startup -- it copies the bundled docs to
$OPENRIG_HOME/reference/ precisely "so agents can find them at a stable path"
(packages/daemon/src/startup.ts). So the guidance sent installed agents to a
path that does not exist on their machine.
Fixed by teaching both contexts rather than by moving files. The packed path
stays internal assembly input and is never taught: it is the resolver's input,
not a user path, and scripts/build-package.sh stages it there for exactly that
reason. The default ~/.openrig is shown only parenthetically -- naming
$OPENRIG_HOME is what stays correct for operators running a custom home.
Repo-only surfaces (source comments, the scaffold test's SSOT_POINTER
constant, filesystem reads of the repo file) keep the repo path, because they
are correct there.
Also trims the mission-slice-sop description from 701 to 392 UTF-8 bytes. A
skill description is a retrieval surface competing for budget with every other
skill; this one was the outlier. The trim is guarded by a test pinning both
the budget and the named trigger terms, since a shorter description that drops
"proof-lock" is a skill that silently stops being retrieved for proof-lock
work. What was cut lives on in the skill body.
Tests: check-packing now pins the staging contract hermetically -- it asserts
build-package.sh's staging and the files allowlist from git-tracked inputs
only, because reading packages/cli/daemon (gitignored build output) would pass
on a developer machine and fail a clean checkout. Byte-equality against a real
artifact is a separate opportunistic check that skips when absent. Plugin
byte-parity is extended to openrig-user, which carried the pointer in both
canonical and bundled copies but had no drift guard.
Slice: release-0.4.7 guidance completeness
Gate: qitem-20260720102526-5e585698 (Guard) / qitem-20260720101929-aa922842
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Four hardening items on SliceIndexer's membership build, from the R1
follow-up disposition.
Schema shape is now INSPECTED, not inferred from a swallowed error.
PRAGMA table_info(queue_items) runs up front: an absent table is the one
structural degradation (a structurally empty index is still published
with its location metadata, so get/list keep their semantics and no
rebuild loop occurs), and an absent tags column skips the typed scan and
selects the body-only fallback. Previously any typed-scan failure was
recorded as "tags column missing", so a transient I/O or BUSY fault
silently degraded membership under a false structural label.
Interrupted builds now refuse to answer. Fallback buckets accumulate in
a local map and publish only after the scan completes; every PRAGMA,
typed-scan and fallback-scan execution failure propagates. Previously
buckets were pre-seeded on the published map and a broad catch let a
partially filled result be cached as authoritative — a
plausible-but-incomplete membership that looked complete. The broad
catch in matchQitems is removed with them: left in place it re-swallowed
a refused build and cached it as an empty detail, defeating the
propagation contract.
Fallback-term derivation is scoped to the slices actually served. An
explicit `served` scope threads through get/list -> buildRecord/
toListEntry -> matchQitems -> ensureMembershipIndex (never ambient
state): a standalone uncached get serves {name}, while list() and any
open withMembershipBatch scope serve all. Locations, knownSlices, the
typed scan and the all-location typedMissions verdict are all
established BEFORE any narrowing, so the cross-slice modern/legacy
eligibility rule is byte-identical — narrowing touches frontmatter reads
only, and a served slice with typed membership reads none at all.
Pins added: interrupted-scan refusal with same-indexer recovery;
tags-present typed fault propagating with zero fallback scans; exact
500th-accepted / 501st-rejected boundaries on both caps with false
prefilter candidates ordered ahead of the boundary; no Markdown read
under unrelated slice paths for a standalone get; and a differential
proving batch-scope and standalone gets return byte-identical
membership. missions-complete-endpoint's fixture, the only db-less
SliceIndexer construction in the tree, now satisfies the required
constructor contract — it was tolerated solely by the catches removed
here; its DB is intentionally unmigrated so the absent-table degradation
is still exercised.
Standalone detail at 353 slices / 8000 qitems: ~67ms -> ~45ms; composite
list unchanged. Timing is supporting evidence only — the read-scoping
pin is what fixes the behavior.
qitem-20260720092639-18110994
Co-Authored-By: Claude Fable 5 <[email protected]>
Making the transport authoritative (c113bd41) correctly stopped the CLI from
refusing a send on a failed advisory probe, and a real failure now names the
resolved target and the underlying cause. But it left the operator with a
diagnosis and no next step: the repo's fact/consequence/action convention was
applied nowhere on this path.
Add that remediation, and only after an ACTUAL transport failure — never from
a probe verdict. A DaemonConnectionError proves the resolved target was
unreachable; it does not prove why. Daemon down, wrong port, wrong host,
firewall, and a wedged event loop are all live explanations, so the action
stays diagnostic and asserts no daemon state.
Deliberately NOT daemonNotRunningError(): that helper's text is the
probe-derived "Daemon not running" claim c113bd41 removed. The shape is
reused, the text is not. The copy also declines to oversell `rig status` --
with an env URL set, that command's own probe reports `stopped` for a mere
timeout (daemon-lifecycle.ts:575-585), the same false-stopped class this
slice exists to remove. Rather than hide that, the wording states it ("a
failed health probe does not prove the daemon is stopped") and gates
`rig daemon start` behind operator confirmation.
One helper serves both local paths, so single-seat and fan-out remediation
are identical by construction rather than by hand-maintained duplication.
Non-connection errors still rethrow loudly, exit codes, target resolution,
and the --json branches are unchanged.
Known gap, explicitly out of scope: both catches still print human text on
the --json path. Pre-existing, unchanged here, routed separately.
Tests: R7 (single-seat) and R7b (fan-out) are strengthened in place rather
than duplicated -- their existing target assertions already pin custom-target
preservation. Each now pins the consequence and all three action sentences
verbatim, plus absence of "Daemon not running" and "unhealthy" so no
probe-derived state claim can creep back. R7b is retitled from
GREEN-characterization to RED: it entered the suite as a characterization
under ff13bcdf, but this lane deliberately makes it fail, and leaving the old
label would be the same stale-label dishonesty this slice keeps finding.
Slice: release-0.4.7 / 05-recovery-lifecycle-honesty
Gate: qitem-20260720093225-95de16bd (Guard) / qitem-20260720091422-1b45cf21
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
`--from` is a global option and reached the action's opts, but
runFanOutSend's local params type omitted it, so on --to/--pod/--rig the
explicit origin was silently dropped and ambient identity was written into
both actorSession (audit attribution) and envelopeSender (what each
recipient sees). An explicit operator instruction disappeared with no
signal — the failure looked like success.
The asymmetry showed it was a gap rather than a policy: single-seat and
both cross-host paths already resolved `opts.from ?? resolveSenderSession()`;
only fan-out did not. The value was already forwarded at runtime and merely
type-stripped, so the repair is to thread it rather than to reject --from on
fan-out — a restriction there would defend nothing and contradict three
sibling paths.
Both attribution surfaces read the same resolved sender, so one line
corrects both. No --from still falls back to ambient identity; raw and
--dangerously-interact still omit the envelope entirely; sibling and
cross-host behavior are untouched.
Docs: the CLI presents the registry bearer only WHEN ONE IS CONFIGURED —
for a URL-only anonymous host the Authorization header is omitted. Three
statements asserted it unconditionally, which has been false since
a0c17305 made the bearer optional. Also retires the "http-bearer"
transport-posture wording in favor of "http" (the bearer is not what
defines the transport), and documents --from, which shipped undocumented.
Tests: F1 asserts an explicit --from lands in BOTH actorSession and
envelopeSender on fan-out — it fails on attribution alone, since delivery
already succeeded, so a delivery-only assertion would have passed against
the defect. F2 pins the ambient fallback so the flag stays additive. Both
restore their env in a local try/finally: they sit outside the nested
afterEach, and an un-restored stub would leak into every later test.
Slice: release-0.4.7 — follow-up to a0c17305 (converged R1/R2)
Gate: qitem-20260720091514-0ea16a29 (Guard) / qitem-20260715061702-ba41fea2
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
resolveLocalDaemonUrl returned the OPENRIG_URL/RIGGED_URL alias without ever
reading the probe result, yet both local send callers awaited getDaemonStatus
first. That cost ~818ms on an instantly-failing probe and ~2.05s when the
probe was timeout-shaped (5x250ms bounds + 4x200ms backoff) — pure latency on
the incident path, paid to compute a value that was then discarded.
Make the resolver async and take the probe lazily: an explicit alias returns
byte-exact with zero lifecycle calls; otherwise probe once and fall through
the unchanged chain (running-state getDaemonUrl, then arg-less DaemonClient
config-file/default). Both callers collapse to a single line, so the probe is
sequenced in one place instead of two.
Precedence, the transport-authoritative contract, DaemonConnectionError
handling, cross-host routing, and lifecycle internals are untouched.
Tests: R8/R8b assert the explicit-URL path makes ZERO lifecycle probe calls
on both single-seat and fan-out (a latency claim without a call-count
assertion is unfalsifiable). R6b/R6c now scrub OPENRIG_HOST/PORT and the
RIGGED twins locally — without that they measured the ambient environment
instead of the contract (a managed seat's OPENRIG_PORT overrode the config
file's port via ConfigStore env-over-file precedence). R7b pins the fan-out
real-down error as a green characterization; the fan-out path was already
correct, so it is labeled as characterization rather than a manufactured RED.
Slice: OPR.0.4.7 / 05-recovery-lifecycle-honesty
Follow-up to 8719d0b5; converged R1/R2 review findings 1-3.
Gate: qitem-20260720085026-8e2e5f20 (Guard) / qitem-20260720060214-ff13bcdf
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Incident (fb2be429 class): a seat with OPENRIG_URL set against a busy
daemon (healthz 2.5-13s under event-loop load) got "Daemon not running.
Start it with: rig daemon start" from rig send — the env-URL status
probe collapses any healthz failure to state=stopped, and both local
send gates refused before ever attempting the transport, misdirecting
the operator to start a second daemon against a live one.
Make the status probe advisory on both LOCAL paths (single-seat and
fan-out): getDaemonStatus is retained only for target discovery, and
the actual POST decides. Target resolution (resolveLocalDaemonUrl):
the configured env alias first — readOpenRigEnv(OPENRIG_URL,
RIGGED_URL), exact string with custom port and precedence preserved —
else the status/state-derived host:port when the probe found one, else
the arg-less DaemonClient's own configured resolution (env alias >
ConfigStore file > default; never a hardcoded literal, so a config-file
custom daemon.host/port is honored). A real down now fails honestly:
DaemonConnectionError from the actual transport is printed verbatim
(naming the target and underlying error), exit 1, on both send and
broadcast — the bare preflight restart line is never emitted from probe
status. Cross-host paths, daemon-lifecycle/status, and sibling commands
untouched.
Pinned by a 9-test matrix (10 behavioral legs): single + fan-out under
probe-stopped and running/unhealthy; OPENRIG_URL/RIGGED_URL custom
ports + precedence via the exact clientFactory argument; no-env targets
(state-derived, configured-file 127.0.0.9:7599 discriminator, default);
honest connection-failure prefix with the preflight line asserted
absent. Runtime smoke on the built CLI: healthz-500 probe verdict with
transport up lands the send in ~8ms; nothing-listening prints the
honest connection error. Companion: the no---host compat test's inline
empty client stub (shielded by the removed refusal) now answers the
POST and asserts it fired exactly once.
qitem-20260720034937-c113bd41
Co-Authored-By: Claude Fable 5 <[email protected]>
Final call-site of the composite class: GET /api/scope/audit walks a
mission's slice dirs calling indexer.get(entry) per slice with no prior
list(), so on 5c95a8ee each uncached get built its own 2-scan membership
batch — 2N total queue scans (80 at 40 slices, guard-measured).
Wrap the synchronous post-mission-guard audit body in ONE
indexer.withMembershipBatch scope: one audit request now shares one
membership batch (constant scan count, pinned by a route-level 40-slice
regression that also pins full response semantics — all slice names,
per-slice railStatus, totalFindings, ok). No helper/API changes, no
semantic body changes (the diff is one comment + the wrapper open/close;
the body re-indent is mechanical), no other call sites.
qitem-20260720051200-43d69e17 (final call-site gate on 5c95a8ee)
Co-Authored-By: Claude Fable 5 <[email protected]>
Corrective gate on the operation-scoped membership index (75245ed6):
two composite operations run list() plus indexer.get() per slice, and
with per-operation batch clearing every uncached get built its own
2-scan batch — 2+2N membership scans (82 at 40 slices, guard-reproduced:
route lens 3888.7ms at 353 slices with one queue row; ReviewGatherer.
composeMission is the same shape on the flagship Review path).
Add SliceIndexer.withMembershipBatch(fn): a synchronous, depth-counted
composite-operation scope. The outermost open never reuses a prior
operation's batch (the index is lazy — first membership access inside
the scope builds from then-current queue state; request-boundary
freshness, no snapshot claim); nested scopes share the outermost batch;
the outermost exit always clears via finally (exception-safe); and
standalone list()/get() keep their unchanged one-operation semantics
(their clears are now conditional on scope depth). The slices GET
handler wraps its whole request (list + lens get loop + mission
sidecar) in one scope; composeMission wraps its list + per-slice
gather projection. composeSliceWithContext is deliberately unwrapped
(its second get is detailCache-absorbed — no O(N) batching).
Load contracts pinned by test: one lens request and one mission compose
each execute a constant number of membership scans (both were 82 at 40
slices; the Review counter counts the membership query family by named
shape — the exact 204-read inventory, including the 122 pre-existing
attention/agent/hasActiveQitem projections, is documented in-test).
Scope API pins: in-scope sharing, two-scope stale-generation freshness,
nested reuse, exception-safe cleanup. Composite benchmarks at 353
slices + 8000x3KB queue: route-shaped 194.0ms, Review-shaped 276.9ms.
qitem-ccf87c0d amended gate (corrective on c8f85802 + 75245ed6)
Co-Authored-By: Claude Fable 5 <[email protected]>
Corrective gate on the batch membership index (c8f85802): guard reproduced
three semantic regressions vs the parent per-slice SQL, all reachable
through shipped surfaces.
1. Haystack NUL parity: SQLite LIKE stops matching at the first raw U+0000
in the scanned text (reachable via POST /api/queue/create JSON bodies).
sqliteVisiblePrefix now truncates BOTH body and tags haystacks before
ASCII fold / prefilter / confirm, so a term occurring only after a NUL
never matches — exactly like the parent.
2. Pattern-side NUL parity: the parent bound the FULL '%term%' LIKE
pattern, and SQLite truncates patterns too — a NUL inside a term (e.g. a
rail-item read by the minimal frontmatter parser) loses the trailing
wildcard, leaving an end-anchored '%prefix'. Translation now builds the
bound pattern and truncates it: the untruncated common case stays an
UNANCHORED contains-match (wildcard-free terms remain pure literals on
the regex engine's fast alternation path — an anchored-everything form
measured an ~80x benchmark regression before bundling), while a
truncated pattern translates verbatim with ^...$ anchors.
3. Cross-operation freshness: the index is now OPERATION-scoped, not
TTL-scoped — it lives inside exactly one cold list() rebuild or one
uncached get() (try/finally clears it), so a qitem written between
operations is visible on the very next uncached read, matching the
parent's per-call SQL freshness. One cold list still shares one batch
(the <=4 LIKE-execution load contract holds); listing/detail TTL
semantics unchanged.
Pins added: haystack-NUL exclusion with before-NUL control; pattern-side
truncated-%RAIL end-anchor with mid-body control; isolated typed and
fallback cross-operation freshness vectors. Bench at 28-mission/353-slice/
8000x3KB scale: cold 93.1ms (parent baseline 11442.9ms).
qitem-20260720042203-18f3300d (corrective gate on qitem-ccf87c0d)
Co-Authored-By: Claude Fable 5 <[email protected]>
PM flagship finding: the Project page sat on "Reading the mission index"
for 10-30s (28 missions / 353 slices on the host; cold /api/slices 9.8-10.3s,
concurrent healthz held 13s) because SliceIndexer.matchQitems ran per-slice
full-table queue_items scans — a leading-wildcard tags LIKE with a full
ORDER BY sort for every slice, plus up to three body-LIKE scans for every
zero-typed slice (~1100 scans per rebuild), and the UI forces that cold
rebuild on every mount/focus via refresh=1. The rebuild is synchronous on
the daemon event loop, so every endpoint starved for the duration.
Replace the per-slice tiers with a per-generation batch MembershipIndex
built in TWO streamed scans: (1) typed membership — tags LIKE '%slice:%'
prefilter in ts_created DESC, qitem_id DESC order, parseScopeTags as the
authoritative confirm, a row crediting every slice it tags, capped after
500 confirmed per slice; (2) fallback buckets for zero-typed slices' term
universe — combined regex prefilter, then EVERY term confirmed per row
(overlap-safe), with SQL-LIKE byte-equivalence: ASCII-only case folding,
%/_ translated to .*/. compiled 'su' so wildcards span newlines and '_'
matches one Unicode code point (a😀b matches %a_b%). Term-first assembly
in the original deduped order keeps equal-timestamp output identical.
matchQitems answers from the index; fresh folders force a rebuild via the
knownSlices set; invalidate()/TTL discipline unchanged; tags-column and
table-absent degradations preserved. One production file.
Load contract pinned by test: one cold list() now executes a constant
number of queue_items LIKE statements (was 96 at 40 slices). Deterministic
bench at host scale (28 missions/353 slices, 8000 qitems x 3KB): cold
11442.9ms -> 98.7ms (~116x); the per-rebuild event-loop blocking window
drops from ~10s to <0.1s.
qitem-20260720033934-ccf87c0d
Co-Authored-By: Claude Fable 5 <[email protected]>
PM dogfood finding #1: a slice whose README carried authored
`## Mini-requirements` and `## Proof contract` sections while its scaffolded
IMPLEMENTATION-PRD.md stayed pristine reported both convention findings
against the PRD, and the Review tab showed PLAN "not planned yet" and
DELIVERED "no proof contract" — the audit twins and Review compose
unconditionally preferred any PRD content over the README, per FILE rather
than per SECTION.
Add isPristineScaffoldSection to the scaffold-placeholder grammar twin
(byte-identical CLI/daemon copies, parity-pinned) and select the source
per section, independently for Mini-requirements and Proof contract: an
authored PRD section stays canonical; only a PRESENT-and-PRISTINE
scaffold-only PRD section yields to an authored (non-pristine) README
section. Missing, prose-malformed, or mixed-authored PRD sections remain
PRD-canonical and visible; selection never reads lifecycle status; the
PRD-absent README fallback is unchanged. Consumers aligned: both
scope-audit twins, Review compose (ONE selected parse drives the PLAN
render, DELIVERED join, phase signal, and plan.ssotPath), and the
slice-detail projector (README bytes captured in its existing scan; the
VM-006 qa-verdict lift now joins against the contract the author wrote).
V8d in progress-review-done-coherence updates to the ratified
expected-change on its fixture shape (README selection lifts the six rows);
the no-authored-contract no-op pins live on in V8a/V8b.
qitem-20260720015700-630eef64 (slice OPR.99.0.2.1)
Co-Authored-By: Claude Fable 5 <[email protected]>
Make the http host-registry bearer pointer optional so a URL-only entry
loads and cross-host send/capture/queue reach a tokenless daemon with no
Authorization header (host+VM are one founder-owned trust domain; the mesh
is the auth boundary). Configured bearer_env/bearer_file behavior is
preserved exactly: it still resolves and sends Authorization, a
configured-but-unresolvable pointer still fails before any request
(permission-gate), and both pointers together remain invalid. The CLI and
daemon validation twins move together (parity-pinned).
Add surgical SSH sender provenance: `rig send --from <session>` names the
originating session in the envelope and actorSession; the cross-host ssh
relay reconstructs `--from <origin>` (explicit, else $OPENRIG_SESSION_NAME)
so the remote envelope names the origin instead of degrading to the relay's
own session. Plumbing only — not a trust gate.
Docs/help updated to the loosened contract (as-built cli-reference, host
add help, loader/placement hints, unsupported-transport strings). No
changes to the daemon auth middleware / public-bind protection; queue-over-
SSH stays deferred; no trust/allowlist/telemetry/lifecycle rework.
qitem-20260715052304-7d5b52dc + addendum qitem-20260715052600-d4ea3f8b
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The herdr layout planner built pages with an alternating-0.5 BSP
(buildBspRoot), which the VM reproduced at N=7 as 4 columns x 2 rows with
six ~14-wide cells and one 27-wide double cell — while the launcher modal
promised Auto grid 3x3.
- buildGridRoot replaces buildBspRoot: the grid shape reuses autoGridCols
(cols = ceil(sqrt(N)), rows = ceil(N/cols)) so the applied layout matches
the modal exactly — N=2 -> 2x1, N=5 -> 3x2, N=7 -> 3x3 (cols x rows).
Equal cells come from first-vs-rest ratios (1/N, then 1/(N-1), ...) —
live pane.layout-verified — with each padded row built as an equal
right-strip and rows combined by equal down-strips.
- Incomplete rectangles are padded with inert blank panes (cmux's
blank-surface precedent); blanks are layout filler only and are never
reported as opened seats.
- Real pane leaves still run the composer paneCommand verbatim via
sh -c (read-only -r / ssh-wrap preserved); one atomic layout.apply per
page, fresh workspace/tab tokens, and honest partials unchanged.
- The workspace.create envelope is now VM-confirmed
(result.workspace.workspace_id + result.tab.tab_id + result.root_pane);
extractWorkspaceId keeps its defensive homes. Per PM ruling the default
blank tab is deliberately LEFT ALONE (no tab.close) — live evidence
shows the populated view tab focused.
- Tests: grid vectors N=2/5/7 (leaf counts incl. blanks, pane order,
inert-blank shape, first-vs-rest ratios per axis), page plans carry
blank counts; BSP tests retired with the function.
UNVALIDATED-ON-HOST: authored under VM-only rules; vitest + lint + build
+ real VM pane.layout geometry (equal 3x3 at N=7 with 7 real TUI reads)
run in the build VM before merge.
Co-Authored-By: Claude Fable 5 <[email protected]>
The topology rig page mounted the full RigStatusCard as a large left-
anchored inline card; in graph-overlay mode the explorer panel covered it
(reproduced: card at x335 with its left half, text, and action obscured),
and at status=up its only affordance was a disabled RUNNING button.
- RigStatusControl (topology-only) now renders a compact vellum control:
verdict badge (tone consumes the composed backend status, one-line
meaning on the tooltip) + running/total seats + a primary button that
opens the existing LaunchRecoveryModal. The control stays a live button
in every state — up opens the read-only plan-before-mutation modal too.
Test contract preserved: rig-status-control-/rig-primary-action-/
rig-status-badge-${rigId}.
- ScopePages right-aligns the control (the explorer overlay anchors left),
so it cannot be covered in graph-overlay mode.
- RigStatusCard is untouched (dashboard kernel card still consumes it);
its status vocabulary maps are exported and shared so both surfaces
speak one verdict language. Loading/remote/selection-pending honesty
branches unchanged.
- Tests: compact contract (no rig-status-card on this surface), click
opens the modal with the plan fetch firing, up-state stays clickable.
UNVALIDATED-ON-HOST: authored under VM-only rules; UI vitest + lint +
build + visual inspection (control visible over graph mode, modal opens)
run in the build VM before merge.
Co-Authored-By: Claude Fable 5 <[email protected]>
The terminal-provider ride's cmux adapter delegated each composed pane to
NodeCmuxService.openOrFocusNodeSurface, whose per-seat workspace-anchor
resolution (current, else existing, else create) appended bare un-gridded
surfaces — reproduced as 12 surfaces stacked into the existing workspace:1
(zero splits, zero new workspaces) and as N separate workspaces under
other cmux state. The modal promised 'Auto grid 3x3 / 7 panes' and cmux
delivered neither the workspace nor the grid.
- CmuxProviderAdapter renders each composer page as ONE NEW token-named
cmux workspace, applying the modal Auto-grid shape (autoGridCols =
ceil(sqrt(N)); N=2 -> 1x2, N=5 -> 2x3, N=7 -> 3x3 — PM ruling), running
paneCommand verbatim (read-only -r / ssh-wrap preserved), degrading
failed pages honestly, refusing honestly (cmux_unavailable) when cmux
is not connected.
- CmuxLayoutService gains buildWorkspacePanes (command-level grid core
with an explicit-cols override; generalized top-row right-splits) and
computeGridLayout; buildWorkspace stays its byte-identical tmux-attach
2-column wrapper for the rig-scope /cmux/launch route.
- Equalization (VM-diagnosed timing root + PM ruling): runs AFTER every
pane command lands + the final settle, in EQUALIZE_PASSES fixed passes
separated by EQUALIZE_SETTLE_MS — never early-exited on the RPC boolean,
which cmux can return true from an early call while later layout churn
restores 2:1:1 binary sizes. CmuxAdapter.equalizeSplits parses cmux's
equalized verdict; the result's equalized field is observability only —
acceptance is final pane-frame geometry. Non-fatal by design.
- Transport root (VM-diagnosed): cmux-transport buildCommand's generic
RPC allowlist omitted workspace.equalize_splits, so every production
equalize threw Unknown cmux method (request_failed, absorbed by the
non-fatal path → grids stayed 2:1:1) while fake-transport adapter tests
masked it; manual cmux rpc bypassed buildCommand and worked. The method
is now allowlisted, with a REAL-transport exact-command regression test
(createCmuxCliTransport + mocked exec) pinning the cmux rpc command and
the equalized-verdict JSON path.
- startup wiring drops the per-seat resolveSeatNode path.
- Tests: provider one-build-per-page + Auto-grid cols vectors (N=2/5/7),
layout-core shape/split/send/equalize-pass vectors, equalize-after-last-
send ordering + second-pass-despite-true + honest equalized:false,
verbatim paneCommand carriage, adapter RPC verdict parsing, production
transport allowlist regression, unavailable refuse.
UNVALIDATED-ON-HOST: authored under VM-only rules; typecheck + vitest +
visual verify (final pane frames equal thirds at N=7, modal screenshot)
run in the build VM before merge.
Co-Authored-By: Claude Fable 5 <[email protected]>
Guard-blocking B1: launchHarness ran a real "codex -p <profile> mcp list"
subprocess before command construction, uncontrolled in the two fleet-profile
tests (green only where an ambient codex+fleet profile happened to satisfy it;
not portable to the clean auth-ready baseline). Arch HK-AR-1 ruled shape (a):
whole-probe DI, inject at the owned contract.
S-1 (adapter, the only product-source change): extract the inline probe into a
module-private defaultProfilePreflight (P1 pin: NEVER exported); add exactly one
optional constructor dep verifyProfilePreflight defaulting to it; type-only
import of CodexProfileProbeResult keeps the dynamic import lazy for production.
Production call path byte-equivalent (default IS the real probe).
S-2: the two fleet-profile tests inject a controlled ok:true probe so no real
codex subprocess runs; their launch-command assertions are unchanged.
S-3: new adapter-boundary test — injected failing probe -> the adapter's
composed error (error + "\n Fix: " + migrationHint) AND tmux.sendText never
called (rejection happens before command construction).
Note: CodexProfileProbeResult.profile is a required field, so the injected
stubs include it (async (profile) => ({ ok, profile, ... })) — plan-literal
erratum folded as a faithful adaptation, flagged to dev46-planner2.
Footprint: exactly 2 files atop frozen 925a9807; factory-rsi + conveyor
byte-stable. Local only, never pushed.
Co-Authored-By: Claude Fable 5 <[email protected]>
Base 704ddb58. Per-failure intentional-vs-regression verdict + citation; none
greened mechanically. Product source UNCHANGED (all 4 were test-side, no
regression). Full daemon suite now honestly green (391 files, 5824 pass, 1 skip).
- factory-rsi-rig FR-3 = STALE (intentional 0.4.6 change, commit 8250d702). The
rig.yaml summary + CULTURE.md ship the design "seats inherit their runtime's
default model; qa/review/dogfood on the alternate runtime vs the builder".
Test tracks the shipped spec: builder=claude-code, qa/review/dogfood=codex,
no per-seat model pin. Prior "Sonnet Claude; Codex builder+checker" superseded.
- conveyor-starter "ships exactly" = STALE (test-ordering). All six specs ship +
load correctly; the expected literal appended factory-rsi last while the
assertion compares a .sort()ed actual (factory-rsi sorts after conveyor).
Restored to sorted order. Product correct.
- codex-runtime-adapter launchHarness profile pair = ENV-COUPLING (not stale,
not regression). launchHarness correctly honors OPENRIG_SHARED_DOCS_ROOT (its
dedicated test covers that); the profile/model launch tests assume the
os.homedir() fallback and flapped by ambient env. Hardened: a beforeEach
neutralizes the ambient var so the queue-root default is deterministic.
Proven: the suite passes with OPENRIG_SHARED_DOCS_ROOT set (was the flap).
Co-Authored-By: Claude Fable 5 <[email protected]>
The predecessor (0ec6411c, frozen NO-PASS) joined the STRIPPED promised text
against the RAW acceptance-row text through an image-stripping comparator.
That relation is strictly COARSER than the acceptance dedup key
(trim+casefold on raw text), so post-dedup row uniqueness did NOT carry to
the join: two distinct rows could collapse onto one obligation. Guard found
two collisions (BLOCKING B1, reproduced independently by QA) — a non-contract
row was false-lifted, and an unverified second ordinal was lifted.
Arch ruled the PIN-C mechanic (Option A, raw-key primary) and CLEARED the
seam. This implements that ruling:
- compose.ts: PromisedItem gains an additive `rawText` — the authored contract
line body, trim-only, captured BEFORE the inline-image strip. `promised.text`
stays stripped and byte-unchanged, so refMatches and every DELIVERED render
are untouched; extractProofContract stays pure (no IO, no mutation of the
acceptance text or the dedup key). `normalizeContractText` is RETIRED — its
only consumer was this join and it never merged, so it leaves no orphan.
- slice-detail-projector.ts: ONE shared `textKey(t) = t.trim().toLowerCase()`
(verbatim the dedup expression, no whitespace collapse) now keys the dedup,
the join, and the FS-1 guard. Joining raw-against-raw makes
join-relation == dedup-relation a fact of the code rather than an argument.
The lift is a multiplicity-gated 1:1 association: a row lifts only when its
key reaches exactly ONE promised obligation, that obligation is verified, and
exactly ONE acceptance row carries the key. Any ambiguity lifts NOTHING
(fail-closed). This makes pm's FR-2 invariants structural: lift only via a
1:1 association, and the Progress lift count can never exceed Review's
verified count. Never sets done=false (FR-3).
Under raw keys the guard's two collisions become distinctions, so the gate is
armed-but-vacuous on real slices; its live purpose is the degenerate case of
two byte-identical authored contract lines.
- Arch Cell A (A1 one-home) preserved byte-identical from the predecessor:
gather.readProofArtifacts extracted VERBATIM into domain/review/proof-io.ts,
gather delegates, the projector imports the SAME reader.
- UI: ZERO edits (consumers key off item.done).
Regressions RC1 (no false lift of a non-contract row), RC2 (only the verified
ordinal lifts), AMB (byte-identical obligations lift nothing, no throw), and
INV (lift count never exceeds Review's verified count), plus the V1 money-shot
(13/13, zero ACTIVE) and V2-V8/FS-1 retained. review-composer's whole-object
assertion on the extractor is updated for the additive field.
Proof dispositions in the suite header are the ASSERTED differential contract
and are TO BE CONFIRMED by the VM run — no leg is stamped green here.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
POST /:missionId/complete writes the mission README status but left the
authored-status sidecar TTL-hot, so /api/slices served the pre-write word
for up to 60s while /api/missions/:id read the new word from disk — an
FR-1 coherence violation through the daemon's own API (guard final
product review). Per the arch ruling (b8d91aee…, drop-the-blob) and plan
v1.6.1 §J-1: one narrow SliceIndexer.invalidateMissionStatusCache()
nulling ONLY the missionStatusCache blob, exactly one additive
post-success call in the /complete route (writeMissionStatusComplete and
readMissionStatus stay byte-unchanged — the C-vii fence holds around the
one-line exception), and the §J-1d regression: hot-primed sidecar →
POST complete → immediate detail+slices coherence, idempotent re-POST,
and the negative that listing/detail caches survive. Out-of-band writes
remain the TTL regime by design. Zero UI edits.
Co-Authored-By: Claude Fable 5 <[email protected]>
Restructures the three VM-005 test files per plan v1.3 §D-bis /
ARCH-RULING-b3 (sha 632ff319…) + guard recheck R4/R5: Tier A differentials
express candidate expectations through both-ends surfaces (DOM harness +
both-ends lib exports) so RED-at-base is named expected/received; V4
crosses the clock boundary BEFORE its expected base failure (t1-vs-t0
byte-stability + post-jump idle differential); V5 carries the
authored==derived agree-cases both-ends green; V1 matrix adds the overview
card (FR-4 bucket at the observable) + storytelling complete-and-hide,
with the detail DOM cell dispositioned N/A per §C-v (no mission-status
chip renders on the detail page — its observable is the raw API payload,
pinned by lockstep + missions-routes). Tier B new-symbol units ride
dynamic imports with t1 presence-first, t2 fence comments, t3
executed-count pins. Implementation bytes FROZEN — delta is exactly these
three test files atop 98ae37542d.
Co-Authored-By: Claude Fable 5 <[email protected]>
The LAST review-tab fix (6 of 6). Mission status was answered four
independent ways (authored README frontmatter / client roll-up / bucket
test / PROGRESS.md live override) with no precedence rule, a taxonomy hole
labeled unknown, and clock-decay. Now: ONE clock-injected reconciled home
(reconcileMissionStatus — authored-when-present wins and never consults
the clock; derived ladder empty/blocked/draft/active/shipped/idle, the
unknown literal deleted), an additive daemon missions sidecar on the
slices payload, all six chip call sites swapped, the Q1-Option-A retirement
of the PROGRESS.md status override (SC-26 superseded for chips, named at
the removal site; hook left consumer-dormant), FR-4 authored-shipped
buckets archive, and STATUS_TO_BUCKET placeholder->draft (C-vi).
Plan of record: IMPLEMENTATION-PLAN-vm005 v1.2 sha256 29c0905e...ee0b8a
(arch CLEAR ec1d0616 + pm PASS 4d9d5887; pins Q1-P1/Q1-P2/Q3-P1 honored).
Tests: V1-V6 matrix (V6 bound to the frozen 5d184b24 packet bytes) +
sidecar/lockstep/scaffold-literal daemon suite + DOM surface tier.
readMissionStatus: export-only visibility change for the lockstep pin
(read path byte-identical, C-vii fence held).
Co-Authored-By: Claude Fable 5 <[email protected]>
Micro-bundle, three items, zero new copy:
- A: twin module gains hasAuthoredNumberedItem (dot-or-paren + non-placeholder,
the ONE authored-numbered-item grammar); compose hasAuthoredMiniReqs
delegates (single-parse pin governs the parse; the twin fn is the line
grammar); both scope-audit mini-reqs arms consume it — healing the audit's
placeholder-blindness AND the dot/paren grammar divergence (a live
audit-absent/review-present R3 case for '1)' items) in one predicate.
Finding message unchanged.
- B: twin module gains isPlaceholderOnlyBlock (per-line; null/empty -> false);
a placeholder-only Intent section is treated as absent at the source so the
existing 'no intent recorded' degrade fires. No new string; UI + freeze
inherit.
- C: prose/bullet-only mini-reqs suppression gets its test pin + doc-comment
disclosure (reviewer-L1 ruling); zero behavior change, asserted on base
behavior first.
Twin pairs byte-identical (parity-pinned via existing CLASSIFIER_FILES).
Tests: unit vectors (block + numbered-item grammars), audit-arm matrix,
cross-reader agreement incl. the paren-heal shown RED at base (disclosed
expected-change), intent degrade + byte-identity carve, suppression pins.
Co-Authored-By: Claude Fable 5 <[email protected]>
Test/proof-only, zero production-source delta from cab26bb0. The §3.9 B1
regression called hasActiveQitem with one arg; against the frozen 302036aa
signature hasActiveQitem(name, slice) that dereferenced undefined.qitemIds and
CRASHED (TypeError), so the RED never observed the old active=true divergence.
Pass the indexed legacyonly SliceRecord as an optional second arg: the frozen
302036aa consumes it (leg 2 reads rec.qitemIds -> the display-tier body-only id
-> returns true = the divergence), the fixed cab26bb0 ignores it -> false. RED
at 302036aa now reaches expect(active).toBe(false) with received true;
GREEN at cab26bb0 passes with false + empty band. No source behavior change.
Co-Authored-By: Claude Fable 5 <[email protected]>
Blocker-only repair on frozen candidate 302036aa (guard B1/B2/B3, arch
fixback plan v1.4 f1-f6).
B1 (hasActiveQitem): drop the qitemIds leg-2 membership check — it inherited
matchQitems' sanctioned legacy substring/body fallback, so a zero-typed
body-mention-only row promoted activeQitemPresent to true (phase BUILD) while
the strict band was empty. Leg 1 streams (iterate + break on first canonical
confirm, NO SQL LIMIT — a pre-confirmation LIMIT would recreate B2 at the
phase signal). Equivalence: every canonical member contains the literal
slice:<name> substring (P1), so leg 1's untruncated prefilter finds them all;
leg 2 could only add non-canonical display-tier ids.
B2 (matchQitems typed tier): cap on CONFIRMED matches, not the prefilter
window. iterate() ORDER BY ts_created DESC, qitem_id DESC (P2 tiebreak =
deterministic), confirm per row, break at 500 confirmed; NO prefilter LIMIT
(else >500 suffix decoys hide a true typed member and un-gate the fallback).
Substring tier + its LIMIT 500 + terms untouched.
f4: delete the dead, untested excludeTagPrefix === "mission:" branch (only
caller passes "slice:").
P3: the two-tier doctrine ships by name in the helper JSDoc and both consumer
comments — SIGNAL tier (phase/band/attention) = canonical-only; DISPLAY tier
(queue-tab qitemIds) = gated legacy fallback; never promote display into signal.
Tests: §3.9 (zero-typed body-only: display-tier keeps it, signal-tier drops it,
phase/band agree), §3.10 (suffix-storm: true member kept, fallback gated,
hasActiveQitem true), §3.10a (P2 tie-vector: deterministic id-DESC order).
Daemon-only; UI untouched; agentsForSlices/composeAgents band code unchanged.
Co-Authored-By: Claude Fable 5 <[email protected]>
Workflows + multi-host coordination + factory foundations.
Deterministic workflow engine: strict spec validation (unknown
keys reject, unreachable steps and unguarded cycles fail); loop-
guard max_hops enforced at projection; waiting-exit replay
absorption; per-instance keepalive watchdogs auto-armed in the
routing transaction; startup sweep re-arms keepalives and
surfaces stuck instances; rig workflow continue relabeled to its
read-only inspector semantics. Full-featured spec language: role,
target.rig, preferred_targets, next_hop.on, invariants,
loop_guards, exception_routing, closure, gates, skill_refs,
spawn_budget with strict-keyset validation and shipped-spec
compat pinned by fixture round-trip.
Workflow CLI: new verbs rig workflow run (instantiate + follow
live to terminal; exit 3 = workflow failed distinct from
transport 1/2), rig workflow watch (mid-flight attach; snapshot-
first; drops reconnect then announced poll fallback), rig
workflow status (needs-attention rollup; proven-empty on a clean
fleet), rig workflow route (frontier re-target in one
transaction; the step does NOT advance). trace/list/show human
output is now formatted; --json byte-identical to before. Out-of-
band terminal closure of a live workflow-frontier packet now
rejects with workflow_frontier_packet.
Workflow web UI: new workflow catalog, per-instance detail + trail,
in-band rollup, visual topology renderer, and step-trail timeline
pages; new hooks for workflow reads + SSE stream (unscoped by
rig); pure layout math extracted for unit tests. UI acceptance
carried by per-slice merge-time real screenshots + the two-host
integration proof; twin fixture backfill routed as a non-blocking
0.4.7 fast-follow.
Exception + human-gate model: rig workflow resume redrive from
failed step (rebound, fresh capability-aware re-resolution, one
fresh max_hops window); exceptions become durable attention items
in the same transaction as the failure; maturity dial
orchestrator-first vs human-only per class, per workflow, per
host with default orchestrator-first; the attention band is
workflow-aware; the happy-path guarantee unchanged. Events:
additive workflow.resumed.
Multi-host coordination happy path: hosts register + select via
new dashboard host-config + indicator surfaces with sticky rig
host select consumed by observe/interactive verbs; remote-host
workspace surfaces readable via --host on the workspace tabs;
cross-host queue routing via rig queue create / handoff /
handoff-and-complete --host and member@rig@<host> destination
form (explicit-only, at-least-once + idempotent, successor-first
close-second, closure_target audit metadata); cross-host direct
coordination verbs (rig send, rig capture, rig transcript, rig
broadcast) gain an http transport branch reaching remote daemon
routes CLI-direct with byte-verbatim ssh preserved; fleet-
attention altitude rollup on the attention band.
Factory foundations: workflow-to-rig binding layer (rig workflow
instantiate / run --rig; roles resolve to seats by capability;
resolve-once, record-in-the-packet; stable seat coordinate); the
single-rig recursive-self-improvement factory MVP shipped starter
with an inner plan-implement-check-review-release loop, dogfood
decoupled + running out-of-band against the shipped product,
publish held at a human sign-off gate; member-exists instantiate
advisory + cross-rig wrapping workflow travelling the cross-host
queue path.
New agent runtime adapter joins the shipped runtime dial as a
first-class option; terminal provider gets first-class treatment
across the terminal launcher, send/capture/broadcast, and the new
provider-agnostic skill; daemon read-path hardening for the
multi-host + workflow load with no functional behavior change.
Two new bundled skills ship canonical + plugin with byte-parity
guard: a rig terminal open/views/status model (AGPL clean-room
rail) and a provider-agnostic vs cmux-specific delta.
Migrations: additive only. Existing v0.4.4 databases upgrade by
running rig daemon start on the new daemon.
Note: v0.4.5 was skipped; there is no cut between v0.4.4 and
v0.4.6. A pre-release fix-batch was applied to the reviewed tree
(product-name neutralization, superlative-prose scrub, shipped-
doctrine touch-ups).
See CHANGELOG.md and docs/releases/v0.4.6.md.
Multi-host + Living Notes.
Multi-host foundation: shareable whole-topology staged spin-up brings
a rig topology up across multiple hosts in stages; VPS product-factory
multi-host hardening ships with rig host verbs (add / list / doctor,
capped at three; one built-in product-factory-vps posture, three-
valued per item); the transport posture is documented (ssh for pane
ops, http-bearer for daemon REST, no cross-transport fallback); the
For-You feed aggregates activity across every registered host; and
rig file moves files across the topology's registered hosts.
Living Notes: durable INTENT / PLAN / DELIVERED signal layer at
mission and slice altitude with agent authorship + timestamps; cheap
composer surfaces make it the first-class place agents record
decisions, plans, and delivered work; the one-structure review
contract reads left-to-right as a single vertical stack (INTENT above,
PLAN + mockup in the middle, DELIVERED with paired proof at the
bottom); a plan change deletes the old plan and writes a new one -
never demotes or stacks multiple competing plans.
Operationalize the SDLC control plane: conventions SSOT ships in
source at docs/reference/sdlc-conventions.md (Living Notes UI section
names, proof-contract format + plannedRef mockup pairing, staged-
approval locks, C1 proof header + closed sets, three role contracts,
curation rule, elastic-middle doctrine, advisory fail-open audit
posture); rig scope slice create scaffolds the convention sections +
proof + PROOF.md + an IMPLEMENTATION-PRD.md skeleton for every
template kind; rig scope audit + rig workspace doctor gain sdlc-
convention advisories (records-and-advises, gates on HIGH only); the
mission-slice-sop skill ships in canonical source with byte-parity
guard; rig proof / rig scope slice create / rig scope slice approve
help + cli-reference teach the SDLC flow.
BREAKING: rig ps default flips to consolidated all-active-rigs
compact projection (the v0.4.0 current-rig-only default is retired -
it hid running rigs from the operator's field of view). --json is
scope-not-shape (still a bare array with existing per-entry keys;
scope widens to all non-archived rigs including stopped ones; one-line
migration for the old fleet firehose: rig ps --nodes -A --full).
-A / --all-rigs keeps exactly ONE meaning (the --nodes fleet widener;
bare -A is a structured teaching error). --nodes names its scope
everywhere (session default local-only; --host <id> --nodes requires
--rig or -A; multi-host fan-out is rollup-only by default; the
explicit ladder --all-hosts --nodes -A --full fans out per-node with
hostId-stamped projected rows). --all-hosts / --hosts --json emits the
shared AggregatedPayload (items + hosts with closed-enum statuses:
ok | unreachable | unsupported-transport | auth-failed).
Operator UX: agent altitude coordination panel scopes multi-agent
state to the right altitude (workspace / mission / slice) so the
coordination surface reads without per-seat drowning; composes with
the workspace observability tabs from 0.4.1. Docs closeout catches
docs/as-built/architecture.md, docs/as-built/cli-reference.md, and
the codemaps up to shipped state.
Migrations: additive only. Existing v0.4.3 databases upgrade by
running rig daemon start.
Behavior change (rig ps default view): the compact all-active-rigs
projection replaces the current-rig-only default. If your call sites
relied on the v0.4.3 default shape, add --full for the rich per-node
projection or --rig <name> for the prior scope.
Public-safety scrub applied inline to packages/ui/twin/corrective/
fixtures-corrective.ts (one demo-fixture role identifier neutralized
to the demo-namespace baseline established at 0.4.1).
See CHANGELOG.md and docs/releases/v0.4.4.md.
"Rigs that survive." Crash the daemon or the whole machine — power-off,
not a graceful stop — and on reboot the seats come back to their
original sessions with pre-crash markers recalled, zero fresh-prime.
Proven at the operating-system level via a real hard power-off
capstone.
Survival backbone:
- Crash-restore resume-token ledger (FR-3 → FR-7 + FR-6.1 periodic
freshness re-stamp): capture-on-adoption, snapshot-refresh, restore-
target-pin, 1h advisory freshness threshold with --fresh re-verify,
and no-silent-fresh-prime on the restore path.
- Fixture-home isolation guard: a fresh-prime that would clobber a
live rig's home dir is refused loudly.
- Liveness PID-verify: live seat distinguished from dead-PID ghost.
- Daemon event-loop health + TerminalSessionBroker resilience across
daemon restarts.
- Startup-proof: challenge-verified orientation read.
Deny-by-default rig-send reversal: only needs_input (a real interactive
picker on the target pane) hard-refuses; unknown / stale / missing /
busy advise-and-send. --dangerously-interact --reason override
unchanged. Removes the friction bridge that routine coordination hit
under the 0.4.1 posture.
New CLI: rig auth secret-boundary hardening (fd-first: open the fd +
operate on the fd, never re-resolve the path — closes the check-then-
use gap under interruption / concurrent-legitimate-rig-process
scenarios; TOCTOU class closed as a consequence; leak-hunt regression
against the real credential file).
rig send unified targeting: multi-recipient list + --pod + --rig;
backward-compatible single-send.
Codex activity-signal hardening: idle Codex takes normal sends; genuine
prompt still blocks; hook-trust autoclear on the restore path for
fully-unattended Codex restore.
Recovery: seat-handover full-cycle (delivers captured context to a
fresh successor via the shipped restore-packet write +
deliverInitialSessionPrompt; loud-unwind both lanes on failure); seat-
forking closeout; idle-gate watchdog wakes an idle seat holding a
claimable gate.
Wave-2 UI: rig-status + launch-control on the workspace; switch-client
view-retarget; dashboard theming (Vellum Dark opt-in via Tailwind
darkMode selector; light byte-identical).
Hardening + bug pile-ins: session-admin mutation auth-guard;
rig queue show bounded body preview; manual configurable compaction
trigger + same-seat guard.
Doctrine: mission-slice-sop skill ships.
Migrations: additive only. 045_resume_verification,
046_seat_identity_verdicts, 047_events_node_type_index. Existing v0.4.2
databases upgrade by running rig daemon start.
Behavior change (rig send default posture): unknown / stale / missing /
busy no longer default-block. If call sites relied on 0.4.1 default-
block, add an explicit guard.
See CHANGELOG.md and docs/releases/v0.4.3.md.
Targeted hotfix for the CLI daemon-status probe's false-negative
window after a daemon restart.
Root cause: the single /healthz fetch in getDaemonStatus lost the race
to the post-restart HTTP listener bind window — process up, listener
not yet accepting — so rig daemon status reported a false "Daemon not
running" / "healthz failed" even while /healthz was returning 200.
Fix wraps the three getDaemonStatus /healthz branches (pid-alive,
no-state, injected-probe) in a status-probe-local bounded settle:
max 5 attempts, 200ms backoff, hard-bounded by construction. Genuine-
down still fails all attempts and reports stopped / unhealthy after
the budget — never masked, never an unbounded wait. fetchDaemonProbe
and start / stop / checkPid timing are unchanged.
Focused tests at the probe layer: transient-then-healthy settles to
running / healthy; pid-alive-but-never-answers stays healthy=false;
genuine-down still reported stopped after the hard-bounded budget.
Scope: packages/cli/src/daemon-lifecycle.ts +
packages/cli/test/daemon-lifecycle.test.ts.
No CLI surface changes. No daemon / DB / migration changes. No
dependency changes.
See CHANGELOG.md and docs/releases/v0.4.2.md.
Final durability-close pass covering the full v0.3.4..HEAD delta (198 commits / 208 files). Beyond what landed in the original wrap-gate (through 8d55ea60) and the slice-35 forward-fix (1cf1bd80), this pass adds documentation coverage for: slice 34 (rig ps current-rig default + -A/--all-rigs + resumeTokenPresent security correction; restored from 0.4.1 to 0.4.0 mid-wrap), slice 38 (real-terminal TerminalSessionBroker — one tmux pipe-pane per session, N WebSocket subscribers, canonical 120x40 geometry; closes the second-view-steals-output failure mode), slice 39 (unified static/live terminal component — shared StaticTerminalPlate, 90x27 mirror, fontSize-scaling selection fix), and slice 01 round-two (global LiveTerminalRegistry cap via ui.terminal.max_live_terminals config, LiveTerminalProvider + ProgressiveTerminal interaction model). Known-Limitations carry-forward note corrected to reflect that nothing of substance carries forward to 0.4.1 from the wrap-gate set (slice 34 and 35 both landed; 38 and 39 shipped via live-dogfood forward-fix). SKILL.md cascade propagated byte-identical (md5 c6c5de2ed3df49402e267021c6ee277e, 50727 bytes) to substrate canonical + hub .claude + hub .agents. 4-layer privacy sweep (paths/identifiers/default-fallback/commit-message attribution) on the full 198-commit ship: clean.
Founder flagged: text selection in the live terminal is "weird" in terminal-view
(scaled grid cells) but fine in graph view (popover ~= natural width, scale ~1).
Root cause (researched): xterm.js #6023 — under a CSS transform:scale ancestor,
xterm's mouse hit-test divides a POST-transform pointer offset by the PRE-transform
cell size, so selection/click drift by the scale factor (worse farther from top-left).
This slice's FR-3 wrapped the live xterm in ScaleToFitTerminal's transform = the cause.
Fix (xterm-maintainer-blessed): the LIVE xterm scales via fontSize, not a CSS transform.
- FocusedTerminal gains fit="natural"|"width"|"contain": it captures the xterm's natural
90x27 size at the base font, measures its fit-wrapper, and sets term.options.fontSize
so 90x27 fits (width: fit-width no upscale; contain: both axes, capped upscale, centered).
A ResizeObserver on the wrapper refits on resize. No CSS transform -> native-correct
selection/click. "natural" mode (feed-card) unchanged.
- ProgressiveTerminal LIVE branch renders <FocusedTerminal fit={fit}> directly (no
ScaleToFitTerminal). The STATIC plate keeps ScaleToFitTerminal's transform (native DOM
selection follows transforms fine). Pixel-matched -> the founder's signed-off 90x27
look + the mirror hold; only the live-scaling mechanism changed.
+1 source-assertion test (live = fontSize, not transform). Full ui suite 5 fail/1284 pass
= 0 introduced (the 5 are known inherited). tsc clean. Needs full re-proof (selection +
scrollback + typing + cursor + look pixel-identical).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Guard found 3 more the first sweep missed; a comprehensive grep found 4 additional
test-file stragglers. Completed to ZERO stale geometry comments:
- terminal-styling-polish.test.tsx (120x40 -> 90x27)
- SessionPreviewPane.tsx (100-col -> 90-col; a 120->100->90 iteration leftover)
- FocusedTerminal.tsx ("top ~16 of 40 rows" -> 27 rows)
- topology-grid-responsive.test.ts x2, session-preview-pane.test.tsx,
terminal-popover-multilive.test.tsx (120-col -> 90-col)
- p5-3-activity-ring-hot-potato.test.tsx: reworded the stale "widens to 880px/904px
live plate" comment to the current w-max no-reshape behavior.
Left intact: the styling-polish not.toContain("w-[880px]"/"w-[904px]") ASSERTIONS
(they verify the hardcoded widths are absent - correct, not stale).
Comment-only, zero runtime change. Touched suites green (25/25). tsc unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Guard's post-proof cleanup, all comment/JSDOC only (zero runtime change):
- BrokerOptions JSDOC: cols/rows now say "Default CANONICAL_COLS (90)" /
"Default CANONICAL_ROWS (27)" (was a stale "Default 40"; the constructor already
defaults to the CANONICAL_* constants, so runtime was never 40 - point at the
constants so the comment can't drift again).
- 13 stale geometry comments across 6 UI files updated 120x40/90x40 -> 90x27,
120-col -> 90-col, the popover's stale ~880px note -> ~650px (90 cols at font 12).
tsc clean (daemon + ui). Source-asserting suites green (UI 27/27, broker+ws 47/47).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Founder sign-off feedback: on the node-detail page the live terminal sat at its
natural 90x27 size in the top-left of the fixed h-[500px] panel, leaving a gray
L-shaped dead zone. Cause: ProgressiveTerminal wraps everything in ScaleToFitTerminal
which fit WIDTH only + capped scale at 1 (correct for the small grid/graph/table
cells, but the node-detail panel is much larger than the 650px-wide terminal).
Fix: add a "contain" fit mode to ScaleToFitTerminal that fits BOTH axes (min of the
width/height ratios), allows a capped upscale (MAX_CONTAIN_SCALE=2), and centers -
so the 90x27 mirror fills the dedicated panel as much as its aspect allows, never
clipped, balanced margins instead of a top-left gap. ProgressiveTerminal forwards a
`fit` prop to both its static + live scalers; LiveNodeDetails opts into fit="contain".
Grid/graph/table keep the default fit-width (cells never upscale) - unchanged.
Tests: +1 source-assertion for the contain mode + node-detail wiring. Full ui suite
5 fail / 1283 pass = 0 introduced (the 5 are the known inherited at base). tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Founder-directed (spec reassigned to dev2-driver). ONE terminal component in two
states: static translucent smoked-glass (/85) that flips IN PLACE to the opaque
#0c0a09 live xterm on click. Same 90x27 landscape geometry (1.72:1, founder-measured;
was 120x40) scaled-to-fit per cell; mirror applied across grid + graph + table.
- terminal-geometry.ts (NEW): dep-free single source of truth (90x27, font 12,
lineHeight 1) shared by static + live so they can't drift; matches broker CANONICAL.
- ScaleToFitTerminal.tsx (NEW): ResizeObserver measure + CSS-transform fit-width
(never clip) wrapping both states identically (the mirror).
- StaticTerminalPlate / SessionPreviewPane: smoked-glass /85, w-max, 90ch geometry,
scrollable static window; whitespace-pre line-returns.
- TopologyTerminalView: 2-col max grid, header-above-full-width ProgressiveTerminal,
no white card, no popover trigger (in-place click-to-live).
- TerminalPreviewPopover: w-max, no reshape on go-live (graph/table mirror).
- FocusedTerminal: natural-size container (full 27 rows, cursor visible) + wheel
scrollback handler.
Live scrollback (per-subscriber, multi-viewer-safe):
- Broker scroll(sub, offset): tmux capture-pane history window, BOTTOM-anchored
(offset lines above the live bottom; verified against real tmux -S -N semantics),
fanout skips scrolled subs, offset 0 repaints live screen + rejoins fanout.
- WS route: {type:"scroll", offset} branch. Client: wheel -> sendScroll, type -> live.
Tests: broker 38/38 (incl. faithful bottom-anchored scroll tests). Full daemon + ui
suites: 0 introduced failures (56 daemon + 5 ui failures confirmed INHERITED at base
212b4523 via both-ref run). tsc clean. LOCAL main only; VM proof + founder sign-off pending.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
39 commit 2/N. The topology terminal grid (TopologyTerminalView), localized to
SeatTerminalCard + TerminalGrid:
FR-2 (responsive columns): 1-col / 2-col / 3-col by browser width, with 3-col
moved from the narrower `lg` to the wider `xl` breakpoint so a scaled
real-terminal static fits 3-across cleanly without cramping
(grid-cols-1 md:grid-cols-2 xl:grid-cols-3).
FR-3 (scale-down, legibility-floored): the static thumbnail renders the
fixed-shape terminal (whitespace-pre from commit 1, never reflowed) scaled down
from a top-left origin (origin-top-left + a per-breakpoint scale, xl:scale-90 at
the densest 3-col), mirroring the live fixed-geometry scale (FocusedTerminal
pins 120x40, no fit/reflow). The exact scale VALUE is legibility-floored and
QA-screenshot-measured per breakpoint; xl:scale-90 is the starting value
(forward-fix per QA's real-source-daemon screenshots).
FR-4 (uncramped, no wasted padding): tighter card padding + gaps
(p-2/gap-2 -> p-1.5/gap-1.5) and a tighter inter-card grid gap (gap-3 -> gap-2).
FR-6 (click static -> live) unchanged: the grid still reaches live via the
TerminalPreviewPopover progressive trigger (preserved through commit 1's
consolidation).
TDD: source-guard test for the responsive cols + scale + padding; topology
render suite 12/12 (no regression); ui tsc clean. The visual result (scale
legibility floor, no-cramping at each breakpoint) is QA-screenshot-validated.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
39 commit 1/N (base 212b4523). The shared static-terminal component + the FR-5
line-return root-cause fix + the opaque mirror flip.
FR-1 (shared static component): new StaticTerminalPlate backs EVERY static
polling-preview terminal - the smoked-glass plate (SMOKED_STATIC_PLATE_CLASS,
moved here as the shared home, re-exported from ProgressiveTerminal for existing
importers) wrapping the compact SessionPreviewPane. The two duplicated callers
are consolidated onto it: ProgressiveTerminal's static mode (interactive
click-to-live button) and TopologyTerminalView's grid thumbnail (non-interactive
plate; click-to-live stays the separate TerminalPreviewPopover trigger, FR-6).
All existing test IDs preserved (progressive-terminal-static, ...-thumb-plate,
...-thumb-pane) so no surface regresses.
Mirror flip (212b4523 forward-fix changed the live look): the compact static
content now reads OPAQUE #0c0a09 (matching the live xterm
LIVE_TERMINAL_RENDER_BACKGROUND) within the smoke plate - was translucent
bg-transparent.
FR-5 (static line-return): the tmux capture is ALREADY pane-width-wrapped;
whitespace-pre-wrap + break-words re-wrapped it at the narrower container width
(double-wrapped/broken line returns). The compact <pre> now uses whitespace-pre
(renders the tmux lines as-is, mirroring the live fixed-geometry xterm) +
overflow-x-auto (scroll/pan wider lines). Default variant untouched.
TDD: new static-terminal-plate test (div/button modes, smoke plate, testid
passthrough); session-preview-pane compact assertion updated to the FR-5 +
opaque reality. Verified: static-terminal-plate 3/3, session-preview-pane 3/3,
progressive-terminal 4/4, p5-topology-terminal 12/12, ui tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.4.0.38 forward-fix, test follow-up to ada55f30. Committing the founder-
verified patch broke terminal UI tests that the patch itself did not update;
this commit makes them match the new source so the tip is green. Two distinct
causes (classified against the branch):
1. focused-terminal-lifecycle.test.ts - two source-guard assertions obsoleted by
the patch (INTRODUCED by ada55f30):
- FR-7 guard asserted toContain("fitAddon"); FitAddon is now removed entirely.
Updated to assert the stronger reality: no FitAddon + pinned cols=120 (the
client pins to broker geometry instead of container-fitting). The no-resize-
send assertion is unchanged.
- reconnect guard asserted toContain("connect()") which was a FALSE-POSITIVE
match on resizeObs.disconnect() (removed with the ResizeObserver). Corrected
to the real reconnect call toContain("connectForGeneration(gen)").
2. focused-terminal-reconnect.test.tsx mock Terminal - all 6 reconnect tests were
already failing at the branch tip cba0a182 (PRE-EXISTING on this branch, NOT
introduced by the uncommitted patch): the repaint-stabilize commit added
term.focus(), which the mock Terminal never stubbed, so component setup threw
and no WebSocket was created. (Verified by running reconnect against cba0a182's
FocusedTerminal: 6 failed.) The patch additionally calls term.scrollToBottom().
Fixed the harness: the mock now stubs focus() + scrollToBottom() to match the
real xterm Terminal interface the component uses. All 6 reconnect tests green.
Verified: focused-terminal-lifecycle 6/6, terminal-styling-polish 4/4,
focused-terminal-reconnect 6/6, ui tsc clean. NOTE for orch-advisor/guard: the
reconnect break was latent in the pre-cap-guard-cleared branch commits; flagging
for awareness.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.4.0.38 forward-fix. Authored + verified live in the dev1-qa dogfood loop;
committed by dev2-driver (the Codex authoring seat is usage-capped). This is the
exact dogfood-verified state.
- Remove FitAddon (+ its ResizeObserver/fit() container-fit): the live xterm no
longer resizes to the container. Instead the terminal is created at the broker
canonical geometry cols=120 rows=40 lineHeight=1, and the container scrolls/pans
(overflow-auto) on smaller surfaces. This is the cleaner realization of the
OPR.0.4.0.38 FR-7 fixed-geometry contract: the client mirrors the broker pane
exactly instead of fitting (which mismatched wrap when the container was narrower).
- scrollTerminalViewportToPrompt(): on activation (and for a 2.5s settle window as
seed/output lands) scroll the viewport so the cursor/prompt row is visible -
finds textarea.xterm-helper-textarea, computes the cursor row, clamps to
[0, maxScrollTop]. This is the founder-verified "scrollback is fixed" behavior.
- Live xterm render surface stays OPAQUE (#0c0a09, from the prior repaint-stabilize
commit) so erase/redraw is cursor-safe. NOTE for OPR.0.4.0.39: the opaque
#0c0a09 live look is the new static mirror-target.
terminal-styling-polish.test.tsx updated to assert the new source (120x40 pin,
lineHeight 1, scroll-to-prompt helpers, FitAddon removed). LOCAL only, no push.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.4.0.38 commit 8/N. Forward-fix for dev1-guard round-3 BLOCKING
(qitem-20260621231300-930ef3b7). After open succeeded, attach awaited seed(sub)
then unconditionally added the subscriber - no torndown recheck after the async
seed. If liveness/session-death (or dispose) tore the broker down while the
capturePaneScreen capture was pending, handleSessionDeath closed the existing
subscribers and cleared the set, but the resuming attach then added the new
subscriber to a torn-down broker without sending 1001/1011 - a live-looking
terminal with no stream/input path (no-live-terminal-lies violation). Guard's
no-edit repro: late attach blocked in capturePaneScreen, flip hasSession=false,
let liveness fire, release capture -> aClosed=[1001], bClosed=[], subscriberCount=1.
Fix (guard's recipe): remember a terminal close reason on every teardown path -
lastClose = {1001,"tmux session terminated"} on session death, {1011,"terminal
broker unavailable"} on dispose / last-detach, and the shared open-failure reason
on open failure. attach now rechecks torndown AFTER await seed (and the existing
after-await-open recheck) and, if torn down, closes the subscriber with the
remembered reason via closeTorndown() WITHOUT adding it. So any attach resuming
after teardown closes honestly and never goes silently live.
TDD: regression added (late attach blocked in seed, liveness fires session death,
seed then resolves -> the late subscriber closes 1001, subscriberCount stays 0,
broker evicts once) - verified RED against the prior commit (b.closed undefined,
subscriberCount 1) then green. Full terminal suite 123 green (33 broker + 9 route
+ 81 adapter); prior open-failure / concurrent-success / AC-5 ring tests stay
green; daemon tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.4.0.38 commit 7/N. Forward-fix for dev1-guard re-review BLOCKING
(qitem-20260621230728-711fa267). The bare `started` boolean singleflight closed
the success case (no double pipe) but not the FAILURE case: a concurrent later
attach saw started=true, took the no-new-pipe path, ran seed, and added itself
to subscribers BEFORE the first open result was known. If openPipe then returned
not-ok, only the FIRST subscriber was closed and the broker was evicted - the
later subscriber stayed in subscribers, got NO close, its WebSocket stayed open,
and broker.input no-oped (torndown). A live-looking subscriber on a dead broker
violates the no-live-terminal-lies invariant. Guard's no-edit repro: two
Promise.all attaches with hasSession delayed-then-false gave aClosed=[1008],
bClosed=[], subscriberCount=1.
Fix: replace the boolean with a shared open PROMISE. The first attach starts the
open once; EVERY concurrent attach awaits the SAME result before it seeds/adds.
On failure the broker tears down once and each waiter closes its own subscriber
with the same honest 1008 (session absent) / 1011 (pipe failure) reason - none
left live; subscriberCount 0; registry evicts once; no temp leak. On success
each subscriber seeds/adds and the single tail + liveness start exactly once
(tailStarted guard). The concurrent-success single-pipe test and the AC-5 ring
tests stay green.
TDD: 3 regressions added (concurrent dead-session -> all close 1008 + evict-once;
concurrent pipe-start failure -> all close 1011 + no temp leak; registry
concurrent dead-session -> all close + size 0). Verified RED-then-green by
stashing the fix: all 3 FAIL against the pre-fix started-boolean code, PASS with
the open-promise. Full terminal suite 122 green (32 broker + 9 route + 81
adapter); daemon tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.4.0.38 commit 6/N. Forward-fix for dev1-guard code-review BLOCKING
(qitem-20260621225626-d5bd136e). The impl-prd (sole authority) requires a
broker-owned SHARED history/ring (TL;DR, FR-4 lines 66-67, AC-5 lines 189-192):
history is session-level, not per-xterm local. The initial build had only the
visible-screen seed, so a late subscriber got just its own capturePaneScreen +
future bytes and could not see the recent output that scrolled off the first
subscriber - the exact AC-5 discriminator.
Fix: a bounded broker-owned recent-output ring (default 64KB, oldest dropped
past the cap), fed from the SAME single tail that fans out. On attach the ring
is replayed to the new subscriber FIRST (raw bytes write into xterm scrollback),
THEN the visible-screen seed paints a cursor-correct current screen on top -
screenSnapshotEscape opens with ESC[2J which clears only the viewport, not the
scrollback, so the scrolled-off history is preserved above (never ESC[3J). The
first subscriber's ring is empty, so it gets only the visible seed. No second
pipe, no resize dependency; seed/no-row-drift and all cleanup guarantees kept.
Ring cleared on every teardown path (final detach / pipe failure / session death).
TDD: 3 AC-5 tests written RED first - a late subscriber receives the broker-owned
history that scrolled off the first subscriber (startPipePane still once); the
ring stays bounded under sustained output; the ring is cleared on final detach.
Full terminal suite 119 green (29 broker + 9 route + 81 adapter); daemon tsc clean.
dev1-driver (plan author) confirmed this was an under-specification in the build
plan, NOT a scope-out - no deferral; in-scope and required. Visual scrollback
coherence across surfaces remains AC-9 (dev1-qa) to confirm.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.4.0.38 commit 5/N. The client half of FR-7 fixed geometry. FocusedTerminal
no longer sends {type:"resize"} to the daemon - neither on ws open nor on the
xterm onResize handler (both relays removed). The daemon broker owns the fixed
canonical 120x40 pane; a client-driven resize would shrink the shared pane for
every other viewer of the same seat. FitAddon still fits the CONTAINER (and the
ResizeObserver still calls fit) so the viewport scrolls/pans - it just never
asks the pane to resize. The WS reconnect / close-code (1008/1011/1001
definitive) logic is untouched.
Source-guard test added asserting no type:"resize" send remains and FitAddon is
retained. ui tsc --noEmit clean; full ui suite green except 5 pre-existing
inherited failures (library-page, live-node-details HG-3a,
mission-progress-heatmap x2, rig-graph PL-019) confirmed unrelated to this slice.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.4.0.38 commit 4/N. Lifecycle hardening from dev1-guard's pre-handoff
review of the costly subtle-bug class:
- detach-during-attach race (route): the WebSocket can close WHILE the async
broker attach is in flight, when the broker ref is still null - onClose would
then skip detach and leak a phantom subscriber + an open pipe once attach
resolved. Fixed with a route-level closed flag: onClose marks closed, and
onOpen re-detaches after attach resolves if the socket already went away
(detach is idempotent). Proven by a route test that closes the WS mid-attach
(slow startPipePane) and asserts the pipe is stopped and the temp deleted.
- fanout isolation (broker): a subscriber whose send throws is now detached
cleanly (collected then removed after the loop, never mutating the set
mid-iteration) instead of just swallowed, so a dead socket cannot wedge the
fanout.
New tests (broker + route): concurrent first-attach singleflight (delayed
startPipePane widens the real race - one pipe for three racing attaches);
fanout isolation detaches the thrower while others keep streaming; last-detach
unlinks a NON-EMPTY pipe file (AC-7 no temp leak); registry concurrent attach
shares one broker; attach-after-final-close yields a FRESH broker. Full
terminal suite green (26 broker + 9 route); daemon tsc --noEmit clean.
Temp-file scope (guard ruling): no rotation/persistence in 38 - the broker
keeps NO in-memory ring (fanout is immediate; only a transient 64KB-capped read
buffer), and every teardown path unlinks the temp + clears timers. A long-lived
active-viewer broker growing its temp file is acceptable 0.4.0 behavior (the
broker exists only while subscribers do); rotation is a 0.4.1 follow-up if it
ever threatens cleanup, not a 38 blocker.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.4.0.38 commit 3/N. terminal-ws becomes a thin subscriber around the
TerminalSessionBroker registry instead of opening its own per-connection
pipe-pane:
- FR-8: one daemon-owned TerminalBrokerRegistry shared across all connections
(lazily built from the first connection's tmux singleton). onOpen adapts the
WebSocket to a broker subscriber and attaches; onClose detaches. The broker
owns pipe/seed/fanout/honest-death/cleanup. No per-connection outputPath,
startPipePane, tail, or liveness loop remains in the route.
- FR-7: the onMessage `resize` branch is DROPPED. keys/text still forward (via
broker.input); a client resize is ignored - the broker owns fixed geometry,
and the old aggressive-resize-on call is gone with the route rewrite.
Broker integration refinements (folded in here as the route-parity step):
- openPipe carries a close code so the subscriber gets 1008 (session genuinely
absent, mirroring the pre-broker route) vs 1011 (pipe/file failure) - the UI
groups both as definitive, so no regression, but the honest distinction is
preserved.
- seed() is now try/catch-robust: a failed capture (or an adapter lacking the
seed methods) never breaks attach - the tail still streams.
- input() no-ops on a torn-down broker (a failed-attach broker must not forward
keystrokes).
TDD: 2 new route tests written RED first against the old per-connection route -
two subscribers of one session share ONE pipe-pane and both receive fanned-out
bytes; a client resize message never resizes the pane (only the one canonical
120x40 geometry call at open). Now green. The 6 pre-existing route tests
(auth 401/403/non-404, input ordering, session-death 1001 + stopPipePane) still
pass against the rewritten route - FR-9 no regression. daemon tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.4.0.38 commit 2/N. The daemon-side broker that fixes the per-connection
pipe bug: ONE tmux pipe-pane per session, MANY WebSocket subscribers, output
fanned out to all. Founder invariant: no live terminal lies.
Broker (TerminalSessionBroker):
- FR-1: first subscriber stands up the single pipe; later subscribers reuse it
(synchronous started-guard prevents a double-pipe even under concurrent attach).
- FR-2: file-tail loop fans new bytes to every subscriber (64KB/read cap preserves
bytes across ticks).
- FR-3: input forwarded to tmux sendKeys/sendText, serialized so rapid input keeps
order. NO client-driven resize message exists (FR-7).
- FR-4: seed-on-attach BEFORE joining the fanout, via the cursor-safe visible-screen
paint (capturePaneScreen + getPaneCursorPosition -> screenSnapshotEscape, absolute
row moves, no drift). Each subscriber gets its own seed; no resize dependency.
- FR-5: session death closes ALL subscribers honestly (1001) - never silent stale-live.
- FR-6: broker survives while any subscriber remains.
- FR-7: fixed geometry set ONCE at open - window-size manual (so tmux won't shrink to
the smallest client) + canonical 120x40, NOT aggressive-resize.
- last-subscriber detach + pipe-failure both stop the pipe and delete the temp file
(no leak), and evict the broker via onEmpty.
Registry (TerminalBrokerRegistry): get-or-create per session, evict on empty.
TDD: 21 tests written RED first (module absent), now green - fanout, no-double-pipe,
input-forward/serialize, detach-keeps-alive, final-cleanup, honest session-death,
fixed-geometry, seed-on-attach, cursor-safe-no-drift (pure-fn discriminators),
pipe-failure-no-leak, dead-session-at-open, registry create-if-absent + eviction.
daemon tsc --noEmit clean.
Geometry decision (drop aggressive-resize, window-size manual + 120x40) confirmed
with dev1-driver as the epic live reference; rows=40 is the reasoned canonical
value (styling slice pinned width only), to be visually confirmed by AC-9.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OPR.0.4.0.38 (real-terminal session broker), commit 1/N. Lift the net-new
cursor-safe seed primitives that were on-disk-only (no branch) in the
opr-0.4.0.1-ff-interaction-model worktree, so the broker can build against
committed code:
- TmuxCursorPosition interface ({x,y,width,height}, zero-based coords).
- capturePaneScreen(paneId): `tmux capture-pane -p` of the VISIBLE screen,
NOT `-S -<lines>` scrollback (scrollback reintroduces the row drift the
absolute-paint seed eliminates). Returns null on error/empty.
- getPaneCursorPosition(paneId): tab-delimited display-message read of
cursor_x/cursor_y/pane_width/pane_height; returns null on non-finite or
out-of-range (x<0,y<0,width<1,height<1) so a bad read never seeds garbage.
TDD: 9 adapter tests written RED first (methods absent), now green; full
tmux-adapter suite 81 passing; daemon tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
rev1-r2 BLOCKING (orch source-verified): the popover SHELL was a fixed compact
w-[calc(80ch+24px)] + overflow-hidden, so the FR-3 wide live sizer (w-[880px])
was CLIPPED on all popover surfaces (graph/table/grid-expand-out) -- the
TUI-optimal LIVE width was not actually visible. r2 correct (verified at source).
Fix (mode-aware, honors "static compact / live wide"): ProgressiveTerminal gains
an onLiveChange(isLive) callback (fired on its static<->live mode flips). The
popover tracks liveInside and widens its shell to w-[904px] (fits the 880px plate
+ padding) when live -- or when non-progressive/feed-card (always live) -- and
stays compact w-[calc(80ch+24px)] for the static preview. The inner sizer is
w-[880px] when live, w-full when static. The existing ResizeObserver recomputes
the popover position when the shell width changes.
Tests: terminal-popover-multilive (new) asserts the shell is compact static then
widens to w-[904px] on going live; p5-3 updated (live popover shell = w-[904px],
not the compact static width); terminal-styling-polish source-locks both widths.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
dev1-guard code-review BLOCKING (qitem-20260621085542-206d687a): the FR-5 grid
thumbnail rendered a raw compact SessionPreviewPane (transparent) directly, so on
the truly-bare grid surface the static thumbnail had NO smoke -- the smoked plate
lived only in ProgressiveTerminal's static button, which the grid no longer uses.
FR-1/FR-4/AC-1 require static content to carry smoked glass on the bare grid. Valid
catch; my test missed it (it rendered ProgressiveTerminal + only source-checked the
grid imports).
Fix: extracted the borderless smoked-glass plate into a shared exported constant
SMOKED_STATIC_PLATE_CLASS (ProgressiveTerminal.tsx) used by BOTH ProgressiveTerminal's
static view AND the grid thumbnail wrapper, so the grid static thumbnail carries the
SAME smoke without restoring live-in-place. The FR-5 expand-out model is intact
(thumbnail + one TerminalPreviewPopover trigger; no in-place ProgressiveTerminal).
Regression (p5-topology-terminal.test.tsx): renders TopologyTerminalView and asserts
the grid thumbnail wrapper carries the smoked-glass plate (bg-stone-950/60
backdrop-blur) while keeping exactly one popover trigger + no in-place static.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Founder styling polish on the merged interactive terminal (sole contract:
impl-prd-terminal-styling-forward-fix.md).
FR-1 (smoked-glass CONTENT, shared layer): FocusedTerminal theme.background
rgba(0,0,0,0) -> rgba(12,10,9,0.6) (stone-950 ~0.6 alpha) so the LIVE content
self-tints as floating smoked-glass on EVERY surface incl. the truly-bare
topology-tab + grid; foreground stays opaque (#e0e0e0) for legibility (AC-4).
ProgressiveTerminal static view gets its own borderless smoked-glass plate
(bg-stone-950/60 backdrop-blur) + variant="compact-terminal" so the static
preview is borderless + transparent and self-tints on bare surfaces too. One
tint per mode -> no double-tint.
FR-2 (borderless): the compact-terminal variant (border-0) + the smoked plate
(no border) make a floating plate, not a bordered box.
FR-3 (measured LIVE width): the popover live sizer 820px -> 880px (~120 cols at
xterm fontSize 12 ui-monospace; the wider of Claude Code + Codex). Computed
starting width; QA's fix-loop finalizes against a real agent TUI. LIVE-only --
static grid columns are NOT widened.
FR-4 (de-dup): the popover drops its now-redundant opaque smoked bg (content
self-tints) so it stays transparent-glassy, not double-tinted; backdrop-blur kept.
FR-5 (grid reconciliation, pinned expand-OUT): TopologyTerminalView SeatTerminalCard
changes from live-in-place ProgressiveTerminal to a SMALL static thumbnail + the
shared TerminalPreviewPopover trigger (the graph/table primitive) -> trigger opens
the wide popover, click-inside goes live. The 3-col overview stays compact; the
live-in-place model is removed; cap=2 binds on the inside go-live. No new primitive.
Tests: terminal-styling-polish.test.tsx (static smoked-glass+borderless+compact
variant; live theme smoke; popover measured width + bg dropped; grid mounts the
popover trigger not in-place); updated p5-3 (popover bg dropped) + p5-topology
a11y (one popover trigger per card, in-place removed). QA owns the captioned
before/after + visual alpha/width finalization.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Completeness forward-fix (slice-01 "one reusable terminal everywhere a terminal
appears"). The project/scope Topology tab still rendered a raw static
SessionPreviewPane per seat (no click-to-interact) on a SEPARATE module-singleton
fallback registry.
FR-1: TopologyTab SeatRow now renders the shared ProgressiveTerminal
(default-static -> click-inside-to-go-live FocusedTerminal) with a session-scoped
terminalKey topology-tab:<session> and the same testIdPrefix.
FR-2 (load-bearing): project/ScopePages ScopeShell now mounts ONE explicit
LiveTerminalProvider (cap from useTerminalCap), mirroring topology/ScopePages, so
the Topology-tab terminal and the page's other terminals (HostMultiRigGraph) share
ONE global registry + configured cap instead of the split fallback. Grounded: the
two ScopeShell definitions are file-local (no double-wrap of topology's provider).
Tests (topology-tab-progressive.test.tsx): AC-1 default-static -> click-to-live on
the Topology tab; AC-2 the topology-tab terminal shares the global cap (cap=2, a
3rd live across surfaces evicts the oldest topology-tab terminal to static); FR-2
source guard the project ScopePages mounts the shared provider.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Test-only. The slice's progressive + multi-live changes made the
p5-3-activity-ring-hot-potato "hybrid terminal hover ... popover" test stale
(VERIFIED: it PASSES on base c8399f6d, FAILS on the candidate -> INTRODUCED by
this slice, not pre-existing). It asserted (a) an immediate always-live
FocusedTerminal and (b) single-open (opening the guard popover closes the driver
popover). Correct product behavior is now default-static -> click-to-go-live, and
progressive popovers COEXIST under the global cap. Updated the test to click
static -> go-live before asserting the live terminal, and to assert both popovers
stay open (multi-live). No production change.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
rev1-r2 BLOCKING (qitem-20260621034537-bfe20f87): the graph/table progressive
popovers still used the single-open TERMINAL_PREVIEW_EVENT, which force-closes
every sibling popover when one opens -- so the popover surfaces could never hold
2 live terminals, making AC-4 (cap=2 multi-live, watch-A-type-in-B) unreachable
there (only the topology grid in-place path reached it). Confirmed at source.
PATH A (resolves the r1/r2 split toward the locked arch "one reusable component
across all 3 surfaces" + "global cap across graph+table+topology"): progressive
popovers now open via LOCAL state and do NOT participate in the single-open
event, so multiple coexist and the global LiveTerminalRegistry cap bounds the
live count (oldest-evicts-to-static). A progressive popover also no longer
dismisses when a pointerdown lands inside another terminal-preview surface
(data-terminal-preview-surface marker), so interacting with B does not close A.
The non-progressive feed-card drill keeps its one-overlay single-open behavior
unchanged.
Test: terminal-popover-multilive.test.tsx -- two progressive popovers stay live
at once; a third evicts the oldest to static under cap=2 (red-first verified).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
QA round-two ruled the driver-flagged item (qitem-20260621031733-9390412e):
the node-detail Overview inline terminal (LiveNodeDetails InlineTerminal) must
join the reusable default-static -> click-inside-to-go-live model under the
global live-terminal cap, not stay an always-live uncapped FocusedTerminal. The
forward-fix spec requires the reusable pattern everywhere a terminal appears and
bounds the live-terminal count, so the uncapped always-live node-detail terminal
was a real hole.
InlineTerminal now renders ProgressiveTerminal (default-static SessionPreviewPane
-> click-inside-to-go-live FocusedTerminal) with a session-scoped terminalKey;
the module-singleton fallback registry keeps the cap global on this surface (it
is not under the topology scope-page provider). Black-glass shell wrapper kept.
Tests: HG-5 updated to assert default-static (static trigger present, no
immediate live FocusedTerminal) -- reproduced red-first, then green. polish-8
source guard updated FocusedTerminal -> ProgressiveTerminal.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
QA round-two BLOCKING (qitem-20260621025642-b4c89c72): the rig-scope graph
node popover (RigNode -> TerminalPreviewPopover) still opened an immediate
always-live FocusedTerminal because the call-site omitted the progressive
prop. Pass progressive so it joins the default-static -> click-to-go-live
ProgressiveTerminal model under the global live-terminal cap. RigGraph
already renders inside the LiveTerminalProvider in the topology scope pages,
and the module-singleton fallback keeps the cap global on the routes.tsx
mount.
Adds rig-node-progressive.test.tsx: opening the RigNode popover renders the
static SessionPreviewPane, not an immediate live terminal (reproduced the QA
finding as a failing test first, then fixed).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The topology terminal grid is now where the global cap binds: SeatTerminalCard's
body IS a ProgressiveTerminal (default-static -> click-to-live IN PLACE), so >=2
cards can be live at once and opening past MAX_LIVE_TERMINALS evicts the oldest
card back to static (AC-4 "watch A while typing in B", reachable in-product).
Dropped the card's full-overlay popover-trigger + the grid's single-open
TERMINAL_PREVIEW_EVENT; kept the pulsing-ring active treatment. Updated the
p5-topology-terminal a11y test to the new single-trigger (the static-mode button).
Scoped the popover's progressive mode to an opt-in prop: graph (HybridTopologyNodes)
and table (TopologyTableView) opt in (progressive); the feed-card live-drill keeps
the always-live FocusedTerminal by default (it is out of this slice's 3-surface
scope and has its own live-drill AC) -- restores feed-card-terminal-drill tests.
ui tsc clean; feed-card + topology + terminal suites 29/29.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
TerminalPreviewPopover now renders ProgressiveTerminal (default-static ->
click-to-live) instead of the always-live FocusedTerminal, keyed by the popover's
rigId:logicalId, and at a wider optimal terminal width (the styling finding). One
LiveTerminalProvider is mounted per scope page (HostScopePage + RigScopePage),
cap from useTerminalCap(), so the global cap bounds live terminals across the
page's graph + table + terminal tab-surfaces. ui tsc clean; 78 ui terminal/topology
tests pass.
Remaining (handoff-able): the topology-grid in-place multi-live (cards render
ProgressiveTerminal directly so >=2 can be live and the cap eviction is reachable
in-product) + loosening the single-open popover CustomEvent, plus the end-to-end
integration test. The cap/eviction LOGIC is already proven by the registry (8) +
ProgressiveTerminal (4) tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The cap is config-backed in one place (AC-5: 2 -> 3 is a config edit, not a
rewrite). Daemon settings-store: add ui.terminal.max_live_terminals to the
allowlist + env map (OPENRIG_UI_TERMINAL_MAX_LIVE_TERMINALS) + config path
(ui.terminal.maxLiveTerminals), mirroring the ui.preview.* keys. UI: add it to the
SettingsKey union + a useTerminalCap() hook that reads the setting and falls back
to the MAX_LIVE_TERMINALS default (2). Updated the daemon documented-key-set +
config-count tests (39 -> 40).
daemon settings tests 71/71; daemon + ui tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The styling-finding fix folded into the same component pass: the interactive
FocusedTerminal rendered as an OPAQUE dark panel (#1a1a1a), losing the preview-era
"floating smoked-glass plate" aesthetic. Use a transparent xterm theme.background
(rgba(0,0,0,0)) + allowTransparency so the popover's bg-stone-950/65 backdrop-blur
shows through, and drop the opaque bg-[#1a1a1a] container classes (live + error
states) so the terminal floats on the glass with no opaque panel. Width is tuned
at the popover wrap during surface wiring.
Note: the real-xterm focused-terminal-lifecycle test is env-blocked in this
worktree (PostCSS cannot resolve tailwindcss -> xterm.css import fails); the
pure/mocked terminal tests (registry 8, ProgressiveTerminal 4) pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The user-facing behavior layer over the cap registry. LiveTerminalProvider wraps
ONE global LiveTerminalRegistry (cap from config, MAX_LIVE_TERMINALS default) and
is mounted above all surfaces so the cap is global; useLiveTerminal falls back to
a module singleton when no provider is present. ProgressiveTerminal is the one
reusable component (AC-3): default-STATIC SessionPreviewPane, a click upgrades it
to a LIVE FocusedTerminal (AC-1/AC-2); going live consults the global cap and
evicts the OLDEST live terminal back to static (AC-4); the cap is config-driven
(AC-5). Static previews are uncapped.
4 behavior tests (mocked xterm/preview children): static-default, click-to-live,
cap=2 oldest-eviction, cap=3 config. Composes with the 8 registry unit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The PM-locked architectural core of the real-terminals progressive-live interaction
model: a framework-free global cap registry. Total simultaneously-live terminals
across all surfaces is bounded by MAX_LIVE_TERMINALS (default 2); requesting a live
terminal past the cap evicts the OLDEST live one, running its revert-to-static
callback (close WS + render static preview, no leak). Re-requesting a live key
refreshes recency; release() frees a slot without eviction; a bad cap floors at 1.
8 unit tests cover admit-to-cap, oldest-eviction, strict insertion order, recency
refresh, release/idempotency, freed-slot reuse, cap clamp, and the 2->3 cap bump.
The default constant lives in one place; the daemon-backed ui.terminal.max_live_terminals
config key (integration phase) overrides it. React provider + ProgressiveTerminal
component + 3-surface wiring + styling fix follow in the integration phase.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
runCrossHostPs reconstructed the remote argv without the new breadth flag, so
`rig ps --host h -A --nodes` was delegated as `rig ps --nodes` -- dropping
all-rigs breadth remotely (rev1-r2 finding). Forward opts.allRigs as --all-rigs
and the normalized opts.active (set by --active OR --running) as --active. The
current-rig default stays local-only and is never applied to a remote call, so an
explicit -A is the only way breadth reaches the remote -- now preserved. Adds
cross-host argv regressions asserting -A and --active reach the remote argv.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
compactNodeProjection emitted an underspecified subset; PRD FR-4 requires the
orch field set. Add the source-available fields the daemon node-inventory already
exposes: rigId, logicalId, sessionStatus, startupStatus, a lastActivity age proxy
(agentActivity.sampledAt -> startupCompletedAt -> null, a documented fallback, not
a fabricated timestamp), and heldReason (short held/attention reason) when present.
The resumeToken value + resumeCommand stay excluded from compact (security);
--full is unchanged. Declares resumeType/resumeToken/startupCompletedAt on
NodeEntry. Adds a regression asserting compact key coverage + secret absence.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Guard BLOCKING: help text still described the old all-rigs default.
Updated help to document current-rig scope from OPENRIG_SESSION_NAME,
-A/--all-rigs for fleet breadth, all-states intentional, resume token
security in compact output, and --running alias. Option descriptions
updated. Test assertion updated for current-rig help wording.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Default rig ps now scopes to the caller's rig (derived from
OPENRIG_SESSION_NAME @rig suffix). -A/--all-rigs restores fleet
breadth. All states preserved (no active-default; non-running IS
the actionable signal for ps). --running added as alias for --active.
Compact projection gains resumeType + resumeTokenPresent boolean
(never the token value or resumeCommand). Remote --host paths skip
the local rig default. Existing test suites save/restore
OPENRIG_SESSION_NAME to avoid env contamination.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
OPR.0.4.0.35 rig scope stage/verified/reconcile verbs were restored to 0.4.0 mid-wrap. This commit adds the lifecycle coverage that the original wrap-gate (AC-1/AC-3) didn't include because slice 35 was out of scope at gate-author-time. Updates docs/releases/v0.4.0.md (Scope automation section + Known Limitations correction), CHANGELOG.md [0.4.0] (New Top-Level CLI Verbs + Known Limitations), docs/as-built/cli-reference.md (rig scope slice/mission stage/verified/reconcile subcommands; last-verified-against-source bumped to 51554eee), and the openrig-user SKILL.md mirror with the new verbs taught + STOP-hand-editing-frontmatter guidance. SKILL.md cascade propagated byte-identical to substrate canonical + hub .claude + hub .agents (md5 d08ec07956658be854bc814481b01a8d).
Brings origin/main (Release 0.4.0 wrap-gate content: release notes, CHANGELOG, cli-reference, openrig-user SKILL.md cascade, package.json bumps) together with local main (privacy-scrub + author-link restore + slice 35 rig scope stage/verified/reconcile). No content authored or changed by this merge — only the union of both branches.
Adds the deterministic maturity-vocab verbs the scope-and-versioning convention
section 2 names but rig scope lacked, so agents stop hand-editing and drifting:
- stage <target> <new-stage>: enum-validated (wip|provisional|established|
canonical|superseded|retired); superseded requires --successor; retired warns.
Surgical updateFrontmatter; findSlice/findMission (path/name) resolution.
- verified <target> --against <source>: provenance mandatory (no bare timestamp);
stamps verified: <today> against <source>, overwriting the prior line.
- repair: EXTENDED to also conform mandatory frontmatter (mint+register a missing
id via the sec.1 lazy parent adoption, add a stage from the sec.4 migration map
/ wip, add a verified placeholder), idempotent + non-clobbering -- per
consolidate-don't-invent, NOT a parallel reconcile verb.
- show: derives read-time trust from (stage x verified) and NEVER stores it -- an
established/canonical with stale/absent/bare/placeholder verified is shown as
effectively provisional. The 90-day window + bare/stale taxonomy MIRROR
packages/daemon/src/domain/skill-audit.ts:34 (the daemon is not importable
across the package boundary, so we mirror rather than cross-import).
FR-3 (create writes id+stage+verified) already shipped by slice 33 -> locked with
a regression test instead of re-implemented. Help teaches the enforcement rules.
New lib/scope/trust.ts (13 unit tests) + 24 integration tests across stage/
verified/repair/show/create/help. cli tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The default-compact AC-4 fix computed a ready-seat startup-context caveat into the
rig rollup but omitted it from the emitted checks, so the top-level verdict /
readiness / counts (derived from emitted checks only) could report a false
restorable/ready while a rig rollup was ready_with_caveats. Carry the
computed-but-omitted ready-seat caveats in a deferred-assessment set that feeds the
top-level counts and verdict, while the emitted checks and repairPacket/recovery
stay token-safe (no ready-detail rows). Default compact now reports
restorable_with_caveats / ready_with_caveats for a hidden caveat.
Adds a no-false-ready regression: clean host + green host-infra + one running/ready
seat with a missing startup context -> default compact verdict
restorable_with_caveats, readiness ready_with_caveats, caveatRigCount 1,
classCounts.ready_with_caveats 1, ready 0, and no emitted startup-context row.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The prior default-compact fix computed the full per-seat detail set
(transcript/resume/queue/hooks) and only suppressed its emission, regressing the
FR-3/AC-4 compute-skip contract. Restore the genuine skip: in default compact a
ready/green seat computes ONLY checkStartupContext (the restore-readiness caveat
signal the FR-8 summary needs), feeds it to the rig rollup, and continues --
transcript/resume/queue/hooks are not probed. Detail is still fully assembled for
full, --ready/includeReady, and non-ready seats. Token-safe compact output is
unchanged.
Adds a behavioral no-call regression: a green ready seat with a clean startup
context but no attach command stays plain ready in default compact (resume-path
not assembled) and becomes ready_with_caveats under --ready.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
rig whoami --json is the every-boot + every-compaction-restore identity call;
its --json path dumped the full ~909-token daemon payload. Make the default
compact via an identity-recovery ALLOWLIST (resolvedBy, identity core, peers
with sessionName, peersNote, edges, transcript path) and add --full/--verbose
for today's complete payload. The daemon skips the contextUsageStore lookup and
the runtimeContext build when compact (?compact=1); a direct /api/whoami with
no param stays full for API back-compat. Cross-host fan-out is untouched.
Tests: cli compact allowlist + --full parity + compact-human-omits-Context +
help (24); daemon AC-4 no-call (contextUsageStore.getForNode not called when
compact, called on full) + back-compat. tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Default compact skipped per-seat detail assembly entirely for ready seats, so
a running/ready seat with a real yellow startup-context caveat never reached
the rig rollup -- the per-rig status and classCounts reported it as ready
instead of ready_with_caveats, disagreeing with caveatNodes. Separate summary
classification from emitted detail: compute per-seat caveat/blocking signals
for every seat and feed them to the rollup (rigChecks), but emit ready-seat
detail to the top-level checks only when not skipping (full, --ready, or a
non-ready seat). Token-safe compact output is unchanged.
Adds a default-compact check({compact:true}) regression and makes the
multi-class test's ready seats genuinely clean so the plain-ready class keeps
real coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
FR-8/AC-7 class derivation checked runningReady before hasCaveat, so a
snapshot-backed running/ready seat with a real yellow startup-context
caveat was miscounted as ready while the per-rig status and caveatNodes
correctly reported ready_with_caveats. Move the hasCaveat branch ahead of
runningReady; attention_required, not_ready, and no-snapshot precedence
are unchanged. Adds a focused regression for the restore-proof-caveat case.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Guard FR-8 re-review BLOCKING: the no-snapshot precedence hid failed/not_ready
seats - a red (failed/down) seat in a no-snapshot rig was counted as unknown
because rigNoSnapshot was checked before hasBlocking.
Fix: reorder the class derivation to attention_required -> not_ready
(hasBlocking) -> unknown (no-snapshot) -> ready (runningReady) ->
ready_with_caveats (hasCaveat) -> unknown. attention/not_ready win first so a
failed/attention seat is surfaced even in a no-snapshot rig; no-snapshot then
overrides only ready/caveat (a clean seat in an unrestorable rig is unknown) -
exactly the prior recipe + the ACK.
Test: AC-7 asserts a no-snapshot rig with one ready + one failed seat counts as
1 not_ready + 1 unknown + 0 ready (the failed seat is not hidden). daemon
120/120 restore-check; daemon tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Guard FR-8 re-review BLOCKING: the unknown/no-snapshot class was not derived
from the real snapshot primitive - a running/ready seat in a rig with no
snapshot still counted as ready.
Fix: buildRigRollup now detects the rig-level no-snapshot signal (the real
`rig.<name>.snapshot` yellow check from checkSnapshot when hasSnapshot is
false / could-not-check) and classifies that rig's seats as unknown (a rig with
no snapshot cannot be restored, so its seats' restore-readiness is unknown, not
ready). Precedence: attention_required (startupStatus) wins first, then
no-snapshot -> unknown, then ready/not_ready/ready_with_caveats. With a snapshot
present the behavior is unchanged.
Test: AC-7 asserts a no-snapshot rig's two running/ready seats count as 0 ready
+ 2 unknown. daemon 119/119 restore-check; daemon tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Guard code-review BLOCKING: the corrective --ready fix was clean, but FR-8/AC-7
(the 5-real-enum ready-confidence class breakdown) was never implemented (the
candidate only shipped per-rig counts).
Add a ReadinessClassCounts breakdown derived from the REAL primitives (no
invented status):
- service: buildRigRollup computes per-seat class counts in its existing node
loop (attention_required from node.startupStatus; ready from running/ready;
not_ready/ready_with_caveats from the seat's red/yellow checks; unknown
remainder). RestoreCheckResult gains a fleet-wide classCounts = sum of the
per-rig classCounts; RigRestoreRollup carries its own classCounts.
- route: the compact projection exposes classCounts in JSON.
- cli: printCompact renders a CLASSES line
(N ready | N ready_with_caveats | N not_ready | N attention_required | N unknown).
The fresh-primed/awaiting-decision SPLIT of ready_with_caveats stays the
deferred/escalated follow-on (not emitted).
Tests: AC-7 asserts the 5 exact class keys (no invented status), correct counts
for a ready/ready/not_ready/attention fixture, the breakdown sums to the seat
total, and per-rig classCounts sum to the fleet total; cli asserts the CLASSES
line renders. daemon 118/118 + cli 26/26 restore-check; daemon+cli tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Corrective forward-fix for the bridged dropped handoff (the prior fix 06ae4284
got finding-1's --ready semantics wrong).
Finding 1 (CORRECTED): --ready was made to DROP compact -> the full firehose.
Per FR-2 it must stay COMPACT and ADD ready-seat detail "in addition to the
always-shown not-ready rows". Fix: --ready now sends compact=1 AND ready=1
(cli); the route reads ready -> includeReady; the service skips ready-seat
detail in compact ONLY when not includeReady; the route keeps green checks
when ready=1; printCompact renders exactly the checks the route scoped. Added
a cli regression that FAILS if --ready is a no-op or drops compact (asserts the
request carries compact=1 + ready=1; --full drops compact).
Finding 2 (kept): the service-level compact skip of ready (green) seat detail
is preserved, now gated by !includeReady so --ready opts back in.
Finding 3 (production route): added a compact-vs-ready test in
restore-check-routes.test.ts (which exercises the real createApp route via
app.request) proving ?compact=1 drops green checks (e.g. daemon.reachable) for
the token win and ?compact=1&ready=1 keeps them.
5 cli human-output regressions (introduced by the candidate b4045ca7; base was
22/22) were test MIGRATIONS: those it('human output...') tests assert full
detail (CONTINUITY/Per-rig summary/host-infra/repair-step count) that moved
behind --full per FR-2 -> migrated their parseAsync to --full (AC-2 parity
preserves the content; printCompact intentionally omits them by default).
FR-8 class/rig counts + FR-9 per-rig table preserved from the candidate.
Rebased onto current main e10e3340. daemon 116/116 + cli 25/25 restore-check;
daemon+cli tsc clean. NO push/release/tag/host-upgrade.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Guard BLOCKING F1: --ready now skips compact param so ready-seat
detail is included. F2: RestoreCheckService.check() accepts compact
option and skips per-seat detail checks (startup context, transcript,
resume, queue, hooks) for green/ready seats. F3: tests rewritten to
exercise the real RestoreCheckService with injectable deps instead
of duplicating route projection logic. Compact mode produces fewer
checks than full mode, proving the skip-ready-detail assembly path.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Default rig restore-check now sends compact=1 to the daemon, which
returns per-rig readiness counts + only non-green checks (no ready-seat
detail). Compact summary shows per-rig grouped counts (ready/blocked/
caveats) + not-ready checks with remediation. --full restores today's
complete per-seat detail. --ready shows ready-seat info alongside
not-ready. --full --json is the only path to the full firehose.
Daemon API back-compat when compact param is absent. Readiness classes
derive from real enums (ready/ready_with_caveats/not_ready/unknown).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The operator-facing set path. Token is read from STDIN ONLY (--token-stdin) -
never a positional argv (which leaks via shell history + argv/ps) and no
--token-file branch this slice. The command carries terminalAuthHeaders() to
the guarded route and NEVER echoes the token in either output mode (the daemon
response is already redacted).
- --token-stdin required (refuses with exit 2 + guidance otherwise; the daemon
is not even contacted).
- --reason required (operator attestation for the audit event).
- Empty stdin -> exit 2.
- Daemon rejection (e.g. 422 malformed) surfaces the redacted route message
without echoing the token.
- Injectable readStdin seam for tests.
Tests (4): stdin->body+auth-header+no-echo; refuses without --token-stdin (no
daemon call); empty stdin; 422 rejection redacted. 14/14 seat CLI; cli tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
POST /api/sessions/:sessionName/resume-token: the managed/attested/audited set
path (host-upgrade de-risk gate), GUARDED by terminalAuthGuard (credential
write).
- SessionRegistry.findResumeContextByName: resolve a canonical session name to
{ sessionId, nodeId, rigId, runtime, currentProvenance }.
- Route: require --reason (operator attestation; 400 if absent), resolve seat
(404 if missing), validate the token per runtime (FR-2; 422 with a REDACTED
error that never quotes the token), updateResumeToken(..., "operator")
(FR-1 operator outranks hook/scrape), emit the append-only
session.resume_token_set audit event (FR-5) with NO raw token.
- The raw token arrives in the request BODY (the CLI reads it from stdin, never
argv) and is redacted from the response, error messages, and the event.
Tests (7): happy-path persist+redact+event; operator outranks hook; malformed
token to 422 redacted + not persisted; missing reason to 400; 404; unsupported
runtime (terminal) to 422; AC-6 auth (401 without the terminal bearer, 200 with
it). 78/78 across session-registry + sessions-routes + resume-token-*; daemon
tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Pure format-validator: resumeTypeForRuntime (claude-code->claude_id,
codex->codex_id, else null) + validateResumeToken (reject unsupported runtime,
empty, over-long, disallowed-charset; trim; never fabricate). Error messages
describe the FORMAT problem and NEVER echo the raw token (credential
redaction). 9/9 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
rev1-r2 re-review (candidate 98986a35) found the --full OPTION description
still said "Show complete per-node payload (default is compact summary)" -
still false after the list slimming (the after-text was fixed but the option
row was missed).
Fix: rewrote the --full option description to "Show all node-list fields per
node (uncompacted rows; recoveryGuidance/currentUsage live on the node detail,
not the list)". Extended the rendered-help regression test to also reject
"Show complete per-node payload".
ps.test 63/63 green; cli tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Foundation for the managed set-resume-token gate (FR-1a + FR-5).
FR-1a: widen SessionRegistry.updateResumeToken provenance to
hook|scrape|operator with a precedence rank (RESUME_PROVENANCE_RANK):
operator (deliberate attested set) outranks hook (runtime) outranks scrape
(pane). A lower-rank write never overwrites a higher-rank persisted token.
Preserves the pre-existing scrape-does-not-clobber-hook rule; adds operator as
the authoritative top rank. Back-compatible (no-provenance writes unchanged).
FR-5: define the append-only session.resume_token_set audit event on the
RigEvent union (rigId, nodeId, sessionName, sessionId, resumeType,
previousProvenance, newProvenance, source=operator_set|reconcile_capture,
reason, operator) - and NEVER the raw token (redacted: true marks the
intentional omission). Consumed by the upcoming route (FR-1b) and reconcile
capture (FR-3).
session-registry 23/23 green (5 new operator-precedence cases); daemon tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
rev1-r2 BLOCKING (candidate d2115e9d): the rig ps help still advertised the
pre-0.4.0 node payload - it said --full emits the complete per-node payload
"including ... recoveryGuidance" and that --nodes --json --full is "same as
pre-0.4.0 default". Both are now false: the node-list intentionally returns
recoveryGuidance null and contextUsage.currentUsage null (relocated to the
single-node detail / whoami).
Fix (AC-4 in-CLI documentation):
- Rewrote the --full description: full relative to the compact CLI projection,
but the node-list carries recoveryGuidance: null and
contextUsage.currentUsage: null even with --full; fetch full guidance and
currentUsage from /api/rigs/:rigId/nodes/:logicalId or rig whoami.
- Dropped "(same as pre-0.4.0 default)" from the --nodes --json --full example.
- Added a regression test (captures the rendered help via outputHelp) asserting
the help no longer advertises the old contract and documents the null
relocation.
ps.test 63/63 green; cli tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Bring rig context into the compact-default read-command grammar (same family
as ps/whoami). The ~3k --json dump was the full 16-field Seat[] pretty-printed;
the human table was already compact.
FR-1: bare `rig context --json` now emits a slim, non-pretty projection -
exactly 7 per-seat keys (session, rig, runtime, usedPercentage,
contextWindowSize, staleness, displayStatus) + the unchanged summary. The
other 9 Seat fields move to --full.
FR-2: --json (encoding) and --full (field-breadth) are orthogonal axes:
- `rig context` (human default): unchanged compact table.
- `rig context --json`: slim 7-key projection, non-pretty.
- `rig context --full --json`: complete 16-field Seat[] + summary, pretty -
the ONLY lossless-parity surface (byte/shape match with the prior --json).
- `rig context --full` (human): widened table = today's columns plus
remaining/source/freshness.
FR-3: CLI-only output compaction. No daemon/route/payload change (the heavy
node-list fetch is OPR.0.4.0.26's concern).
FR-4: --help teaches the compact default + --full.
All visible seats are preserved in compact (no seat hidden; --threshold stays
the only seat filter). Migrated 3 existing context tests that asserted
full-only fields on bare --json to --full --json; added AC tests for the exact
7-key compact schema, full-only-keys-absent, --full 16-field parity, non-pretty
+ smaller, all-seats-preserved, widened human table, and help. 19/19 green;
cli tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Slim the daemon node-LIST payload so even --full and the UI consumers stop
paying for two source hogs, while preserving full data on the detail/whoami
paths and breaking no consumer.
FR-1: getNodeInventory no longer inlines per-node recoveryGuidance prose in
the LIST (it duplicated ~47KB across nodes and no node-list consumer read it).
The full guidance is recomputed on the single-node detail path (getNodeDetail).
FR-2/FR-3: getNodeInventoryWithContext drops the heavy currentUsage blob from
the LIST contextUsage (a serialized per-node usage payload, ~79KB across a
large fleet) while keeping every scalar the ring/table/filter consumers use.
The full currentUsage stays on the detail (getNodeDetailWithContext) and
whoami paths.
FR-4: no consumer migration needed. Verified at current main:
- the only recoveryGuidance LIST-shaped reader (cli restore.ts) actually reads
the restore-result payload, a separate builder - unaffected;
- no cli/ui node-list consumer reads currentUsage (only the detail hook type);
- all node-list contextUsage consumers read scalars only, preserved.
Tests: migrated the 3 node-inventory recoveryGuidance assertions to the detail
path; added dedup-contract tests (list nulls both fields + keeps scalars;
detail keeps full currentUsage + recoveryGuidance) and an AC-1 list-vs-detail
JSON slimming/relocation proof. 43/43 node-inventory tests green; daemon + cli
+ ui typecheck clean.
No parser/UI/convention change. Detail/whoami unchanged (already full).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Make PROGRESS.md reliable and deterministically updatable so the PROGRESS
UI always has a valid, agent-maintainable data source.
FR-3 (new): `rig scope slice progress <path>` and `rig scope mission
progress <mission>` deterministically update a progress rail:
- --add "<text>" [--section <heading>] [--status active|done|blocked]
appends a UI-valid `- [ ]`/`- [x]`/`- [~]` row, creating the section
(default ## Rail) when absent; idempotent; refuses conflicting
same-text-different-status duplicates (pointing at --set).
- --set "<text>" --status ... rewrites the indicator of the single
exact-trimmed-text-matching row; errors on 0 or >1 matches.
- Targets PROGRESS.md, or a readme-only scope's README rail; errors to
create/repair when no surface exists. Surgical: frontmatter and the
`# H1` title source are preserved byte-for-byte.
FR-6 (new): `rig scope slice/mission repair` idempotently backfills a
missing PROGRESS.md (mission repair also sweeps its slices), skipping
intentional readme-only scopes and existing rails - the fix for
stale-host scopes created before create began scaffolding.
FR-5: README scaffold templates now carry the mandatory `verified:`
provenance field alongside `stage` (scope-and-versioning section 2
requires both; a bare `created:` is not an epistemic signal).
FR-4: the scaffold templates and the verb output are proven to parse
into the expected tree by the daemon ProgressIndexer (the real UI data
source), end-to-end.
FR-1/FR-2 create scaffolding already shipped; locked by existing
regression tests (unchanged).
Pure markdown editors live in lib/scope/progress-edit.ts (unit-tested);
the command layer stays thin. No parser, convention, or UI changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Rev1-r2 BLOCKING: -o wide was advertised in help but never implemented.
Removed the wide format reference; -o json remains as the encoding
switch (alias for --json). The default human output is the compact
table; no separate wide mode is needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Revises slice 28's caller-scoped default to the docker/kubectl
read-command grammar: rig queue list = active (pending/in-progress/
blocked) + compact + current-rig (derived from OPENRIG_SESSION_NAME
@rig suffix). Four orthogonal axes: -a/--all (history), -A/--all-rigs
(breadth), --full (body/chain), -o json|wide (encoding). --mine
replaces 28's implicit caller scope as an explicit flag. Daemon adds
rig suffix-match WHERE (end-anchored, LIKE-escaped) + activeOnly
filter, both server-side before LIMIT. Handed-off excluded from
default active set (closed source; pending child carries live work).
Firehose still reachable via --full --all --all-rigs.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Guard BLOCKER 1: --all now suppresses both default as-scope and
compact, producing the full firehose (today's default parity).
Guard BLOCKER 2: default as=OPENRIG_SESSION_NAME only applies when
no explicit scope flags (--as, --destination, --source) are set,
so existing explicit-scope consumers are unbroken. Added 4 CLI
regression tests covering --all no-compact, --destination no-as,
--source no-as, and default as+compact injection.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Default rig queue list now sends caller-scoped (as=OPENRIG_SESSION_NAME,
destination OR source union) + compact (no body/chainOfRecord) params
to the daemon. The daemon applies the scope WHERE before LIMIT and
uses a compact column projection, cutting the ~64k default to
low-thousands. --all restores the full unscoped fleet view, --full
restores complete per-item fields. Daemon API stays back-compat when
the new params are absent. Active-first ordering for scoped queries.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The hook review prompt can surface during the thread-id capture
window, not just before it. captureFreshThreadId now calls
dismissCodexInteractiveGates (which handles both update_gate and
hook_trust_gate) on each poll iteration instead of only
dismissSkippableCodexUpdatePrompt. Added regression test for delayed
hook-trust gate appearing mid-capture that would fail on the prior
commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The codex fork path hit an interactive "Hooks need review" prompt that
stalled the fork and prevented thread-id capture. Added hook_trust_gate
probe detection (looksLikeCodexHookReviewPrompt) and a combined
dismissCodexInteractiveGates method that handles both update_gate
(option 3, skip) and hook_trust_gate (option 2, trust all) before
attempting thread-id capture. The managed launch path already prevented
the workspace trust gate via provisionWorkspaceTrust; this carries the
same auto-drive pattern to the hook-review gate on the fork path.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Default human rig ps --nodes now renders a compact 6-column table
(RIG, SESSION, LIFECYCLE, ACTIVITY, WORK, REASON) instead of the
full 14-column detail table. --full restores the full-detail table.
Adds discriminator tests asserting compact columns present and
full-only columns (CTX, RESTORE, RUNTIME, etc.) absent in default.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Default rig ps --nodes output now emits a compact per-node summary
(rigName, canonicalSessionName, lifecycleState, agentActivity.state,
reason when attention, hasAssignedWork, pendingWorkCount, latestError
when attention) instead of the full fat record. --full/--verbose
restores the complete payload (byte-equivalent to the pre-0.4.0
default). Adds --rig and --session convenience filters. Updates help
text to document compact defaults and queue-as-frontier guidance.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
AC-4: createApp now returns Hono (not {app, injectWebSocket}).
New createAppWithWebSocket used only by startup.ts for the
production WS path. All test callers doing createApp().request()
work normally again.
Module-level _lastInjectWebSocket captures the WS handler when
enableNodeWebSocket is true, used by createAppWithWebSocket.
12/12 terminal tests + 15/15 transport-routes (previously broken).
tsc 0.
Separate test file mocks @xterm/xterm Terminal.open() to throw.
FeedCardTerminalDrill renders, drill clicked, popover opens,
FocusedTerminal catches the error and renders "Terminal unavailable".
Asserts: no /preview fetch, no captured/snapshot copy in popover.
This proves the inherited degradation path from FocusedTerminal
surfaces honestly when the live terminal cannot initialize.
1/1 feed-card-drill-ac3 + 6/6 feed-card-terminal-drill.
FR-2: createApp WS registration gated by deps.enableNodeWebSocket
(default false). Only startup.ts createDaemon sets it true.
In-memory test callers get a safe no-op injectWebSocket and
app.request works normally. Restores ~360 daemon tests.
FR-1: FeedCardTerminalDrill copy converged from captured snapshot
to live terminal. Comments, title, label all say live terminal.
Test inverted: asserts FocusedTerminal renders + NO /preview fetch.
Label test proves live terminal not captured/snapshot.
Dead SessionPreviewPane import removed from TerminalPreviewPopover.
4/4 feed-card-terminal-drill. tsc 0 daemon + UI.
Rev1 blocker: card id was activity-needs-input-${logicalId}, causing
collisions when the same logicalId exists in multiple rigs. Now uses
activity-needs-input-${rigId}-${logicalId}. rigId also set in payload
and FeedCard.rigId.
Regression test proves two rigs sharing logicalId produce distinct
card ids (dismiss isolation).
14/14 activity-elite. tsc 0.
Guard blocker: activity-needs-input cards used synthetic seq=-1 but
were not classified as synthetic, causing CLEANUP-1 hazard (seq
collision with event cards in dismissal).
isSyntheticFeedCard now recognizes both queue-attention-* and
activity-needs-input-* prefixes. Feed.tsx uses isSyntheticFeedCard
for dismissal routing and filtering (string-id dismissal, not seq).
eventDerivedSeqsForPrune filters out all synthetic cards.
2 regression tests: activity-needs-input classified as synthetic +
eventDerivedSeqsForPrune excludes them (no -1 in seqs).
13/13 activity-elite. tsc 0.
FR-4 production wiring: useNeedsInputSeats hook fetches all nodes
across all rigs, filters needs_input via getActivityStateWithSource,
and produces NeedsInputSeatEntry[]. Feed.tsx merges
needsInputSeatToFeedCard results into the For-You feed alongside
queue-derived attention cards via the existing
mergeAttentionIntoFeed + sortFeedByDecisionBand pipeline.
Pane-grade needs_input cards labeled "(activity-grade)" in title.
Hook-grade cards render without qualifier. Both reach the
action-required feed band.
48/48 tests. tsc 0.
Finding 1: formatRollupLabel now uses needsInputHookGrade for
"needs you" count. Pane-grade needs_input shown separately as
"N needs input (activity-grade)". AC-4 regression test proves
1 hook + 1 pane = "1 needs you + 1 needs input (activity-grade)",
never "2 needs you".
Finding 2: needsInputSeatToFeedCard added to attention-feed.ts.
Projects needs_input seats into FeedCard for composition with the
existing For-You feed. Pane-grade labeled "(activity-grade)" in
the card title. Hook-grade renders without qualifier.
11/11 activity-elite tests. tsc 0.
Finding 1: SeatOverviewTable + HybridTopologyNodes now show
time-in-state and honest grade label (activity-grade suffix).
All 4 PRD-named consumers enriched.
Finding 2: ActivityRollupBar component added to rig-scope topology
page (ScopePages.tsx). Uses computeActivityRollup from
getActivityStateWithSource per seat. Renders "N working / N idle /
N needs you" summary bar.
Finding 3: FR-4 attention composition via rollup (needs_input count
surfaced in rollup bar) + consumer-level honest labeling.
47/47 tests. tsc 0.
FR-2: getTimeInState helper prefers eventAt over sampledAt for
duration. Added eventAt to AgentActivitySummary. formatDuration
for human-readable time labels.
FR-1: isHookGradeNeedsInput helper for elevation gating. Time-in-
state wired to Explorer and RigNode tooltip/aria.
FR-3: computeActivityRollup aggregates per-seat states from
getActivityStateWithSource. formatRollupLabel for display.
FR-5: All enriched surfaces carry source label.
AC-4: 3 honesty regression tests proving pane_heuristic needs_input
is NOT hook-grade elevated; hook-grade requires source=hook;
pane needs_input is visible but activity-grade.
10/10 activity-elite + 37/37 activity-visuals-pl019 = 47/47. tsc 0.
Removed the graph-view-only watermark instance (RigGraph.tsx:500-505)
and the dead rigStamp computation (:367) + unused shortId import.
globals.css .stamp-watermark is untouched (shared by RigStamp).
RigStamp primitive at rig-stamp.tsx:49 still uses stamp-watermark.
Registration marks, ReactFlow nodes/edges, empty-topology state,
table view, HostMultiRigGraph all preserved.
Test: converted the positive rig-stamp-watermark assertion into a
negative regression guard (no data-testid rig-stamp-watermark in
graph view + no stamp-watermark in graph subtree innerHTML).
9/9 rig-stamp + ghost regression pass. 1 pre-existing PL-019
activity-dot test failure is unrelated to this change.
Guard blocker: remote single-node launch body was {} even when
--hold-reason was provided. Now conditionally includes holdReason
matching the local launch path. Regression test added proving
holdReason appears in the POST body.
19/19 remote-http-ops. tsc 0.
Guard blocker: remote launch-subset body key was targets but the
daemon route expects seats. Fixed to match the local launch path
and the daemon contract at sessions.ts:225-229. Test assertion
updated from targets to seats.
18/18 remote-http-ops. tsc 0.
FR-3: rig whoami --all-hosts fans out to all registered HTTP hosts.
--hosts id,id for subset. Per-host result envelope with identity
(url, installRoot, rig roster) or failure entry. Partial failure
exits 3 but always includes reachable host data. Unknown --hosts
ids rejected. SSH-only hosts get honest failure entry.
Uses the same per-host pattern as ps fan-out: resolve bearer,
check HTTP status on /api/info + /api/ps, classify failures.
83/83 focused tests. tsc 0.
4 new tests added to remote-http-ops.test.ts:
- up --host: all body fields (plan/targetRoot/existing/freshLogicalIds)
- down --host: exact-id-first over same-name collision
- down --host: missing bearer exits 1, no /api/ps or /api/down request
- whoami --host: non-2xx /api/ps fails without fake identity
10/10 remote-http-ops total.
6 focused regressions in remote-http-ops.test.ts:
up --host:
- sends correct body (sourceRef/autoApprove/cwdOverride) with bearer
- missing bearer exits nonzero, no HTTP request
down --host:
- resolves name via remote /api/ps, posts resolved rigId with bearer
- ambiguous name exits nonzero, no /api/down request
whoami --host:
- returns installRoot + rig roster from /api/info + /api/ps with bearer
- non-2xx /api/info fails without fake identity (exit 1)
6/6 remote-http-ops. tsc 0.
Finding 1: up --host now sends the daemon's expected body shape
(sourceRef, autoApprove, cwdOverride, targetRoot, freshLogicalIds).
Finding 2: down --host resolves rig name via remote /api/ps before
posting /api/down. Exact-id-first, active name filtering, ambiguous
name halt. resolveRemoteRigId helper added to remote-host-ops.ts.
Finding 3: whoami --host uses deps.clientFactory (injectable), checks
HTTP status on /api/info and /api/ps, uses installRoot (the real
field) instead of nonexistent version/hostname.
Finding 4: JSON failure output now sets nonzero exit code for both
up --host and down --host.
92/92 existing tests. tsc 0.
HTTP --nodes now delegates to the same handleNodes function as local
ps, passing requestHeaders for the remote bearer. Nodes are flattened
to bare arrays (not nested under rigs), matching local ps output.
HTTP rig path now applies summary before limit (matching local
semantics), uses the same envelope shape (entries/totalRigs/truncated),
and outputs bare array when no shaping flags are used.
handleNodes accepts optional requestHeaders param so it works with
both local (no headers) and remote (bearer) clients.
181/181 across all test suites. tsc 0.
Rewrote runHttpPs to use the same validation pipeline as local ps:
--active sugar + conflict check, parseFilter, limit validation
(non-negative integer), parseFields with ALLOWED_*_FIELDS,
psApiPath for includeArchived, summarizeRigs/summarizeNodes for
--summary, and selectFields for --fields.
4 new regression tests:
- --summary returns summary object with totalRigs
- invalid --limit rejected before HTTP request (0 API calls)
- --active sugar works on HTTP path
- --active + --filter conflict rejected before HTTP request
9/9 ps-http. tsc 0.
Blocker 1: runHttpPs now forwards --includeArchived as query param
to the remote /api/ps. --nodes fetches per-rig /api/rigs/:id/nodes
from the remote daemon. Bearer header sent on every request.
Blocker 2: fan-out --hosts with unknown ids is now a selector error
(exit 1, not silent empty success). SSH-only hosts in --hosts get
an honest per-host failure entry instead of being silently dropped.
--all-hosts targets only HTTP hosts (correct). Fan-out also forwards
includeArchived to the remote API.
132/132 across host-registry + cross-host + ps suites. tsc 0.
Blocker 1: FailedStep moved to cross-host-types.ts as the single
source; both cross-host-executor.ts and host-registry.ts import from
there. No duplicate union.
Blocker 2: HTTP transport validation now requires exactly one of
bearer_env or bearer_file. Neither and both are rejected with clear
errors. Runtime resolveRemoteBearer handles unresolvable declared
sources as permission-gate.
Blocker 3: Updated existing test assertions for new transport/error
messages. Added 19 new foundation tests: HTTP registry validation
(valid env/file, no source, both sources, empty url, mixed transports),
resolveRemoteBearer (env set/unset, no source, no token in errors),
classifyHttpFailedStep (200/401/403/404/500/0), classifyHttpError.
56/56 across host-registry + host-registry-http + cross-host-executor.
tsc 0 cli.
Extended HostEntry with http transport: url + bearer_env/bearer_file
(per-host bearer reference, not inline). SshHostEntry and HttpHostEntry
as discriminated union. KNOWN_TRANSPORTS widened to ssh + http.
Validation handles both variants.
resolveRemoteBearer: resolves per-host bearer from env var or file.
Missing bearer returns failedStep=permission-gate (no network request).
classifyHttpFailedStep: maps HTTP status to the shared FailedStep enum
(401/403->permission-gate, 4xx/5xx->remote-command-failed,
connection error->remote-daemon-unreachable, 2xx->none).
hostDisplayTarget helper for type-safe display across SSH/HTTP.
All existing callers (send, capture, ps, whoami) migrated from
host.target to hostDisplayTarget(host). cross-host-executor narrowed
with sshHost after transport guard.
tsc 0 cli.
Rev1 blocker: added generationRef counter to FocusedTerminal. Each
effect invocation increments the generation. onclose/onopen/onmessage
and the reconnect timer all check generationRef.current === gen before
proceeding. Cleanup increments generation, invalidating any pending
reconnects from the old session.
Regression test: renders with old-session, rerenders with new-session,
fires old socket onclose, advances past reconnect timer, asserts no
stale reconnect to old-session (only 1 old-session socket exists).
4/4 reconnect behavioral tests. tsc 0.
Guard blocker: rewrote focused-terminal-reconnect.test.tsx with
MockWebSocket instances that store real callbacks.
3 behavioral tests:
1. close triggers [disconnected - reconnecting...] message + new
WebSocket after 3s with same session/token URL (instances[1])
2. unmount after reconnect closes instances[1].close() + no
third socket after 5s
3. unmount before 3s reconnect timer -> no second socket created
27/27 across all focused UI suites.
Rev1 finding 1: FocusedTerminal now auto-reconnects after WS close.
onclose schedules a 3s reconnect timer via connect(). Cleanup clears
the timer and sets mountedRef false to prevent reconnect after
unmount. Shows [disconnected - reconnecting...] during the gap.
Rev1 finding 2: imported @xterm/xterm/css/xterm.css for proper
browser layout and focus styles.
48/48 UI + 12/12 daemon. tsc 0.
Guard blocker: lifecycle test now asserts stopPipePane was called
for the death-test session after WS close, proving the onClose
cleanup path runs on session death.
5/5 WS route (4 auth + 1 lifecycle with close + cleanup).
QA blocker 1: xterm onData now routes through mapXtermInput which
splits special sequences from printable text. Tab/Ctrl-C/arrows/
Enter/Backspace/Delete send as {type:"keys"} with tmux key names.
Printable text sends as {type:"text"} with no implicit Enter.
12/12 input mapper regression tests.
QA blocker 2: terminal-ws route now runs a 2s liveness interval
checking tmux.hasSession. When the session disappears, the WS
closes with code 1001 and cleanup runs (stopPipePane + temp file
removal). Interval cleared on normal close too.
11/11 daemon (4 WS route + 7 auth) + 19/19 UI. tsc 0.
QA-returned blocker: the prior candidate 342cd85c had a 404 on valid
WS upgrade. This test exercises the production mounted path via a
real HTTP server (serve + injectWebSocket + raw http.request upgrade).
4 tests:
- valid token + good Origin => non-404 (the QA regression discriminator)
- missing token => 401
- bad Origin => 403
- wrong token => 401
Uses the same createNodeWebSocket-on-main-app pattern as production
server.ts. Would fail on 342cd85c (sub-app 404).
4/4 WS route + 7/7 daemon auth + 3/3 CLI headers.
QA blocker: createNodeWebSocket was bound to a sub-app, but
injectWebSocket on the server sees the full URL path. The sub-app
route /:sessionName didn't match /api/terminal/:sessionName,
causing 404 on valid WS upgrade.
Fix: createNodeWebSocket now binds to the main Hono app in
server.ts. registerTerminalWs registers the route at the full path
/api/terminal/:sessionName directly on the main app. The upgrade
handler sees the correct path and matches.
7/7 daemon auth + 36/36 UI. tsc 0.
Guard blocker: consolidated all 4 /open-cmux UI callers through the
shared postOpenCmux helper (LiveNodeDetails, RigNode, LaunchCmuxButton
+ useCmuxLaunch). No direct fetch to /open-cmux remains in consumers.
postOpenCmux owns the URL construction + terminalAuthHeaders(), so
the runtime fetch-intercept test for postOpenCmux covers all callers.
Source guard: verifies all 3 consumer files import postOpenCmux and
have no direct fetch to /open-cmux. Fails closed on zero matches.
7/7 token-carry + 51/51 UI. tsc 0.
Guard blocker: exported fetchNodePreview and fetchSessionPreview from
useNodePreview.ts for testability. Rewrote terminal-token-carry tests
to stub globalThis.fetch and assert the actual RequestInit headers
carry Authorization: Bearer <token>.
Runtime assertions prove fetchNodePreview and fetchSessionPreview
send the token header. Source guards retained for useCmuxLaunch and
LaunchCmuxButton.
6/6 token-carry tests.
Guard blocker: UI callers of protected preview and open-cmux routes
were using bare fetch without auth headers. Added terminalAuthHeaders()
to all 4 callers: useNodePreview (both node + session preview),
useCmuxLaunch, and LaunchCmuxButton missing-seat open loop.
7/7 daemon auth + 3/3 CLI headers + 44/44 UI. tsc 0 all packages.
FocusedTerminal now uses dynamic import for xterm with try-catch
fallback so jsdom environments degrade gracefully.
Test setup: added matchMedia stub + HTMLCanvasElement.getContext
null-stub for jsdom (xterm requires both).
Updated stale assertions in live-node-details.test.tsx,
polish-8-demo-readiness.test.tsx, and
p5-3-activity-ring-hot-potato.test.tsx to reference FocusedTerminal
instead of SessionPreviewPane/compact-terminal.
44/44 across live-node-details + polish-8 + activity-ring tests.
1. Committed @hono/node-ws + @xterm/xterm + @xterm/addon-fit in
package.json manifests and lockfile.
2. WS auth: terminal-ws route now validates token from both
Authorization header AND ?token= query param (browser WebSocket
cannot set headers). Uses constant-time comparison.
3. Generated token persistence: daemon writes generated terminal-token
to ~/.openrig/terminal-token (mode 0600) and reuses it on restart.
CLI resolveTerminalToken reads OPENRIG_TERMINAL_BEARER_TOKEN env
first, then ~/.openrig/terminal-token file. Both daemon and CLI
share the same token source.
4. Resize via proper adapter method: added TmuxAdapter.resizeWindow
with shellQuote + integer validation. terminal-ws route uses it
instead of casting through private exec.
tsc 0 daemon + cli.
Daemon: GET /api/terminal/:sessionName WebSocket route using
@hono/node-ws. Output streams via re-pointed pipe-pane -> temp file
-> polled read -> WS send. Input: JSON messages with type keys
(raw send-keys, no implicit Enter), text (literal paste), or resize
(tmux resize-window). Terminal-token auth applied. WebSocket injected
into Node HTTP server at startup.
UI: FocusedTerminal component using @xterm/xterm + @xterm/addon-fit.
Connects to daemon WS, maps xterm onData to text messages (no
implicit Enter on printables). Auto-fits on resize. Honest disconnect
message on close.
Dependencies added: @hono/node-ws (daemon), @xterm/xterm +
@xterm/addon-fit (ui).
tsc 0 daemon + cli. UI integration into the 3 consumer surfaces
(TopologyTerminalView, LiveNodeDetails, agent page) is the next
commit.
OPT-2 security: mounts authBearerTokenMiddleware on all protected
terminal routes (transport send/capture/broadcast + preview aliases +
open-cmux) with a dedicated always-required terminal-token. Token
sourced from OPENRIG_TERMINAL_BEARER_TOKEN env or daemon-generated
(randomBytes(32).hex). No-fall-open: token is always required.
client.ts post() header-merge fix: the private fetch() method now
merges options.headers into init.headers so all HTTP methods (post,
get, delete, etc.) can carry auth tokens. Previously only put() and
postText() merged headers.
CLI carry: rig send + rig capture pass terminal-token via the fixed
header-merge path (reads OPENRIG_TERMINAL_BEARER_TOKEN env).
UI carry: extended missionControlAuth with terminal-token storage key
+ URL prime (termToken/term_token query params). Daemon injects the
token into served index.html via inline script.
tsc 0 daemon + cli.
Rev1 blocker: source=hook is now gated on evidenceSource=runtime_hook
AND state non-unknown AND not stale AND not fallback. Stale
runtime_hook with no terminal signal returns source=none.
Non-hook evidence sources (tmux_session, session_registry, etc.)
return source=none; only pane_heuristic returns its own source.
terminalActive true/false always wins as terminal_activity.
Added stale/fallback fields to AgentActivitySummary. 5 new regression
tests: stale non-unknown runtime_hook, tmux_session, session_registry,
unrecognized evidenceSource all return source=none.
37/37 activity-visuals + 17/17 ring/topology.
Guard blocker: stale/unknown runtime_hook was still returning
source=hook. Now all unknown-state activity returns source=none.
source=hook is exclusively for fresh non-unknown runtime_hook
(the earlier branch). Updated regression test to assert stale
runtime_hook + null terminalActive returns source=none.
33/33 activity-visuals.
Guard blocker: the unknown-activity fallback at the bottom of
getActivityStateWithSource returned source=hook for any activity
object regardless of evidenceSource. Now only returns source=hook
when evidenceSource === runtime_hook; all other unknown-activity
objects return source=none.
2 regression tests: unknown pane_heuristic + null terminalActive
returns none; unknown runtime_hook + null returns hook.
33/33 activity-visuals.
Rev1 blocker: getActivityStateWithSource now checks evidenceSource to
distinguish runtime_hook (true hook-grade) from pane_heuristic
(activity-grade). For pane_heuristic activity, terminalActive is
preferred when available (source=terminal_activity); when absent,
source exposes the actual evidenceSource (e.g. pane_heuristic).
Only runtime_hook activity returns source=hook.
Consumer (activity-grade) suffix now triggers on any non-hook,
non-none source. 4 regression tests added for pane_heuristic paths.
31/31 activity-visuals. tsc 0 UI touched files.
Blocker 1: all 4 dot consumers now use getActivityStateWithSource()
and expose data-activity-source attribute (hook/terminal_activity/none).
Title/aria-label includes (activity-grade) suffix for terminal_activity
fallback so operators can distinguish hook-grade from fallback.
Blocker 2: rewrote stale monitor-silence comments in node-launcher.ts,
profile-resolver.ts, rigspec-instantiator.ts, types.ts, and
agent-manifest.test.ts to reflect current truth: window_activity is
the sole source, per-seat silenceWindowSeconds is currently inert
(retained for future decision), NodeLauncher no longer configures
monitor-silence.
27/27 activity-visuals + 93/93 daemon (tmux-adapter + node-launcher).
tsc 0 daemon + UI touched files.
FR-1..4: getActivityState now consults terminalActive when hook
activity is null/unknown/stale. Fresh hook (running/needs_input)
always wins. Fallback never synthesizes needs_input. Honest source
exposed via getActivityStateWithSource (hook vs terminal_activity).
All 4 consumer sites threaded: Explorer.tsx, RigNode.tsx,
SeatOverviewTable.tsx, HybridTopologyNodes.tsx.
FR-5: removed dead setMonitorSilence method from tmux.ts, the call
from node-launcher.ts, and the direct tests. Fixed two stale comments
(index.ts, startup.ts) to describe window_activity poller accurately.
Per-seat silenceWindowSeconds plumbing left inert (documented finding).
27/27 activity-visuals + 7/7 ring + 72/72 tmux-adapter + 21/21
node-launcher. tsc 0 daemon. UI tsc 0 for touched files.
QA blocker: CodexResumeAdapter.resume now runs verifyCodexProfileLoads
before sending the resume command when codexConfigProfile is present.
On preflight failure, returns ok:false with profile-load error and
migration hint before any tmux interaction.
Injectable exec option added to CodexResumeOptions for test stubbing.
23/23 codex-resume (profile preflight pass + block tests added).
146/146 across all four affected suites. tsc 0.
Rev1 blocker: audit scanner skipped slice dirs with no README.md and
no PROGRESS.md (the continue after orphan_progress check). Now
classifies them through classifyScopeItem with null frontmatter and
no progress, emitting missing_id + missing_progress findings. Applied
to both CLI buildAuditCommand and daemon scope-audit route.
54/54 CLI + 14/14 parity. tsc 0 cli+daemon.
QA blocker: malformed rail + non-empty PROGRESS.md rendered the
markdown content instead of the actionable malformed-rail EmptyState.
Reordered the ternary so audit rail status (missing/malformed) is
checked first, before missionProgress.content. A mission with
malformed frontmatter now shows the diagnostic EmptyState with the
rig scope audit command regardless of whether PROGRESS.md has content.
Hook was called after conditional loading/error returns, violating
React Rules of Hooks. Moved to top-level hook block with
detailQuery.data?.missionId ?? null (disabled on null via hook's
enabled guard). sliceAuditEntry computed after the loading guards.
Blocker 1: SliceScopePage progress tab now renders rail-status
EmptyState with audit command action when slice has missing/malformed
rail, using useScopeAudit(detail.missionId) to find per-slice results.
Blocker 2: scope-audit-parity.test.ts now loads BOTH CLI and daemon
classifiers, runs them against 10 shared fixtures covering all rail
statuses, finding kinds, and severity levels, and asserts output
equality. Byte-equivalence checks retained alongside.
14/14 parity + 53/53 CLI commands. tsc 0 all packages.
Orphan detection: slice dirs with PROGRESS.md but no README.md now
emit orphan_progress finding (malformed rail status), not ok:true.
extractFrontmatterRaw: only matches leading --- delimiter (must start
at position 0). Body --- blocks no longer parsed as frontmatter.
13/13 scope-audit. tsc 0 cli.
Strict parse failure now sets railStatus=malformed (was left as
present/missing). Parse error without id line emits a registration_
ghost finding so it surfaces in severity/report. Both CLI canonical
and daemon byte-equivalent mirror updated.
3 new regressions: parse error + PROGRESS.md = malformed (not
present); parse error + id line = ghost + malformed; parse error
without id = finding + malformed.
13/13 scope-audit (was 10). tsc 0 cli+daemon.
Fresh createDaemon startup with no runtime_verifications row now
probes Codex version via runtimeVerifier.verifyCodex() before
ensureCodexFeatureFlag. 0.12x gets hook enablement; 0.13x+/unavailable
skips. Fixes the fresh-DB gap where legacy 0.12x hook enablement was
silently skipped.
27/27 focused tests unchanged. tsc 0 daemon.
getDerivedAttentionOutcome now mirrors deriveNodeLifecycleState:
restoreOutcome=attention_required is a derived attention class
regardless of sessionStatus; restoreOutcome=failed still requires
sessionStatus=running. Removes the blanket sessionStatus=running
gate that blocked non-running attention_required seats.
2 new regressions: non-running attention_required clears with
from=attention_required; non-running failed does NOT clear (mirrors
lifecycle derivation). 28/28 seat-attention. tsc 0 daemon.
ClearAttentionResult now includes derivedEvidence field carrying
source, kind, runtimeCwdVerified for derived-class clears. Flows
through to daemon route JSON and CLI --json surface.
Both evidence-gated and operator-attestation paths populate
derivedEvidence when derivedOutcome is non-null. Undefined when
no derived class was cleared (startup-only clears unchanged).
2 new tests: evidence-gated derivedEvidence surface + operator
attestation derivedEvidence surface. 26/26 seat-attention. tsc 0.
getDerivedAttentionOutcome now returns the actual latest restoreOutcome
(failed|attention_required|null) instead of a boolean. performEvidenceClear
and operator-attestation path thread the real outcome into the emitted
restore.outcome_reconciled.from field.
Tests assert payload.from === "failed" for failed-derived cases (both
evidence and operator-attestation paths). No false audit trail.
24/24 seat-attention-reconciler. 39/39 node-inventory. tsc 0 daemon.
deriveRestoreOutcome now reads restore.outcome_reconciled events
per-node-latest alongside restore.completed/subset_completed. A
newer reconcile (to:operator_recovered) overrides older failed/
attention_required; older reconcile does not override newer failure.
SeatAttentionReconciler extended: accepts the derived class where
startupStatus=ready but restoreOutcome=failed+running. Evidence-
gated clear emits restore.outcome_reconciled (operator_recovered)
for the derived class. Operator attestation records
runtimeCwdVerified=false honestly. A seat in both classes clears
both; a seat in one clears only that one.
restore.outcome_reconciled evidence type generalized: accepts both
strict shape (tmux/fgProcess/resumeTokenUsed/paneState) and clear-
attention shape (source/kind/state/runtimeCwdVerified).
Tests: 4 node-inventory reconcile-event ordering tests + 4 seat-
attention derived-class tests (clear, refuse, both-classes,
operator-attestation). 63/63 focused. tsc 0 daemon+cli.
Invalid last_verified (non YYYY-MM-DD or NaN) now returns bare_verified
instead of passing through to checkStaleness. isValidDate requires
YYYY-MM-DD regex + valid Date parse.
Array-valued source_evidence/sourced_from now handled: each element
checked for self-reference, only real non-self sources count. All-self
array = bare_verified. Mixed array with one real source = verified
(joined with semicolons).
3 new regressions: invalid date + real source -> bare_verified;
array of only self paths -> bare_verified; array with one real
source -> verified.
24/24 skill-audit (was 21). tsc 0 daemon.
isSelfReferential now also resolves evidence relative to
dirname(skillPath) (the skill scan root). Catches ./test-skill/
SKILL.md, test-skill/, and similar relative-to-parent forms.
3 new regressions: ./test-skill/SKILL.md metadata, test-skill/
metadata, top-level verified against ./test-skill/SKILL.md -- all
fail bare_verified.
21/21 skill-audit. tsc 0 daemon.
extractVerified now checks source_evidence against the skill's own
SKILL.md path, directory path, and bare "SKILL.md"/"./SKILL.md".
Self-referential sources treated as bare_verified (the SKILL.md file
is the origin, not evidence the skill was verified against a source).
Top-level "verified: <date> against <own-path>" also treated as bare.
Two new regression tests: source_evidence equal to full SKILL.md path
fails bare_verified; source_evidence "SKILL.md" (bare filename) fails
bare_verified.
15/15 skill-audit (was 13). tsc 0 daemon.
CLI now carries mirrorDriftError from the daemon response. Both JSON
and human modes treat it as audit failure: exit non-zero, print
MIRROR DRIFT CHECK UNAVAILABLE, FAIL includes the reason. Never
prints PASS when mirror proof is unavailable.
16/16 tests. tsc 0 daemon+cli.
Refactored: scripts/mirror-skills.mjs gains checkModeAbsolute(source,
target) that takes absolute paths, reuses shared EXCLUDES/parseChanges,
and does not call ensureTargetExists. No duplicate logic.
skill-mirror-drift.ts now imports checkModeAbsolute from the shared
script via dynamic import (import.meta.url-resolved). Pre-checks
source+target existence (no mkdir). Returns ok:false with reason when
unavailable.
New skill-mirror-drift.test.ts (3 tests): wrapper returns structured
result, does not create skills/_canonical (read-only invariant),
error path returns typed reason.
16/16 total (13 audit + 3 mirror drift). tsc 0 daemon+cli.
New skill-mirror-drift.ts: checkMirrorDriftSafe resolves absolute
source+target paths from repo root (via import.meta.url), checks
both exist before calling rsync, never creates directories. Returns
ok:false with reason when source/target missing or rsync fails.
Audit endpoint uses checkMirrorDriftSafe instead of raw checkMode.
mirrorDriftError surfaced in API response when check unavailable --
not silently omitted (no false-green from unavailable mirror proof).
13/13 skill-audit. tsc 0 daemon+cli.
B1: audit endpoint now dynamically imports mirror-skills.mjs checkMode
and passes the result to auditSkills. Graceful no-op when rsync or
mirror-skills is unavailable. Mirror drift findings flow through to
the API response.
B4 dedupe fix: seenPaths seeded from Claude results BEFORE iterating
Codex results. Shared ~/.openrig/skills paths no longer duplicate.
13/13 skill-audit. tsc 0 daemon+cli.
B1: mirror drift wired via mirrorDrift opt on auditSkills. checkMode
result produces mirror_drift findings. Two regression tests.
B2: sha256Hex reused from files/file-write-service (no direct
createHash). hashSkillFolder composes per-file sha256Hex.
B3: body carried in SkillProvenanceEntry. isExempt checks real body
for legacy banner, not a phantom frontmatter._body. Regression test
proves body banner exempts the skill.
B4: audit endpoint scans both claude-code and codex roots, deduped by
path. Codex-only skills under .agents/skills are now visible.
13/13 skill-audit tests (was 10). tsc 0 daemon+cli.
Tests for the two guard-blocked behaviors:
1. HTTP 201 with result.node.status=attention_required renders as
red "Fork failed [attention_required]", not green success.
2. Runtime-matched sibling with null agentRef/profile excluded from
picker -- shows no-sibling message, no sibling select available.
7/7 fork-now-action tests (was 5).
B1: Fork-now now parses the actual add_member response shape
(data.result.node.status). Failed/attention_required launches shown
as red errors with status + error detail, not green success. Only
launched status renders as green success.
B2: Sibling picker filters to runtime-matched members with non-empty
agentRef AND profile. Nodes with null identity fields are excluded
from the picker -- no malformed payload possible.
5/5 fork-now-action tests. tsc clean for changed files.
ForkNowAction: button + modal with target picker (rig -> pod ->
runtime-matched sibling -> new member id). Posts exact schema-valid
payload to POST /api/rigs/:rigId/pods/:podNamespace/members with
wrapped member body, snake_case session_source mode agent_image,
cloned agent_ref/profile from sibling, cwd from image sourceCwd.
Graceful disables: null/empty sourceCwd -> Fork-now disabled (no cwd
fallback, preserving shipped no-inferred-cwd invariant). No runtime-
matched sibling in pod -> disabled with honest explanation.
Identity safety: mints new member id only. No re-key, no migration,
no preservation of image original identity.
Fail-loud: fork failure surfaces inline, no auto-retry/fresh fallback.
Mode is agent_image, NOT fork (live-seat fork deferred to 0.4.1).
agentImageRow meta: populates existing meta field with version, token
estimate, fork count, last used relative age.
useNodeInventory extended with agentRef/profile/codexConfigProfile
fields for sibling identity cloning.
Tests: 5 ForkNowAction tests (button render, null-cwd disable, empty-
cwd disable, modal open, confirm disabled without fields). tsc clean
(only baseline TopologyTableView errors from missing react-table dep).
2026-06-17 12:01:43 -07:00
2524 changed files with 354625 additions and 20736 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
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.
**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 --nodes` 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).
@@ -81,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
|
@@ -93,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.
@@ -120,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
@@ -137,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."
ca-certificates curl xz-utils git tmux tini procps \
python3 make g++;\
rm -rf /var/lib/apt/lists/*
# --- layer 2: pinned node (an in-range engines version — see scripts/check-engines) ---
ARGNODE_VERSION=22.22.1
ARG TARGETARCH
RUNset -eux;\
case"${TARGETARCH}" in \
amd64)NODE_ARCH=x64 ;;\
arm64)NODE_ARCH=arm64 ;;\
"")echo"TARGETARCH is empty — the legacy (non-BuildKit) builder does not populate it. Pass --build-arg TARGETARCH explicitly (build-testbed-image.sh does) or enable BuildKit. REFUSING to default to amd64, which would install wrong-arch Node." >&2;exit1;;\
| L3 | daemon-in-container: boots vs container-local sqlite, healthz on the published port, `rig up` settles a zero-token stub topology | `L3-daemon-in-container.md` |
| L4 | the 51-02 hermetic contract: the env-helper still REFUSES a foreign `OPENRIG_URL`**inside** the container (fail-closed not weakened by "we're in a container anyway") | `L4-hermetic-fail-closed.md` |
| L5 | multi-host: N containers as N distinct named self-hosts on one docker network, composed via the shipped host registry over HTTP (plan §3) — **and** the 51-09 live-leg rider | `L5-multi-host-and-51-09.md` |
| L6 | the 51-02 runner's **container-mode** (step-3) drives a REAL scenario through a container by manifest identity + host-mode byte-intact parity + the L4 fail-closed step-3 form; the ledger carries the image manifest id | `L6-container-runner-e2e.md` |
Sequencing: **L0 → build → L1 → L2 → L3 → L4 → L5**, with **L6 after L3** (it needs only the built image
+ an in-container daemon, plus the built HOST CLI bin for the read side). L3 is where the stub payload is
exercised (and where `docker/testbed/stub-assets.list` is finalized). L4 depends on the 51-02 hermetic
env-helper being present in the packaged image (it ships in the openrig package the Dockerfile installs);
if it is NOT packaged, its fail-closed leg is exercised in the **step-3 form** — L6.3.
**51-09 live-leg rider (orch, merge desk).** The testbed image's first multi-host run doubles as the
51-09 live-leg executor: L5 boots the containers as the two named hosts `H_A`/`H_B` and then runs the
shipped 51-09 collision/alignment runbook **in the same host-side session** —
`packages/daemon/test/fixtures/self-host-live-legs/RUNBOOK.md` (LEG A cross-host stamped-triple
round-trip, LEG B founder-collision). Execute both families together; return both evidence sets.
## Shared conventions
**Image identity.** All legs run against the image the build verb tagged, by manifest identity:
| [content-surfaces.md](architecture/content-surfaces.md) | The operator-allowlisted file browser, atomic conflict-checked writes + JSONL edit audit, PROGRESS.md tree indexer, the one-screen Steering composer. |
| [living-notes-review.md](architecture/living-notes-review.md) | **v0.4.4** — the Living Notes review surface: the ONE `ComposedSliceReview` (intent→plan→delivered) projection, pure composer + gatherer, staged-approval locks, C1-bound `verified`, `/api/review/*` routes, freeze export, ranged media serving, and the SDLC on-disk convention it projects. |
| [plugin-agent-image-context-pack.md](architecture/plugin-agent-image-context-pack.md) | The filesystem-canonical content layer — plugin discovery, agent images, context packs, the Claude auto-compaction policy enforcer. |
| [`architecture/architecture-rules-and-event-system.md`](architecture/architecture-rules-and-event-system.md) | The cross-cutting invariants — the 25 architecture rules (incl. **rule 15**, restore honesty), the RigEvent union + SSE delivery, intentional compatibility limits. |
| [`architecture/living-notes-review.md`](architecture/living-notes-review.md) | **Added v0.4.4** (not part of the original monolith): the Living Notes review surface — the one intent→plan→delivered projection, staged-approval locks, proof artifacts, freeze export, ranged media serving. |
The UI half of the old `### UI architecture` section is now the **4 modules
under `ui/`** (see [`ui.md`](ui.md), also a redirect stub, and the `ui/` index
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.
@@ -66,6 +66,7 @@ One row per module. "Reach for this when…" mirrors each module's actual
| `plugin-agent-image-context-pack.md` | Four filesystem-canonical content primitives the daemon discovers and serves. | You need how OpenRig discovers plugins, captures/forks agent images, assembles/sends context packs, or how the Claude auto-compaction enforcer decides to send `/compact`. |
| `packaging-bootstrap-bundles.md` | How a topology is packaged into a shareable bundle and reconstituted elsewhere. | You need how rig/pod bundles are assembled (schema-v2 vs legacy v1), how bundle create/inspect/install + `/api/up` route across source kinds, the staged BootstrapOrchestrator flow, or which legacy install-engine seams still ship. |
| `architecture-rules-and-event-system.md` | The cross-cutting invariants that belong to no single subsystem. | You need the 25 architecture rules + startup/import constraints, the shape of the RigEvent union and its SSE delivery, or the intentional compatibility limits that still describe the shipped system. |
| `living-notes-review.md` | **v0.4.4** — the one intent→plan→delivered review projection over on-disk SDLC markdown. | You need the `ComposedSliceReview` contract, the `/api/review/*` routes, staged-approval locks (`--scope spec|delivery`), the `## Proof contract` ↔ proof-artifact join and C1-bound `verified`, the freeze export, or ranged media serving for review evidence. |
### ui/
@@ -150,6 +151,7 @@ Reading-order rule: open a module's `prerequisite-reads` first
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."
@@ -311,7 +319,7 @@ This prompts the agent to actually invoke the skills, not just have them as pass
## System Check
After identity recovery, verify:
1. `rig ps --nodes` shows your rig running
1. `rig ps --nodes` shows your rig running (scoped to your session's rig by default; outside a managed session name it explicitly: `rig ps --nodes --rig <name>`)
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: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. |
| `pods` | Pod[] | yes | — | At least one pod required. Each pod is a bounded context containing members and pod-local edges. |
| `edges` | CrossPodEdge[] | no | `[]` | Cross-pod edges connecting members in different pods. Must use fully-qualified `pod.member` IDs. |
### Attaching a permission policy
Attach a policy to a rig with `permission_policy`, either at the rig level or on a member:
```yaml
# a built-in, by name:
permission_policy: builtin:standard
# or a custom policy file, by relative path (resolved from this spec's directory):
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
@@ -210,6 +271,7 @@ edges:
| `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. |
@@ -324,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
@@ -458,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`.
@@ -472,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.
> **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 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
| | 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 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 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
`--full` returns the complete record (raw byte-equivalent). **Slice 34 — breadth default flipped to CURRENT-RIG** (derived from `OPENRIG_SESSION_NAME`'s `@<rig>` suffix) to match the docker/kubectl-aligned `rig queue list` grammar: `-A`/`--all-rigs` for fleet breadth; `--rig <name>` overrides the current-rig default; `--session <sess>` narrows further. All-states stays the default (per orch-lead's ruling: ps surfaces topology/readiness, where stopped/recoverable/attention is the actionable signal — unlike queue-list where the actionable set is active items). `--active` / `--running` remains the opt-in active-state filter. Closes a ~77,000-token status-glance incident at root + a fleet-scale unbounded-default-output bomb. **Resume-token security**: `--full` JSON now emits `resumeTokenPresent` (boolean) instead of the resume token value itself, so an orch glance never accidentally leaks token material.
> **⚠️ Milestone:** v0.4.7 is where the **web UI moves to maintenance mode**. The CLI is the primary product surface from this release forward; the web UI still ships and still works, but is no longer receiving new feature work. If you drive OpenRig from a browser today, plan a transition to the CLI (and, from v0.5.0 onward, the terminal TUI: `rig` / `rig tui`). See the UI section below for the specifics that ship in 0.4.7.
**Recovery honesty is the through-line.** On a resumed restore, the startup sequence now loads a seat's applicable skill preloads before the role-defining first message — so a resuming seat knows its skills before it starts working. Compaction recovery is honest: transcript ingest exposes degraded states explicitly, and a post-compact restore-and-audit is gated on the seat being idle so "restore-sent" actually means "delivered". Daemon liveness reporting is honest — a dead seat drops effective running to 0 and reports `attention_required`; `rig send` and `rig capture` return an explicit "session missing" error when the seat is gone. CLI probes report uncertainty honestly — an unconfirmable status returns `UNKNOWN` with no false start advice, and a confirmed-stopped seat fails with actionable guidance.
Migrations are additive only. Existing v0.4.6 databases upgrade by running `rig daemon start` on the new daemon.
## What Shipped
### Starter bootstrap
Product-team starter bootstrap is cleaner, and starter seats get a skill preload on both fresh-start and restore. Default culture loads at startup, and rig specs are audited at load time. **Rigs instantiated before v0.4.7 need re-instantiation** (or spec-level patching) to pick up the starter fixes — the loader and audit changes apply immediately, but starter-spec content lands at instantiation.
### Skills wave
The bundled skill layer gains public routing and a default projection, plus public-skill strip and mirror controls, plus a plugin fix that keeps the documented skill count honest. The bundled skill inventory grows substantially, from a handful to broad coverage across core, PM, pod, and process families.
### Slack connector + human queue
First-class Slack connector shape (CLI `rig slack` commands + supporting library). Hosted create crosses an inconclusive local probe to the real configured-daemon result.
### CLI contract honesty
`--json` errors, flag validation, and scoped overdue behavior are tightened for machine-readable use.
### UI — moved to maintenance mode
**The web UI is frozen at this release in maintenance mode.** The wording is CLI-primary — never "deprecated"; the UI still ships and still runs, but it is no longer receiving new feature work, and CLI/TUI is the primary surface going forward. What lands in v0.4.7 to make this explicit to users:
- A dismissible in-app banner in the web UI announcing the maintenance-mode status.
- A `rig ui open` stderr notice at launch time, so operators driving the CLI see the status before they open the browser.
- Documentation positioning updated across README and user-facing docs.
Existing v0.4.6 deployments keep working; nothing is removed. The substantive product investment shifts to the CLI and, from v0.5.0, the terminal TUI.
### Queue, topology, review polish
- Queue compact-list rows mark elided fields so "omitted" is distinguishable from "empty".
- Nested Approve posts a missions-root-relative scope path.
- Mission review card composition is polished.
- Unverified delivered items read "artifact-recorded" rather than "nothing delivered".
- The drawer file viewer resolves inline in-body images via the file-asset API.
- Proof-of-work Markdown links open in the in-app drawer.
- Docs guard encodes the root-placement rule for `docs/DESIGN.md`.
### Host sizing guidance
Recommended: add swap on the host, and plan for roughly ~2 GB RSS per seat as an observed baseline.
## Behavior + Compatibility
- **Migrations** — additive only. Existing v0.4.6 databases upgrade by running `rig daemon start` on the new daemon.
- **Rig-spec starter behavior** — the loader and audit changes apply immediately; starter-spec content lands at rig instantiation. Rigs instantiated before v0.4.7 need re-instantiation (or spec-level patching) to pick up the starter fixes.
- **Seat-liveness API** — consumers should key seat liveness off the honest lifecycle state (updated every few seconds) rather than the older session-status field, whose staleness cleanup is tracked for a subsequent release.
- **Web UI posture** — frozen in maintenance mode; no CLI surface breakage. Nothing is removed from the UI at this release.
Launch posture is configurable, the deny set writes at a level that project-local approvals can't override, and dangerous operations are gated by explicit prefix rules that actually hold under the current harness.
Migrations are additive only. Existing v0.4.7 databases upgrade by running `rig daemon start` on the new daemon.
## What Shipped
### Configurable launch posture
The hardcoded `--permission-mode acceptEdits` launch flag is replaced by a configurable posture defaulting to `dontAsk`. This closes the class of freezes where an autonomous seat could get stuck on a modal permission prompt with nobody to click through it. `dontAsk` is the default because it matches what an autonomous seat can actually respond to; `acceptEdits` remains selectable when a seat needs the prior behavior; a deliberate `bypassPermissions` value is preserved untouched across writes.
### Deny set that project-local approvals can't override
The posture writes to user-level `~/.claude/settings.json` instead of the project-local approval file. **Deny wins over project-local approvals** as a result. Writes are additive (never destructive to sibling keys) and forward-migrate a legacy `acceptEdits` value into the new schema. A deliberate `bypassPermissions` value is preserved.
### Bounded-dangerous deny set
Four dangerous operations are gated by default: `git push`, `gh pr create`, `npm publish`, and `rig down`. `rig down` in particular is gated via the prefix rule `Bash(rig down:*)` — the current Claude harness (2.1.220) only supports prefix matches on Bash rules, and flag-only patterns provably don't gate target-first forms like `rig down <rig> --force`, so the prefix rule is the only shape that actually holds. Plain `rig down` is gated in v0.4.8; selective allowance (e.g. `rig down <rig>` for a specific target) is deferred to v0.5.0 server-side enforcement.
### `rig up` un-gated
The prior release's ask-gate on `rig up` is removed — `rig up` is a reversible operation and doesn't warrant an interactive gate.
### Permission-policy foundation (built-ins land in v0.5.0)
- **Harness-neutral policy schema.** The permission-policy spec is a harness-neutral surface with `default_posture`, `floor`, and `allow` / `ask` / `deny` expressed as **semantic actions** (`push_to_remote`, `force_push`, `delete_files`, `read_secrets`, `create_pr`, `publish_package`, and so on) rather than raw shell-command patterns.
- **`rig setup --policy <name>`** — a new flag on `rig setup` records a permission-policy choice into an existing rig spec. Takes a built-in name or a path to a custom policy file. The **built-in policy files themselves land in v0.5.0**; v0.4.8 ships the framework that consumes them.
- **Two surfaces.** The **launch-flag** surface (Claude `--permission-mode`, Codex sandbox / bypass flags, Pi `--approve` / `--no-approve`) is stable and set by OpenRig for you — this is where the floor and full-bypass YOLO live. The **config-file** surface (Claude `~/.claude/settings.json`, Codex `config.toml`) is where allow/ask/deny rules live; because harness rule grammars change frequently across versions, this surface is applied interactively by an agent-driven skill (that skill ships in v0.5.0 as `applying-a-permission-policy`).
## Deployment Note (operators read this)
For the posture writes to reach seats, **the daemon's `HOME` must equal the seat's tmux `HOME`**. This is a Claude 2.1.220 settings-path invariant that matters for the remote-host leg of any upgrade:
- On a local-only host, this is typically satisfied automatically.
- On a remote-host upgrade, verify HOME parity between the daemon process and the tmux seat process before treating the upgrade as complete.
(v0.5.0 documents this as a Known Issue for Codex `HOME` divergence; the product fix ships in v0.5.1.)
## Superseded
An initial v0.4.8 attempt (with `dontAsk` baked as a **platform default** rather than an OpenRig-side default) was withdrawn on final review; the shipped v0.4.8 is a re-scoped permission-agnostic base plus a policy-spec system. Nothing from the withdrawn attempt shipped.
## Behavior + Compatibility
- **Migrations** — additive only. Existing v0.4.7 databases upgrade by running `rig daemon start` on the new daemon.
- **Default launch posture** — changed from hardcoded `acceptEdits` to configurable, default `dontAsk`. If a seat needs the prior behavior, select `acceptEdits` explicitly.
- **Deny-set write location** — user-level `~/.claude/settings.json`; the deny set wins over project-local approvals. Writes are additive.
> **UI posture (unchanged from v0.4.7):** the web UI is in **maintenance mode** — still ships, still runs, no new feature work. **CLI/TUI is the primary surface.** v0.5.0 lands the TUI (`rig` / `rig tui`) as the new mission-control home.
## Summary — Mission control in the terminal + context library + permission policies + provider usage + honest CLI
**Mission control in the terminal.** Typing `rig` (or `rig tui`) opens the new TUI: file-tree navigator, dense agent detail (working directory, runtime, context %), a topology graph with the whole fleet on one screen, honest status/activity language, motion design, working scrolling, and honest width-clip indicators throughout.
**v0.5.0 includes everything from v0.4.8.** There is no divergence between the two releases — the v0.4.8 permission-posture work and shipped skills are all present in v0.5.0.
Migrations are additive only. Existing v0.4.8 databases upgrade by running `rig daemon start` on the new daemon.
Building on the v0.4.8 permission-policy foundation, v0.5.0 ships **the built-in policy templates plus the agent-driven skill that translates them into the target harness's live config**.
- **Four built-in policy templates** — `locked`, `standard`, `open`, `yolo`. Read-only from the package; copy to customize. Plus `none` as a deliberate no-policy choice.
- **`rig setup --policy <name>`** — records the chosen policy into a rig spec. Takes a built-in name (`locked | standard | open | yolo | none`) or a path to a custom policy file (custom policies live as `.policy.md` files you author).
- **`applying-a-permission-policy` — bundled skill for the translation.** At rig setup / preflight, the skill reads the policy spec, checks the seat's current harness version, shows the concrete diff before writing, and lands the config into Claude `~/.claude/settings.json` and/or Codex `config.toml`. Never blind writes. The translation is agent-driven on purpose — harness permission formats change frequently across versions, so a deterministic writer would break the moment the harness surface shifts.
- **Two surfaces** — the **launch-flag** surface (Claude `--permission-mode`, Codex sandbox/bypass, Pi `--approve` / `--no-approve`) is stable and OpenRig-set for you: the floor (Claude `acceptEdits`, Codex workspace-write) and the full-bypass YOLO mode. The **config-file** surface (Claude `~/.claude/settings.json`, Codex `config.toml`) is where the allow/ask/deny rules live and where the skill applies your chosen policy.
- **What's deterministic vs best-effort** — the launch-flag floor and YOLO are deterministic. Fine-grained config-file rules are best-effort because harness rule grammars vary; the skill surfaces the caveats to you (prefix collisions, target-first leaks, Claude's lack of a native network gate) via the diff-before-write flow. If a translation is uncertain, you can always fall back to hand-editing the harness's own settings file, or to YOLO / floor as blunt instruments.
### Permission-writer guarantee, now test-pinned
v0.5.0 upholds the v0.4.8 promise — **OpenRig never writes permission entries into your Claude `settings.json`** — and pins it with a permanent guard test plus an empty-writer sweep. The one sanctioned exception is the project-local `acceptEdits` floor that v0.4.8 itself defines. Warning-ordering during permission-policy discovery is cleaner (pre-existing main-floor warnings first, then policy-attachment warnings); presentation-only, semantic fence unchanged.
### Context library
`rig context` stores and composes context; `rig walk` delivers it paced; `--context` / `--body-context` attach a stored context (by reference) to `rig send` / `rig broadcast` / `rig queue create`. Grammar is strict: **nouns store and compose** (`rig context` never delivers on its own); **verbs deliver** (`rig send`, `rig broadcast`, `rig walk`, or `rig queue create` via `--context` / `--body-context`). The old context-window usage viewer from v0.4.x is removed; the `rig context` name now belongs to the library.
### Behavior change (v0.4.8 → v0.5.0)
`rig.yaml` startup `context_pack` entries are no longer delivered at rig instantiation. **They are rejected with a teaching error pointing you at `rig context compose` + a delivery command.** Users who adopted `context_pack` in a `rig.yaml` on v0.4.8 (shipped days ago; small window) should compose the pack via `rig context compose` and then deliver it with a delivery command — `rig send --context`, `rig broadcast --context`, `rig walk`, or `--context` / `--body-context` on `rig queue create`. The bundle router still stores the pack; it just doesn't auto-deliver it at startup.
### Provider usage observability
The daemon tracks account-level usage per host — the "am I about to hit a usage limit" question — exposed via `GET /api/provider/usage` and `rig provider status`. Explicit-unknown when the provider doesn't report it; conflict-shows-both-facts when signals disagree. Codex account-switch flows are preserved.
### Plan amendment done right
`rig scope slice approve --re-approve --reason "..."` re-stamps a locked plan with an append-only audit trail — replacing an earlier workaround where an already-approved status forced a `Status`-note edit.
### Honest CLI output
- `rig ps` says when it is showing one rig of many, rather than silently limiting.
- Activity-hook fix ends the fleet-wide "producer link stale" advisories that were firing on every send. Operator-facing quality improvement.
### Build discipline
Contributor gates and lanes have a single source of truth at `docs/reference/developing.md`.
### UI status (unchanged from v0.4.7)
The web UI stays in maintenance mode as introduced in v0.4.7 — still ships, still runs, no new feature work. CLI/TUI is the primary surface, and v0.5.0's TUI (`rig` / `rig tui`) is where mission-control investment lands going forward. The UI is not deprecated and not removed; existing deployments continue to work.
### Bundled skills
Two new bundled skills join the shipped set alongside the v0.4.8 skills:
- `applying-a-permission-policy` — agent-driven translation for permission policies (see the Permission-policy section above).
- `delegating-work` — distribution guidance for every seat.
The `openrig-user` skill is updated with the v0.5.0 context-library grammar (`rig context compose`, `rig walk`, `--context` / `--body-context`).
## Known Issues
- **Codex `HOME` fix not landed in v0.5.0** — a permission-posture fix that ensures the daemon and Codex seats agree on the `HOME` environment (so the posture writes reach the seat) was accepted for v0.5.0 but was inadvertently omitted from the shipped build. It ships as an early bug-fix in v0.5.1. In the meantime, the v0.4.8 deployment note applies: **the daemon's `HOME` must equal the seat's tmux `HOME`** for the permission-posture writes to reach Codex seats — verify this on any remote-host upgrade.
## Upgrade Notes
- Migrations are additive-only; run `rig daemon start` on the new daemon.
- Any `rig.yaml` still declaring a startup `context_pack` will see the teaching-error rejection above; migrate to `rig context compose` + a delivery command.
## Behavior + Compatibility
- **Migrations** — additive only.
- **`rig.yaml` startup `context_pack`** — rejected at instantiation with a teaching error. Migrate to `rig context compose` + delivery commands.
- **Context viewer removal** — the v0.4.x context-window usage viewer is removed; the `rig context` name is the library.
- **Permission-writer surface** — pinned by a permanent guard test: OpenRig writes zero permission entries into `~/.claude/settings.json` beyond the sanctioned project-local `acceptEdits` floor.
## See Also
- `CHANGELOG.md` — the `[0.5.0]` entry.
- `docs/releases/v0.4.8.md` — the prior release (permission-posture fast-follow).
## 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.
# OpenRig v0.5.7 — elastic topology and coherent work delivery
OpenRig 0.5.7 makes a running topology easier to create and reshape without
hand-authoring YAML, and gives agents a coherent, addressable install of the
work they are joining.
## Elastic topology lifecycle
- **Create without YAML.** Start a rig from public CLI inputs without first
authoring a RigSpec.
- **Grow the topology in place.** Add one or several members to an existing
pod, or create and populate a new pod, with mixed outcomes reported
honestly.
- **Keep the declaration faithful.** Spec show/export preserves the topology,
including a member's declared agent-image source, so save and recreate do
not silently change how that member launches.
- **Shrink safely.** Removal refuses while a target owns active work. An
explicit drain fallback preserves queue-item identity and state, verifies
that the fallback session is live, and removes only the intended seat or
pod. Ordinary no-work cleanup remains direct.
## Coherent work delivery
- **Composable product-journey SDLC.** The shipped work model composes world,
project, mission, and slice context instead of requiring one monolithic
briefing.
- **Useful work-install defaults.** Fresh project, mission, and slice
scaffolds provide conventional Markdown and manifest files without
overwriting authored content.
- **Optional YAML enrichment.** A project manifest can add an authored intent
and ordered context addresses. Invalid optional values warn explicitly and
retain the safe default install.
- **Addressable Markdown loading.** Installed skills can resolve precise
Markdown sections by relative address, including through normal symlinked
plugin projections.
- **Explicit project identity.** Agents can select and switch between projects
without carrying context across roots; ambiguous, duplicate, and unknown
identities refuse instead of guessing.
- **Complete public plugin-only skill catalog.** Every cataloged public skill
supplied only by a plugin is projected with the helper files its
instructions reference.
## Verification performed
- Targeted Node 22 daemon and CLI lifecycle/work-install suites and affected
package builds passed throughout the surviving release range, with
independent QA and scope/maintainability review at the completed user-story
boundaries.
- The product cut at `14e785197` was packaged and installed in an isolated VM.
Daemon health reported that exact clean commit, SQLite `quick_check` passed,
and the existing ready topology was preserved across cutover.
- Disposable public CLI journeys covered create, grow, faithful export,
guarded shrink, explicit drain fallback, and project/mission/slice work
installation. A fresh agent session reconstructed the current five-piece
work frontier through the installed command without cross-project leakage.
- A bounded TUI smoke opened the live topology, exercised help and basic
navigation, and exited cleanly.
- The final release-note-only cut carries no product-source change from the
validated product cut. Its package assembly, hash-bound surface review, and
substance-gate receipt are release-manager gates that must pass before tag or
publication.
## Operator notes
- Database migration head is `077_node_session_source`; normal daemon startup
applies the ordered migration list automatically. No manual migration step
is required.
- Run the exact-cut substance gate and retain its durable receipt before any
tag, GitHub Release, or npm publication. Candidate QA does not imply those
publication acts occurred.
## Known limitations
- Restore-honesty and disaster-path work are deferred to 0.5.8.
Loaded 100 of 2524 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.