Compare commits

...
Author SHA1 Message Date
OpenRig contributors 7d7970fd50 fix(daemon): keep binary files byte-identical in rig bundles
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
2026-10-01 01:14:37 +00:00
Mike SchwarzandOpenRig contributors afde814f5b fix(daemon): keep a blocked row's park timer through a seat handover (#242)
* 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]>
2026-09-30 17:19:00 -07:00
Mike SchwarzandOpenRig contributors cc2c37dc2f fix(daemon): keep UTF-8 intact across herdr socket chunks (#241)
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]>
2026-09-30 16:11:27 -07:00
Rudy Mizrahi Celekli cc02aef23f fix(daemon): preserve executable mode during atomic file edits (#235)
Signed-off-by: Rudy Celekli <[email protected]>
2026-09-30 16:04:49 -07:00
Rudy Mizrahi CelekliandOpenRig contributors 657bc01368 fix(cli): preserve local rsync directory content operands (#231)
Signed-off-by: Rudy Celekli <[email protected]>
Co-authored-by: OpenRig contributors <[email protected]>
2026-09-30 16:04:20 -07:00
Rudy Mizrahi Celekli 9b9cf9ec4c fix(daemon): release SSE subscriptions aborted during replay (#233)
Signed-off-by: Rudy Celekli <[email protected]>
2026-09-30 16:03:50 -07:00
Rudy Mizrahi Celekli 7810d5120a fix(cli): disclose stale environment service receipts (#232)
Signed-off-by: Rudy Celekli <[email protected]>
2026-09-30 15:57:21 -07:00
Rudy Mizrahi Celekli 6678cef5a1 fix(daemon): preserve discovery claims during async rescans (#237)
Signed-off-by: Rudy Celekli <[email protected]>
2026-09-30 15:56:39 -07:00
Rudy Mizrahi Celekli 3dca5ee54b fix(cli): decode SSH and rsync output across UTF-8 chunks (#230)
Signed-off-by: Rudy Celekli <[email protected]>
2026-09-30 15:49:34 -07:00
Mike Schwarzanddev-driver e6bfdce242 fix(transport): distinguish unverified runtime from stopped agent (#240)
Co-authored-by: dev-driver <dev-driver@openrig-build>
2026-09-30 15:24:11 -07:00
d3ad0d1995 fix(daemon): observe native processes independently of the inherited locale (#239)
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]>
2026-09-30 15:14:59 -07:00
Toan NguyenandOpenRig contributors 56a3efeaa1 fix(tmux): finish the printable-separator migration for client and cursor reads (#12) (#179)
Co-authored-by: OpenRig contributors <[email protected]>
2026-09-30 15:10:40 -07:00
Rudy Mizrahi Celekli f45c64f642 fix(daemon): clamp oversized suffix ranges to asset bounds (#234)
Signed-off-by: Rudy Celekli <[email protected]>
2026-09-30 15:04:25 -07:00
Rudy Mizrahi Celekli 8a8a3633ca fix(daemon): keep fenced example headings out of context addresses (#236)
Signed-off-by: Rudy Celekli <[email protected]>
2026-09-30 15:00:29 -07:00
Rudy Mizrahi Celekli c149053c5e fix: Keep CLI request deadlines active through response bodies (#201)
* fix(cli): retain request deadlines through response bodies

Signed-off-by: Rudy Celekli <[email protected]>

* fix(cli): preserve unknown outcomes for unreadable responses

Signed-off-by: Rudy Celekli <[email protected]>

---------

Signed-off-by: Rudy Celekli <[email protected]>
2026-09-30 14:59:53 -07:00
Rudy Mizrahi Celekli 87fc4fa2d5 fix(attach): accept runtime guards for existing nodes (#228)
Signed-off-by: Rudy Celekli <[email protected]>
2026-09-30 14:59:15 -07:00
f056f06a34 fix(daemon): keep ps rows whose command name contains spaces (#82)
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]>
2026-09-30 14:58:44 -07:00
Thomas Bray da2297e13a fix(paths): make module URLs and transcript containment portable (#168)
* Fix module URL conversion and transcript containment across platforms

* Refresh generated integrity digest for portable precompact hook

* fix(transcripts): contain unsafe fallback paths

* test(transcripts): cover unsafe fallback separators
2026-09-30 14:44:47 -07:00
Thomas BrayandMTG-Thomas 812e1e555f refactor(tmux): add shell-free argv execution seam (#167)
* 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]>
2026-09-30 14:44:12 -07:00
Guillaume FossierandClaude Sonnet 5.5 b2679174e5 fix(cli): skip ps for daemon process liveness on Windows (#173)
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]>
2026-09-30 14:43:40 -07:00
c0a322950c test(cli): skip release-surface tag diffs when v0.3.1/v0.3.2 are absent (#152)
* 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]>
2026-09-30 14:43:08 -07:00
JUSHUANGHUI LI 4658c48842 fix(cli): report unknown seat launch outcome on timeout (#191) 2026-09-30 14:39:26 -07:00
Qiaochu Huandhobostay c849d54f5f fix(daemon): leave user config content untouched when stripping codex activity hooks (#186)
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]>
2026-09-30 14:38:54 -07:00
Qiaochu Huandhobostay b3b9bdc09d fix(daemon): project file-shaped entries instead of failing them on ENOTDIR (#185)
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]>
2026-09-30 14:38:24 -07:00
Toan Nguyen 757b204348 fix(kernel): give kernel Claude seats the shared activity hooks (#160) (#178) 2026-09-30 14:37:53 -07:00
Toan Nguyen ed0f323092 fix(daemon): derive terminal pickup for closed qitems (#164) (#176) 2026-09-30 14:37:22 -07:00
Mike SchwarzandOpenRig contributors bcc0010de9 fix: replace stopped same-name rig generations on YAML import (#196)
* 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]>
2026-09-30 14:08:23 -07:00
5177ec8913 fix: provider-aware Codex readiness for non-OpenAI providers (#222)
* 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]>
2026-09-30 13:37:36 -07:00
Mike Schwarzanddev-driver 5b344e0578 fix(claude): preserve unset native config selection (#225)
Co-authored-by: dev-driver <dev-driver@openrig-build>
2026-09-30 12:02:47 -07:00
Mike Schwarzandv-openrig-build b43498d7da docs(contributing): ask security and integration PRs for practical scenarios (#224)
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]>
2026-09-30 11:57:13 -07:00
Mike Schwarzanddev-qa 8b5e948807 chore(release): prepare 0.6.3 regression repair notes (#223)
Co-authored-by: dev-qa <dev-qa@openrig-build>
2026-09-30 11:25:15 -07:00
Mike Schwarzanddev-qa 4dc4047cdb fix(ci): serialize script tests sharing build outputs (#221)
Co-authored-by: dev-qa <dev-qa@openrig-build>
2026-09-30 11:03:08 -07:00
ammann 700af77ae0 fix(cli): protect proof media from artifact writes (#177) 2026-09-30 11:02:23 -07:00
Mike SchwarzandOpenRig contributors 3b1b335dbe fix(daemon): rig remove never kills another rig's live seat; seat refs skip archived rigs (#174) (#181)
* 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]>
2026-09-30 10:23:18 -07:00
Mike SchwarzandOpenRig contributors e3ebd4ac2c fix(daemon): clean up unusable snapshot refresh helpers (#189)
* 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]>
2026-09-30 09:31:52 -07:00
Mike Schwarzanddev-driver 100be9d646 fix: recognize managed Claude seats behind launch wrappers (#220)
* fix: recognize managed Claude behind shell wrappers

* fix: normalize observed Claude executable name

---------

Co-authored-by: dev-driver <dev-driver@openrig-build>
2026-09-30 09:31:13 -07:00
Mike Schwarzanddev-driver f8f3aff68b fix(transport): recognize proven Codex behind shell wrappers (#171)
Co-authored-by: dev-driver <dev-driver@openrig-build>
2026-09-29 17:03:50 -07:00
Mike Schwarzanddev-driver 1618bab05f fix(daemon): bound overlapping identity and transcript polls (#169)
Co-authored-by: dev-driver <dev-driver@openrig-build>
2026-09-29 15:38:24 -07:00
Mike Schwarzanddev-driver b68571318d fix(codex): project shared skills to the Codex target (#162)
* fix(codex): project shared skills to the Codex target

* test(projection): allow runtime-aware resolver wiring

---------

Co-authored-by: dev-driver <dev-driver@openrig-build>
2026-09-29 15:24:58 -07:00
Mike Schwarzanddev-driver 6ab706d435 chore(release): prepare 0.6.2 (#153)
* chore(release): prepare 0.6.2

* docs(release): clarify the default bundle target directory

---------

Co-authored-by: dev-driver <dev-driver@openrig-build>
2026-09-29 13:04:56 -07:00
Mike Schwarzanddev50-guard a745475b07 fix(daemon): preserve managed launch target across pane reuse (#151)
Co-authored-by: dev50-guard <dev50-guard@openrig-build>
2026-09-29 09:46:38 -07:00
Mike SchwarzandOpenRig contributors 18201ac659 fix(transport): don't type into a bare shell where an agent runtime should run (#142) (#150)
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]>
2026-09-29 09:30:28 -07:00
Mike Schwarzanddev-driver aa7bc4407f feat: guide first-project permissions and provider choice (#147)
Co-authored-by: dev-driver <dev-driver@openrig-build>
2026-09-29 09:30:11 -07:00
6b8bee36b1 fix(bundle): install v2 bundles into --target and resolve paths on the client (#146)
* 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]>
2026-09-29 09:29:33 -07:00
Mike SchwarzandOpenRig contributors 705cda2f0e test(daemon): give successful launch fixtures a startup context and adapter (#148)
#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]>
2026-09-29 09:01:59 -07:00
Sunil 5b7e07c906 refactor: reuse shared constantTimeEqual in terminal-ws (#145)
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.
2026-09-29 08:35:15 -07:00
9523b9de02 test(daemon): pin the #131 sender canonicalizer's edges and scope (#144)
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]>
2026-09-29 07:48:56 -07:00
Wu Shuwen dc14d71793 fix(restore): surface missing startup context (#107) 2026-09-29 07:48:37 -07:00
Wu Shuwen 636a4790f9 feat(herdr): set agent hint from seat runtime (#106) 2026-09-29 07:22:37 -07:00
Wu Shuwen e4070e4169 fix(tui): preserve unassessed health in focused views (#102) 2026-09-29 07:22:14 -07:00
Wu Shuwen 1fffd9c988 Fix Claude rate limit reset times (#108) 2026-09-29 07:19:43 -07:00
FenjuFu cb29c1f7ce feat(daemon): allow forwarding ANTHROPIC_BASE_URL to seats (#143)
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]>
2026-09-29 07:19:21 -07:00
Johnny Bae e8e8585be9 fix(restore): honor the configured shared-docs root (#139) 2026-09-29 07:19:03 -07:00
Wu Shuwen f6e5df150b fix(tmux): use printable separators for windows (#101)
* fix(tmux): use printable separator for windows

* fix(tmux): preserve separators in window names
2026-09-29 07:06:10 -07:00
Wu Shuwen 0e416ab4ce fix(codex): canonicalize hook trust paths natively (#103) 2026-09-29 07:05:31 -07:00
Shravan Sumanthanan b3f2b085cd fix(daemon,ui): fix timezone flake, macOS realpath in test, and dumb term runner editing (#123)
* 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
2026-09-29 07:05:03 -07:00
KorallisandClaude Opus 5.5 a09388aabd fix(daemon): canonicalize a self-host-suffixed sender to the local seat (#131) (#135)
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]>
2026-09-29 07:04:36 -07:00
Mike Schwarzanddev-qa d83bbebe97 chore(release): prepare 0.6.1 maintenance notes (#129)
Co-authored-by: dev-qa <dev-qa@openrig-build>
2026-09-29 03:03:08 -07:00
Mike Schwarzandv-openrig-build d7fbf88639 docs(readme): link the guide, help, Q&A and updates on X near the top (#127)
* 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]>
2026-09-29 03:02:20 -07:00
Mike Schwarzandorch-advisor f87a956dbb fix(codex): pass real git dirs, not a worktree's .git file, to --add-dir (#121) (#126)
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>
2026-09-29 01:57:09 -07:00
Mike SchwarzandOpenRig Contributors 7981bca7a3 fix(daemon): recognize custom Codex status-line model fields (#125)
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]>
2026-09-29 01:53:35 -07:00
4a920dba73 fix(daemon): parse occupant boot_at as UTC, not local time (#124)
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]>
2026-09-29 01:52:12 -07:00
Mike Schwarzandorch-advisor a567d9b193 chore: remove the retired TUI drivability spike (#119)
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>
2026-09-29 01:44:40 -07:00
50e519b399 Run installed queue durability checks alongside PR test suites (#117)
* 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>
2026-09-28 22:10:45 -07:00
Wu Shuwen 0dd5a56367 fix(cli): support project scope for execution view (#105) 2026-09-28 21:34:04 -07:00
Wu Shuwen 5d3f62f17b Recognize Codex composer prompt variants (#111) 2026-09-28 21:06:12 -07:00
Mike Schwarzandv-openrig-build 61f13f64ff docs: one help guide for agents (docs/reference/help.md, rig context get help) (#113)
* 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]>
2026-09-28 20:59:08 -07:00
Mike Schwarzandv-openrig-build 95c6c9126c chore: remove the internal .evidence folder from the repository (#118)
.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]>
2026-09-28 20:04:36 -07:00
Mike Schwarzandv-openrig-build ea52fa8f83 fix(specs): add frontmatter to the shipped vault-user skill (#115)
* 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]>
2026-09-28 19:21:34 -07:00
Mike Schwarzanddev-qa 57380a250e Release0.6.0: works for strangers (#109)
* chore(release): prepare OpenRig 0.6.0

Add per-seat permission selection and typing protection, rig-wide Herdr entry,
Node 22/24 support with SQLite 13, and first-use improvements. Label the optional
Jev/Rig Stream classifier and Slack manifest setup assistance experimental.

* docs: finalize 0.6.0 verification scope and release references

Record the accepted Node 22 candidate install, retained Node 24 evidence,
and unperformed native permission checks without broadening test claims.

---------

Co-authored-by: dev-qa <[email protected]>
2026-09-28 14:07:36 -07:00
Shravan Sumanthanan 4fcf2910bb fix(daemon): tolerate missing metadata mapping in readMissionReadiness (#72) (#91)
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.
2026-09-28 02:55:00 -07:00
Mike Schwarzandv-openrig-build 5fead45078 docs: recommend Node 22 on Apple silicon and state platform support (#94)
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]>
2026-09-28 02:48:03 -07:00
ae957e5957 test(daemon): update two fixtures left behind by the process-lineage move (#84)
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]>
2026-09-27 22:25:30 -07:00
Mike SchwarzandOpenRig contributors edc01fe658 fix(codex): launch Codex seats with --no-daemon when supported (#69) (#77)
* 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]>
2026-09-27 18:40:56 -07:00
Mike SchwarzandOpenRig contributors f137f11bef Release 0.5.17: install with Bun (#73)
* 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]>
2026-09-26 23:23:22 -07:00
Mike SchwarzandOpenRig contributors c9be421b9c fix(package): install with Bun without the unpublished @openrig/daemon (#66) (#68)
* 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]>
2026-09-26 22:47:33 -07:00
Mike SchwarzandOpenRig contributors 5c54703035 Release 0.5.16: choose CLAUDE.local.md for Claude managed blocks (#25) (#63)
* 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]>
2026-09-26 02:05:03 -07:00
Mike Schwarzandorch-advisor bee173902a chore: restore Context7 indexing settings after the claim (#62)
Co-authored-by: orch-advisor <orch-advisor@openrig-build>
2026-09-26 01:06:40 -07:00
Mike Schwarzandorch-advisor a52767c1fa chore: reduce context7.json to the claiming fields until the library is claimed (#61)
Co-authored-by: orch-advisor <orch-advisor@openrig-build>
2026-09-26 01:05:11 -07:00
Mike Schwarzandorch-advisor 0ffbf3df48 chore: drop the $schema field Context7's claim check rejects (#60)
Co-authored-by: orch-advisor <orch-advisor@openrig-build>
2026-09-26 01:02:57 -07:00
Mike Schwarzandorch-advisor fbc30c2b6d ci: list file and line only in the public portability report (#59)
Co-authored-by: orch-advisor <orch-advisor@openrig-build>
2026-09-26 01:00:48 -07:00
Mike Schwarzandorch-advisor 6d27f87586 chore: add and configure context7.json (#58)
* chore: add context7.json to claim the Context7 library listing

* chore: configure Context7 indexing scope and usage rules

---------

Co-authored-by: orch-advisor <orch-advisor@openrig-build>
2026-09-26 00:58:16 -07:00
Mike Schwarzandorch-advisor 3b2fc4e624 ci: add a portability report for pull requests (#56)
* 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>
2026-09-26 00:52:05 -07:00
Mike Schwarz 1039f72637 Merge pull request #54 from mvschwarz/community-adoption/readme-badges
README: badges, a short demo of agents working, and star history
2026-09-25 20:18:02 -07:00
v-openrig-build a5e510c51f docs(readme): show the OpenRig TUI in the demo loop 2026-09-26 02:34:30 +00:00
v-openrig-build 100b1630f3 docs(readme): use the seamless demo loop 2026-09-26 01:02:55 +00:00
v-openrig-build b848edc580 docs(readme): keep the four badges on one line 2026-09-26 00:59:34 +00:00
v-openrig-build 3efa12a260 docs(readme): move the demo loop below Install and first run 2026-09-26 00:57:05 +00:00
v-openrig-build f64b735a89 docs(readme): add a 9-second loop of agents working, from the civilizations video 2026-09-26 00:47:24 +00:00
v-openrig-build 3fec689fa6 docs(readme): add npm, license and star badges and a star-history chart 2026-09-26 00:27:13 +00:00
Mike Schwarz 49c1c783a3 Merge pull request #50 from mvschwarz/first-users-0515-final-20260925
Prepare 0.5.15 first-use recovery and Pi fixes
2026-09-25 13:19:40 -07:00
OpenRig contributors 51c0413d7b docs: keep release notes independent of publication timing 2026-09-25 18:50:57 +00:00
OpenRig contributors 0f6d94310c docs: state the first-start retry eligibility precisely 2026-09-25 18:49:30 +00:00
OpenRig contributors 1e74970aab docs: finalize 0.5.15 recovery and Pi release notes 2026-09-25 18:46:22 +00:00
OpenRig contributors ad0b39c60a fix(pi): read framed input beyond canonical terminal limits
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)
2026-09-25 14:47:50 +00:00
OpenRig contributors 4a7c49f4c0 fix(pi): submit pasted messages at explicit input boundaries
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)
2026-09-25 14:06:31 +00:00
OpenRig contributors 608415dc9b Recover an added seat after first-start projection failure
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.
2026-09-25 13:01:16 +00:00
OpenRig contributors 2d1d383010 feat: improve first-use recovery and Pi replies 2026-09-25 12:01:33 +00:00
Mike Schwarz 91a04d7098 Merge pull request #46 from mvschwarz/docs/readme-first-use-20260925
docs: bring installation and first use to the top of the README
2026-09-25 04:00:46 -07:00
OpenRig agent (dev-guard) e5e7e30bd4 docs: bring installation and first use to the top of the README
Adapt the planning draft with prerequisite ordering, explicit response targets
and documentation-index wording. Preserve machine-effects and upgrade text.

Co-authored-by: OpenRig agent (dev-planner) <[email protected]>
2026-09-25 09:48:18 +00:00
Mike Schwarz 6109431f97 Merge pull request #45 from mvschwarz/community-adoption/repo-hygiene-publication-20260925
docs: add contributor, conduct, security, and support guidance
2026-09-25 02:16:08 -07:00
OpenRig agent (dev-guard) ae8cf9c8f2 docs: add contributor guidance and community reporting routes
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]>
2026-09-25 08:59:47 +00:00
Mike Schwarz b374dde300 Merge pull request #37 from danielkuykendall23-boop/fm/openrig-pi-reply-mirror
fix(daemon): show Pi replies in the seat pane with Pi 0.84+
2026-09-24 21:59:18 -07:00
Mike Schwarz 0859caa33c Merge pull request #39 from mvschwarz/docs/openrig-software-factory-20260925
Add Software Factory guidance and agent-configured permissions
2026-09-24 20:29:58 -07:00
v-openrig-build fac88fb27c docs: link configuration disclosure before first launch 2026-09-25 03:08:09 +00:00
v-openrig-build 3f1b95f49b docs: disclose machine configuration changes in README 2026-09-25 02:59:12 +00:00
v-openrig-build efde04f93c docs: preserve chosen Codex permission scope 2026-09-25 02:43:20 +00:00
v-openrig-build 681b5c1aa0 docs: guide agent-configured command permissions 2026-09-25 02:22:48 +00:00
v-openrig-build 01a7d611a0 docs: prefer rig grow for factory teams 2026-09-25 02:16:06 +00:00
v-openrig-build e11959d714 docs: make factory permission guidance portable 2026-09-25 01:21:49 +00:00
Mike Schwarz 634b107f13 Merge pull request #38 from mvschwarz/fix/first-users-startup-20260924
fix: improve Codex startup and restore reporting
2026-09-24 18:05:06 -07:00
v-openrig-build 4940c10253 docs: show incremental growth of a running factory rig 2026-09-25 00:55:00 +00:00
v-openrig-build 350dba4d4d docs: clarify private factory trial and refocus evidence 2026-09-25 00:03:14 +00:00
v-openrig-build b13133420d docs: explain queue-only factory operation and wake limits 2026-09-24 23:53:14 +00:00
v-openrig-build a69cd97279 docs: normalize recipe companion ending 2026-09-24 22:39:45 +00:00
v-openrig-build 767cc7053d docs: add versioned OpenRig Software Factory recipe 2026-09-24 22:39:21 +00:00
OpenRig Contributors 6553aaef3a fix: improve Codex startup and restore reporting 2026-09-24 22:12:02 +00:00
danielkuykendall23-boop 7490587e50 fix(daemon): mirror Pi replies from assistantMessageEvent deltas
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.
2026-09-24 16:26:37 -04:00
Mike Schwarz 9c9e518d29 Merge pull request #34 from mvschwarz/docs/first-users-permissions-20260924
docs: explain agent permissions for first use
2026-09-24 11:56:07 -07:00
v-openrig-build 5f01c17f69 docs: keep first-task walkthrough ahead of optional permissions 2026-09-24 18:02:39 +00:00
v-openrig-build 7eef424446 docs: explain first-user permission choices and launcher precedence 2026-09-24 17:53:40 +00:00
Mike Schwarz c8fca9d5c4 Merge pull request #20 from mvschwarz/docs/readme-devqa-20260923
docs: update stale README details and add TUI visual
2026-09-23 13:00:52 -07:00
v-openrig-build d7703c3035 docs: preserve README guidance and target stale TUI details 2026-09-23 19:48:16 +00:00
v-openrig-build c4a5e1e4aa docs: refresh README for OpenRig 0.5.14 and TUI workflow 2026-09-23 19:27:02 +00:00
v-openrig-build cc75efdd17 docs(release): finalize 0.5.14 inspection and known limits 2026-09-14 22:10:20 +00:00
v-openrig-build 2edc1b7a6d fix(tui): use authenticated catalog read for first entry 2026-09-12 07:24:17 +00:00
v-openrig-build 39a3f8fc57 perf: index inventory restore and startup event reads 2026-09-12 03:43:12 +00:00
v-openrig-build b22c9794b7 fix(tui): distinguish observed rig presence on topology landing 2026-09-12 00:23:48 +00:00
v-openrig-build 83a3422435 perf(daemon): index recent rig review reads 2026-09-12 00:14:12 +00:00
v-openrig-build 452e3f73bc Align release teaching with corrected navigation and mission outcomes 2026-09-11 20:17:42 +00:00
v-openrig-build 8f9c25e0b7 Keep mission NEXT consistent with unfinished assigned outcomes 2026-09-11 19:42:47 +00:00
v-openrig-build 53ca93f746 Keep TUI navigation useful and derive mission outcomes from native facts 2026-09-11 18:29:06 +00:00
v-openrig-build 76d7165e1b docs: describe final 0.5.14 history and read repairs 2026-09-11 13:03:16 +00:00
v-openrig-build 714c676b22 Avoid duplicate recovery lookup in delivery receipt projection 2026-09-11 12:12:23 +00:00
v-openrig-build 96f20a7c2b Reuse proof policy manifest inputs within scopes and execution reads 2026-09-11 11:03:01 +00:00
v-openrig-build c29a180a0e Bound fleet fan-out by one elapsed read budget 2026-09-11 09:38:29 +00:00
v-openrig-build 6474317dbd Preserve canonical human precedence in blocker enrichment 2026-09-11 08:27:09 +00:00
v-openrig-build ddb77d0a7a Keep optional blocker enrichment out of TUI page failures 2026-09-11 08:03:08 +00:00
v-openrig-build bb6337a62c Preserve queue identity provenance through archiving 2026-09-11 04:52:56 +00:00
v-openrig-build 96bc19d715 fix: reconcile release migration fixture and summary help 2026-09-11 02:41:38 +00:00
v-openrig-build d4f8d57c2a fix: keep Projects source errors local to affected entries 2026-09-11 01:26:23 +00:00
v-openrig-build 1dc9d2a78a docs: align capability guide with Feed, System and reliable reading 2026-09-11 00:37:24 +00:00
v-openrig-build 447af5d7f1 release: prepare unpublished 0.5.14 reading and notification candidate 2026-09-11 00:05:58 +00:00
v-openrig-build 979400c29f fix(tui): retain failed Feed sources across refreshes 2026-09-10 23:15:41 +00:00
v-openrig-build 49f1d34e78 Use the canonical human address classifier in TUI blocker reads 2026-09-10 22:34:21 +00:00
v-openrig-build 87c66c1325 Compose human requests and delivered updates in Feed and group instance views under System 2026-09-10 22:22:05 +00:00
v-openrig-build a5d6cc8df7 Compose admitted S02 for 0.5.14 2026-09-10 22:11:38 +00:00
v-openrig-build 9db2180045 Compose admitted S01 for 0.5.14 2026-09-10 22:11:38 +00:00
v-openrig-build a17a37c24b Describe retained settings without claiming a fresh resolution 2026-09-10 21:53:53 +00:00
v-openrig-build 66e4893a56 Keep last successful read time on file refusal 2026-09-10 21:43:20 +00:00
v-openrig-build 679da25f12 Restore the current location highlight after a scoped load 2026-09-10 21:26:03 +00:00
v-openrig-build 0df014ea95 Deliver complete human briefs and distinguish informational updates 2026-09-10 21:19:56 +00:00
v-openrig-build e35ea69962 Enter the observed running rig instead of the first rig 2026-09-10 21:19:27 +00:00
v-openrig-build bce21a1224 Keep TUI reads scoped and useful through entry and refresh 2026-09-10 21:12:24 +00:00
v-openrig-build bda26cb90d test: bind ceremony admission and non-temp home fixtures explicitly 2026-09-10 04:58:51 +00:00
v-openrig-build 354dc83f64 docs: correct bounded historical authority and evidence claims 2026-09-10 04:29:02 +00:00
v-openrig-build bdc14ffa4b fix(tui): correct visible Herdr product spelling 2026-09-10 04:01:28 +00:00
v-openrig-build 20de5dac54 docs: teach 0.5.13 discovery and clarify external destinations 2026-09-10 03:44:39 +00:00
v-openrig-build 0c64e5e40f release: prepare unpublished 0.5.13 human journey candidate 2026-09-10 03:22:43 +00:00
v-openrig-build 698a37e06e fix(tui): wrap empty health page explanation 2026-09-10 02:59:01 +00:00
v-openrig-build 7f73a3e5fd Compose S04 Attention and S08 correction into cumulative Wave B 2026-09-10 02:07:33 +00:00
v-openrig-build a395e4329e Merge S09 Git context into Wave B integration 2026-09-10 01:48:03 +00:00
v-openrig-build 651f05be2f Label retained resolved Attention requests as source records 2026-09-10 01:44:21 +00:00
v-openrig-build 8ae22d43df Separate human Attention from operational signals and expose original sources 2026-09-10 01:28:15 +00:00
v-openrig-build b66ad34bcb Keep CONFIG provenance colors separate from operational status 2026-09-10 01:27:40 +00:00
v-openrig-build 9baa029532 Make CONFIG, Help and health readable with predictable command focus 2026-09-10 01:15:14 +00:00
v-openrig-build 2e40c84a8b Merge S05 terminal views into S03 for S06 presentation 2026-09-10 01:06:51 +00:00
v-openrig-build 36ae8d5d61 Compose S08 correction contract into private S04 on S03 2026-09-10 00:57:31 +00:00
v-openrig-build d379622bc5 Keep explorer focus on the selected project 2026-09-10 00:42:51 +00:00
v-openrig-build 12d9ab672d Describe SSH attach readiness without claiming login verification 2026-09-10 00:39:35 +00:00
v-openrig-build fc4d34bd68 Keep project identity through TUI work navigation 2026-09-10 00:36:51 +00:00
v-openrig-build 7cb19ae77a Expose current correction context and distinguish action from later effect 2026-09-10 00:32:46 +00:00
v-openrig-build 421c0e33a8 Browse terminal views and bind explicit Open to resolved preview 2026-09-10 00:28:48 +00:00
v-openrig-build c873a3c6f0 Bound detail wrapping allocations for long file lines 2026-09-09 23:49:38 +00:00
Dev-Guard 1203d6f053 Add explicit Git context selection and preserving updates 2026-09-09 23:17:21 +00:00
v-openrig-build c15af6caf1 Preserve reader position across startup detours 2026-09-09 23:13:56 +00:00
v-openrig-build a35ef2ca15 Merge S07 posture and retain TUI loading, filtering and input parity 2026-09-09 23:04:44 +00:00
v-openrig-build 170411a589 Merge S01 startup with S02 reading and preserve both reducer paths 2026-09-09 22:59:43 +00:00
v-openrig-build 0fcdc5bcf6 Add scoped human-led and delegated operating posture 2026-09-09 22:48:00 +00:00
v-openrig-build 0151002bc9 Show startup consent controls within constrained terminal height 2026-09-09 22:46:47 +00:00
v-openrig-build 12059e7558 Preserve filtered Specs caller on Back 2026-09-09 22:45:06 +00:00
v-openrig-build 07a6fa2f6e Keep skipped startup data honest and reset local reader on return 2026-09-09 22:42:13 +00:00
v-openrig-build 2cd82e4375 Make Specs previews and current source reading navigable 2026-09-09 22:22:39 +00:00
v-openrig-build d926b3905e Keep local source content visible on constrained startup screens 2026-09-09 22:20:24 +00:00
v-openrig-build fb06b4f506 Keep TUI Help and local reading available through startup checks 2026-09-09 22:14:54 +00:00
v-openrig-build b6c8d8f044 Isolate CLI mission-root and host-home verification fixtures 2026-09-09 12:29:46 +00:00
v-openrig-build 18148212cf Fix release verification fixtures, delivery receipt episodes and CLI license 2026-09-09 12:17:31 +00:00
v-openrig-build c8afda612c docs: repair public reference provenance and discovery 2026-09-09 11:10:02 +00:00
v-openrig-build 5d70fec7db docs: align changelog with public release guidance 2026-09-09 10:11:01 +00:00
v-openrig-build 10b5462513 docs: keep historical migration disposition in project authority 2026-09-09 10:07:57 +00:00
v-openrig-build 7392830214 docs: teach proof judgments and absorb 0.5.12 capabilities 2026-09-09 10:03:53 +00:00
v-openrig-build 2dd355a43c release: prepare exact 0.5.12 workflow and TUI candidate 2026-09-09 09:43:16 +00:00
v-openrig-build d5808f4652 fix(workflow): own and recover dependency failure exceptions atomically 2026-09-09 08:51:38 +00:00
v-openrig-build 86c149fbf5 fix(startup): keep Codex launch commands out of canonical tty input 2026-09-09 07:47:37 +00:00
v-openrig-build 1668a14130 Show attributed proof judgments in ordinary provenance views 2026-09-09 05:54:10 +00:00
v-openrig-build b98566f313 Keep CONFIG search clearing out of navigation history 2026-09-09 04:21:22 +00:00
v-openrig-build c645d7dc60 Match CONFIG arrow hints to actual detail scrollability 2026-09-09 04:11:27 +00:00
v-openrig-build 30a1a397fd Connect the passive CONFIG browser to normal TUI navigation 2026-09-09 03:57:17 +00:00
v-openrig-build 1b0362b1ad Merge commit 'f8aee7678b5acff6ff4336407dbfdca5cba473ca' into r0512-s06-config-browser-dev50guard 2026-09-09 03:42:39 +00:00
v-openrig-build f8aee7678b Deliver full retained obligations to fresh conversations 2026-09-09 03:01:58 +00:00
v-openrig-build bb3f7e1a09 Keep exact resume out of fresh context continuation 2026-09-09 02:49:25 +00:00
v-openrig-build 5ee8fac866 Add passive CONFIG projection and presentation core 2026-09-09 02:43:34 +00:00
v-openrig-build d074d48433 fix(tui): explain pending fresh context before historical resume state 2026-09-09 02:01:56 +00:00
v-openrig-build e28438c91f fix(startup): observe the current native screen for readiness 2026-09-09 01:56:13 +00:00
v-openrig-build 47cf5f467a fix(startup): leave native hook decisions to the operator 2026-09-09 01:51:35 +00:00
v-openrig-build 77ed259abc fix(startup): scope obligation priming to deliberate fresh replacement 2026-09-09 01:39:20 +00:00
v-openrig-build bbe8924acf fix(startup): continue gated fresh context through the same TUI 2026-09-09 01:37:42 +00:00
v-openrig-build d9b46ad04e fix(startup): require an interactive native conversation for readiness 2026-09-09 01:27:07 +00:00
v-openrig-build c74eb648a2 fix(continuity): retain deliberate fresh successor across reboot 2026-09-09 01:14:34 +00:00
v-openrig-build 2fb68fd749 fix(startup): surface native hook review before readiness 2026-09-09 00:55:40 +00:00
v-openrig-build 971580f98f fix(tmux): classify empty server as absent session 2026-09-09 00:05:08 +00:00
v-openrig-build b6f5d7d1fa fix(startup): preserve probed client and classify native refusals 2026-09-08 23:51:00 +00:00
v-openrig-build 3ff7522828 feat(tui): add selective startup through existing lifecycle 2026-09-08 23:32:23 +00:00
v-openrig-build 6893651046 Deliver selected SDLC teaching at workflow returns 2026-09-08 21:49:21 +00:00
v-openrig-build 84e51af6ae Expose exception owner readiness and adopt future routing safely 2026-09-08 20:54:29 +00:00
v-openrig-build 3585614f4f fix(execution): expose authored mission and wave guidance 2026-09-08 20:13:33 +00:00
v-openrig-build 2d974ed849 feat(workflow): inspect and safely revise running lifecycle graphs 2026-09-08 19:42:24 +00:00
v-openrig-build 04830fc9df Show the active delivery recovery before later waiting safety nets 2026-09-08 17:57:53 +00:00
v-openrig-build dea77fea57 Keep healthy workflow waits quiet with shared bounded recovery 2026-09-08 17:28:56 +00:00
v-openrig-build 40af19fb98 Keep proof reader joins scoped to selected item identity 2026-09-08 15:55:31 +00:00
v-openrig-build e4e15c699d Record attributed proof judgments and derive shared readiness 2026-09-08 15:22:57 +00:00
dev-driver ffd5ac1dd7 docs(release): disclose compact diagnosis read defaults 2026-09-07 18:26:45 +00:00
v-openrig-build e821caf2e8 Make diagnosis evidence expansion explicit on accepted diagnostic base 2026-09-07 18:24:47 +00:00
dev-driver 0dc0e0d00c test(daemon): isolate verification fixtures and refresh accepted expectations 2026-09-07 17:25:34 +00:00
v-openrig-build 13ba1212a9 fix(cli): replace unavailable permission diagnostic links with inline guidance 2026-09-07 16:07:58 +00:00
v-openrig-build 6271b26685 docs: scope packaged process guidance and correct sender policy claims 2026-09-07 15:24:34 +00:00
v-openrig-build 9006fc5afd fix(skills): reconcile public guidance and test execution claims 2026-09-07 14:51:30 +00:00
v-openrig-build 20866784f9 Curate public world routes and role guidance without private authority 2026-09-07 14:29:36 +00:00
v-openrig-build 386e515556 Curate portable operating and topology guidance; align chain defaults 2026-09-07 14:16:33 +00:00
v-openrig-build 3949b26c4e Clarify packaged policy, restore, and plugin distribution guidance 2026-09-07 13:45:49 +00:00
v-openrig-build a095f9a36c docs: refresh capability canon for 0.5.11 2026-09-07 12:32:48 +00:00
v-openrig-build f30e7c6bd6 release: prepare 0.5.11 usability and workflow candidate 2026-09-07 11:22:50 +00:00
v-openrig-build 8dbbd09113 fix(tui): keep unverified daemon wording coherent across the screen 2026-09-07 10:56:11 +00:00
v-openrig-build acd7207384 fix(tui): show authored rig summary as spec purpose 2026-09-07 09:16:18 +00:00
v-openrig-build 025ece9e0a Compose accepted everyday TUI with daemon startup and origin attribution 2026-09-07 08:03:48 +00:00
v-openrig-build c3281fede1 Compose accepted daemon startup identity with control-plane and origin attribution 2026-09-07 07:59:08 +00:00
v-openrig-build 4efbaf7ae9 Clarify executor and queue-store custody during cross-instance recovery 2026-09-07 07:23:59 +00:00
v-openrig-build 2df8ab8ce2 tui: add contextual completion and readable local activity 2026-09-07 07:22:08 +00:00
v-openrig-build 64ca6dfd87 Confirm child exit and recheck daemon state publication 2026-09-07 07:21:56 +00:00
v-openrig-build 8673092bec Preserve local origin on direct remote CLI writes during outages 2026-09-07 07:17:36 +00:00
v-openrig-build 49ab324be3 compose: add accepted Connections TUI to workflow recovery baseline 2026-09-07 06:32:05 +00:00
v-openrig-build f0d0b6222f Fix daemon startup ownership and child identity 2026-09-07 06:22:34 +00:00
v-openrig-build d9d9df3787 fix(workflow): propagate capability evidence read failures 2026-09-07 05:00:39 +00:00
v-openrig-build 59d252f67c fix(workflow): resolve human fallback and retire recovered overdue alerts 2026-09-07 05:00:39 +00:00
v-openrig-build c8f0e98f81 fix(startup): retire lean proof before readiness checks 2026-09-07 04:30:05 +00:00
v-openrig-build 213e5a56e5 Honor authored startup proof selection 2026-09-07 04:30:05 +00:00
v-openrig-build 61d4510210 fix(queue): retain source evidence on stuck-sweep findings 2026-09-07 04:13:44 +00:00
v-openrig-build 7bc90ac4e3 Preserve shutdown receipt failures across status and repeated stops 2026-09-07 04:13:44 +00:00
v-openrig-build fd7e3d4ede Bound daemon shutdown and verify the original stop target 2026-09-07 04:13:44 +00:00
v-openrig-build 1642d2b5ab Clarify that workflow receipts record evidence references 2026-09-07 03:30:22 +00:00
v-openrig-build 986a24a60a Make workflow progress and spec consumers navigable in the TUI 2026-09-07 03:25:12 +00:00
v-openrig-build f5b5a9c52d fix: accept consistent runtime aliases in context commands 2026-09-07 02:47:02 +00:00
v-openrig-build a05d1b1a22 fix: make missing-snapshot recovery guidance actionable 2026-09-07 01:11:41 +00:00
v-openrig-build d85004dba3 Add repository first-use starter and shared kernel TUI 2026-09-07 01:11:41 +00:00
v-openrig-build 6f5807b5b8 Resolve Codex process threads through retained native conversations 2026-09-07 00:34:38 +00:00
v-openrig-build efa2a62f33 Correlate Claude walk closures independently of append order 2026-09-07 00:03:51 +00:00
v-openrig-build 9df127c1ae Verify complete walk inputs and current Codex turn records 2026-09-06 23:49:34 +00:00
v-openrig-build 001ce48f04 fix(transport): bracket terminal text pastes at every size 2026-09-06 23:13:25 +00:00
v-openrig-build 53c099b07f fix(workflow): reject prerequisite cycles despite routing hop guards 2026-09-06 21:39:43 +00:00
v-openrig-build ed0b699815 feat(workflow): inherit project release profiles and expose obligations 2026-09-06 21:09:04 +00:00
v-openrig-build b4c833c646 chore(release): prepare private 0.5.10 cut 2026-09-06 15:50:42 +00:00
v-openrig-build b26419182d Route external reply hints through the human queue 2026-09-06 12:21:43 +00:00
v-openrig-build 58ca5c7256 Preserve indeterminate human readiness on invalid daemon replies 2026-09-06 11:54:43 +00:00
v-openrig-build 0ab9722c57 Make human delivery routing and lifecycle outcomes explicit 2026-09-06 11:35:17 +00:00
v-openrig-build 60d494002a Distinguish typed health diagnoses from topical queue tags 2026-09-06 08:14:01 +00:00
v-openrig-build b128514b19 Update starter regression contracts for selected SDLC 2026-09-06 07:21:24 +00:00
v-openrig-build ac21bc2558 Align role startup with authored SDLC component and wave selection 2026-09-06 06:02:03 +00:00
v-openrig-build 11f2e4d405 Compile authored mission workflows and back off unchanged waits 2026-09-06 04:58:10 +00:00
v-openrig-build a4e911ca91 fix(gateway): resolve direct human replies 2026-09-06 02:04:40 +00:00
v-openrig-build 2dc2ed4dce feat: make human delivery observable end to end 2026-09-06 01:43:10 +00:00
v-openrig-build ad0beb148e Merge accepted health and workflow outcomes 2026-09-06 00:43:52 +00:00
v-openrig-build f9c6e1ab9f fix(health): retain uncertainty in false-positive assessments 2026-09-06 00:34:14 +00:00
v-openrig-build 295b395c49 feat(health): derive passive ceremony episodes for agent diagnosis 2026-09-06 00:19:24 +00:00
v-openrig-build 6b679ccfac Calibrate ceremony health against evidenced handoff outcomes 2026-09-05 23:08:19 +00:00
v-openrig-build 3f8eeffc94 fix(health): bind authority context to finding scope 2026-09-05 22:28:37 +00:00
v-openrig-build 0c82ea1128 fix(health): enforce diagnosis custody and evidence boundaries 2026-09-05 22:16:28 +00:00
v-openrig-build 6e625bfb62 feat(health): add agent-operated diagnosis and policy 2026-09-05 21:57:08 +00:00
v-openrig-build 1900aed78f feat(workflow): re-present authored waiting packets 2026-09-05 20:38:29 +00:00
v-openrig-build 539e52a43c fix(scope): let done slices satisfy dependencies 2026-09-05 18:36:36 +00:00
v-openrig-build 8b91f61e60 fix(config): refuse invalid context threshold resets 2026-09-05 17:44:38 +00:00
v-openrig-build d4675396dc fix(health): hide cleared findings by default 2026-09-05 17:10:27 +00:00
v-openrig-build af8f195d0c fix(health): calibrate context pressure and clarify summary 2026-09-05 17:03:32 +00:00
v-openrig-build b6ecc5e7ed fix(tui): retain health severity with state
(cherry picked from commit ec4c21d2db57edb73a5add9f0cc9b22ba5d91444)
2026-09-05 13:29:25 +00:00
v-openrig-build 7ce653991a feat(tui): surface canonical fleet health
(cherry picked from commit d6b16bc408fbb9b479e999aa5f62eb2c997d7049)
2026-09-05 13:29:25 +00:00
v-openrig-build 9717a96257 fix(cli): keep health readiness observational 2026-09-05 13:06:36 +00:00
v-openrig-build f487c56ea0 feat(cli): add read-only health self-scout 2026-09-05 12:27:04 +00:00
v-openrig-build f9671f9838 fix: preserve live context health episode identity 2026-09-05 11:24:52 +00:00
v-openrig-build 599a487861 fix: reset context severity across episodes 2026-09-05 10:40:33 +00:00
v-openrig-build 24aabc4129 feat: add deterministic health detectors 2026-09-05 10:32:56 +00:00
v-openrig-build 7449a9c32a fix: canonically order health revisions 2026-09-05 09:31:27 +00:00
v-openrig-build 7be0589334 fix: bind health replay to S13 evidence 2026-09-05 09:06:11 +00:00
v-openrig-build b21faf409e feat: add canonical health projection contract 2026-09-05 08:49:31 +00:00
OpenRig Agent dbf05f9d59 chore(release): prepare private 0.5.9 cut 2026-09-05 01:57:52 +00:00
v-openrig-build 102dabce4e fix(tui): make instance table continuous 2026-09-04 23:08:33 +00:00
v-openrig-build 2f9c277029 fix: keep attempted resume lineage unverified 2026-09-04 22:25:40 +00:00
v-openrig-build 840b2c4015 fix: preserve resume token through attention recovery 2026-09-04 22:10:13 +00:00
v-openrig-build 2454f1ba88 merge: compose final accepted 0.5.9 source heads 2026-09-04 20:47:58 +00:00
v-openrig-build 2dead87988 merge: compose accepted S15 workflow head
Resolve the generated skill digest conflict by regenerating from the accepted S05 and S15 source trees, preserving both openrig-upgrade and openrig-skills entries.
2026-09-04 20:47:44 +00:00
v-openrig-build aa9afdaf53 merge: compose accepted S14 recovery head 2026-09-04 20:47:02 +00:00
v-openrig-build 2c29f8606c merge: compose accepted S05 migration head 2026-09-04 20:46:49 +00:00
v-openrig-build c2b71be82a fix(tui): retain rig identity on scrolled instance rows 2026-09-04 20:31:11 +00:00
v-openrig-build 2582cd3656 fix: rescope legacy restore reconciliation 2026-09-04 20:28:58 +00:00
v-openrig-build 0d02b9da48 fix(tui): sharpen recent and instance tables 2026-09-04 20:14:11 +00:00
v-openrig-build a7f5388434 fix: reconcile restored seats against attempt 2026-09-04 20:01:58 +00:00
v-openrig-build 34895f7a0c fix(upgrade): refuse incomplete migration evidence 2026-09-04 19:44:21 +00:00
v-openrig-build 28fcd3adcf fix: require structured Claude resume chooser 2026-09-04 19:30:20 +00:00
v-openrig-build 73425ab502 fix(upgrade): preserve finalizer recovery invariants 2026-09-04 19:08:00 +00:00
v-openrig-build 780f88dfae fix restore recovery truth and narrow launch 2026-09-04 19:03:36 +00:00
v-openrig-build 1c0695c81c fix: narrow agent-operated software trigger 2026-09-04 18:48:16 +00:00
v-openrig-build 4a8e49b079 feat: add agent-operated 0.5.9 migration bridge 2026-09-04 18:46:27 +00:00
v-openrig-build e952d4ef3d feat: add agent-operated workflow taxonomy 2026-09-04 18:31:00 +00:00
v-openrig-build b486d6bb96 fix(upgrade): make migration helper CLI explicit 2026-09-04 18:06:27 +00:00
v-openrig-build 9b13bb473a fix(upgrade): bind managed system world identity 2026-09-04 06:48:17 +00:00
v-openrig-build 4330db95f5 fix(upgrade): preserve child symlinks in context migration 2026-09-04 06:22:56 +00:00
v-openrig-build f82397698a fix(upgrade): bind deletion to verified source 2026-09-04 03:57:46 +00:00
v-openrig-build 857d05a0dc fix: bind effective model to current occupant identity 2026-09-04 03:16:14 +00:00
v-openrig-build 47044143e6 fix(upgrade): bind legacy tail deletion 2026-09-04 03:14:43 +00:00
v-openrig-build 8d45b0b07d fix(upgrade): verify telemetry migration convergence 2026-09-04 02:42:22 +00:00
v-openrig-build 128e87deb9 merge: admit S07 resize repair onto S05 successor 2026-09-04 02:20:54 +00:00
v-openrig-build 64854cbfb9 fix(tui): redraw on terminal resize 2026-09-04 02:00:11 +00:00
v-openrig-build 0d1d3afcfa fix(upgrade): handle empty system world scaffold 2026-09-04 01:55:03 +00:00
v-openrig-build 62f53a1934 feat: complete system world migration seam 2026-09-04 01:39:20 +00:00
v-openrig-build a0433001c2 fix(daemon): exclude archived rigs from instance recent 2026-09-04 00:14:56 +00:00
v-openrig-build 7b6a8055d3 feat(tui): add instance mission control 2026-09-04 00:00:39 +00:00
v-openrig-build 93ffd025d2 merge: compose accepted release 0.5.9 heads 2026-09-03 23:32:37 +00:00
v-openrig-build 74ce0c0144 feat(tui): complete founder live QA corrections 2026-09-03 22:59:49 +00:00
v-openrig-build bd2cd16d3a Merge commit 'fb843ffc14c9a19f1cffd854e6566d4395119481' into r059-pre-s11-accepted-compose-dev50qa
# Conflicts:
#	packages/daemon/package.json
2026-09-03 21:14:16 +00:00
v-openrig-build c6c13d0143 Merge commit '3ae2b4af17a7fc241cb6189b9c52cbf2d7953dcc' into r059-pre-s11-accepted-compose-dev50qa 2026-09-03 21:14:00 +00:00
v-openrig-build 49c12479b3 Merge commit '1cc203a4f0eff10a37ed6876b1e0f7c5b56bb24b' into r059-pre-s11-accepted-compose-dev50qa 2026-09-03 21:13:57 +00:00
v-openrig-build 67c2f99ad9 Merge commit 'c03f335abee24521912adeeb8934e9dcc1f45ae5' into r059-pre-s11-accepted-compose-dev50qa 2026-09-03 21:13:53 +00:00
v-openrig-build fb843ffc14 fix(tui): preserve long lifecycle evidence actions 2026-09-03 21:05:09 +00:00
v-openrig-build 27354de779 fix(tui): wrap lifecycle actions 2026-09-03 20:46:07 +00:00
v-openrig-build 1cc203a4f0 merge(s05): compose telemetry and instance bootstrap waves 2026-09-03 20:33:41 +00:00
v-openrig-build 5f7bc3c2cc fix(tui): clear stale scope address state 2026-09-03 20:32:20 +00:00
v-openrig-build 3fd4fbcc09 fix(workflow): expose lifecycle acceptance in CLI 2026-09-03 20:29:21 +00:00
v-openrig-build c03f335abe fix(daemon): report restored skill state after rollback 2026-09-03 20:26:57 +00:00
v-openrig-build 89ff391c98 feat(tui): ship composed production workspace 2026-09-03 20:16:22 +00:00
v-openrig-build 6354806281 fix(s05): harden context library boundaries 2026-09-03 20:06:37 +00:00
v-openrig-build 13a4e1a1d2 fix(workflow): enforce packet lifecycle guards 2026-09-03 20:00:21 +00:00
v-openrig-build 74a822a082 fix(s05): recover interrupted collector rollback 2026-09-03 19:57:31 +00:00
v-openrig-build f9f7e0f00e feat: initialize canonical OpenRig instance layout 2026-09-03 19:49:43 +00:00
v-openrig-build 75ecb6dda7 feat(workflow): productize project lifecycle waves 1-3 2026-09-03 19:25:00 +00:00
v-openrig-build 01a0c27f85 feat(s05): relocate Claude telemetry under state 2026-09-03 19:20:56 +00:00
v-openrig-build 6310d2a792 test(s05): pin telemetry state relocation 2026-09-03 18:52:43 +00:00
v-openrig-build 3d85fcb2bf fix(daemon): use configured skill catalog in preflight 2026-09-03 18:45:41 +00:00
v-openrig-build 3ae2b4af17 fix: preserve watchdog reminder semantics and provenance 2026-09-03 16:02:22 +00:00
v-openrig-build 5820f741bf fix: verify fresh-launch occupant before stop 2026-09-03 14:05:43 +00:00
v-openrig-build aac0f5a33c feat: add explicit single-seat fresh launch 2026-09-03 13:43:53 +00:00
v-openrig-build 5bd400d810 merge: reconcile workspace scaffold and skill loadouts 2026-09-03 11:36:19 +00:00
v-openrig-build 3c1f7a7e53 fix: verify complete foreign skill ignore coverage 2026-09-03 11:05:30 +00:00
v-openrig-build 383ac117f7 fix: isolate managed skill ignores per worktree 2026-09-03 10:46:59 +00:00
v-openrig-build 15e9b9ca2d fix: keep managed skill projections git-clean 2026-09-03 10:17:11 +00:00
v-openrig-build 65155dfe70 Merge OPR.0.5.9.1 workspace scaffold 2026-09-03 10:06:17 +00:00
v-openrig-build b0e2870213 docs(workspace): correct additive scaffold guidance 2026-09-03 09:36:03 +00:00
v-openrig-build 9e37a52fca fix(skills): preserve mode-only projection edits 2026-09-03 09:31:43 +00:00
v-openrig-build fbeeab3682 fix(docs): align init-workspace guidance 2026-09-03 09:21:29 +00:00
v-openrig-build 518a4aee48 Add managed skill catalog and composable loadouts 2026-09-03 09:06:21 +00:00
v-openrig-build 26fdbf5fda feat(workspace): expose project install selectors 2026-09-03 08:43:35 +00:00
v-openrig-build 41d72bb297 feat(workspace): emit canonical project scaffold 2026-09-03 08:34:26 +00:00
v-openrig-build 8770314c59 feat(context): add provider-shaped onboarding profiles 2026-09-03 07:10:37 +00:00
v-openrig-build eeb913bcd1 fix(daemon): name wake-or-escalate in parked-owner notice 2026-09-03 06:08:02 +00:00
v-openrig-build 8cc7fde304 docs: place TUI execution guidance in tracking 2026-09-03 01:50:20 +00:00
v-openrig-build 7cffb9250d docs: absorb 0.5.8 capability delta 2026-09-03 01:42:12 +00:00
v-openrig-build 8b46e6504b docs: make 0.5.8 verification durable 2026-09-02 23:58:21 +00:00
v-openrig-build 04614f4793 chore(release): prepare private 0.5.8 cut 2026-09-02 23:17:07 +00:00
v-openrig-build 226de0064e fix(tui): keep evidence gap count honest 2026-09-02 22:06:39 +00:00
v-openrig-build f97839e7ea fix(tui): count only working execution lanes 2026-09-02 21:31:20 +00:00
v-openrig-build 40c5263cb0 fix(tui): make the SCOPES mission execution rows readable and keep declared state apart from evidence
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.
2026-09-02 21:07:42 +00:00
v-openrig-build ed2f2b6073 chore(gate): restore release gate hermeticity 2026-09-02 20:55:27 +00:00
v-openrig-build df29bb7ef9 docs(sdlc): require release git strategy 2026-09-02 20:29:47 +00:00
v-openrig-build 92a2b17dc9 fix(restore): invalidate stale native identity snapshots 2026-09-02 17:47:39 +00:00
v-openrig-build aa5e187982 fix(restore): recover from stale outage snapshots 2026-09-02 17:47:39 +00:00
v-openrig-build 1b53fb8f37 fix(tui): make scopes escape return to mission 2026-09-02 17:01:17 +00:00
v-openrig-build 4bfe4b039d feat(tui): open mission execution from scopes 2026-09-02 16:36:58 +00:00
v-openrig-build 26d15eb902 fix(cli): resolve work-install slices by dot id 2026-09-02 14:27:58 +00:00
v-openrig-build 8d791840d8 fix(daemon): restore deterministic compaction assets 2026-09-02 13:05:46 +00:00
v-openrig-build 0728e41832 test(daemon): align clean-checkout fixtures 2026-09-02 12:16:00 +00:00
v-openrig-build a2f34be961 test(S16): regenerate eval fixtures for current refs 2026-09-02 11:42:22 +00:00
v-openrig-build 59ce55cb4b test(S16): align clean-checkout fixtures with current contracts 2026-09-02 11:22:51 +00:00
v-openrig-build 5c12506aee test(daemon): align hermetic PATH pin with tmux wrapper 2026-09-02 10:58:52 +00:00
v-openrig-build d32a64f624 test(daemon): restore model-fidelity enumeration 2026-09-02 10:51:35 +00:00
v-openrig-build 6b353ff397 fix(daemon): retain canonical diagnostics for malformed pod roots 2026-09-02 10:35:56 +00:00
v-openrig-build 93777b04ed feat(topology): make self-description honest 2026-09-02 10:35:56 +00:00
v-openrig-build 12a4673602 test(daemon): align provider hook policy pin 2026-09-02 10:07:26 +00:00
v-openrig-build c609d6012b fix(daemon): preserve add-member cwd override 2026-09-02 09:54:56 +00:00
v-openrig-build 6b480a2d54 fix(daemon): converge topology creation effects 2026-09-02 09:54:56 +00:00
v-openrig-build 9bbf80c4f8 test(daemon): state pane identity in partial restore fixture 2026-09-02 09:48:43 +00:00
v-openrig-build 56baef3270 fix(daemon): stop Codex resume matching before prompt 2026-09-02 09:43:26 +00:00
v-openrig-build 98a591bf73 fix(daemon): handle resume-named Codex profiles 2026-09-02 09:43:26 +00:00
v-openrig-build 459499b412 fix(daemon): bind Codex resume lineage to session argument 2026-09-02 09:43:26 +00:00
v-openrig-build 74193d33d9 fix(daemon): verify resumed Codex process lineage 2026-09-02 09:43:26 +00:00
v-openrig-build d70e5ffb25 fix(daemon): require positive runtime evidence on restore 2026-09-02 09:43:26 +00:00
v-openrig-build 1aea7ef973 fix(daemon): require joined identity for restore success 2026-09-02 09:43:26 +00:00
v-openrig-build 96149fa381 test(daemon): state active occupants in restore fixtures 2026-09-02 08:57:25 +00:00
v-openrig-build 045ee97675 fix(daemon): reject semantic registry drift 2026-09-02 08:47:02 +00:00
v-openrig-build 7562cfe6c7 fix(daemon): align human queue admission with registry truth 2026-09-02 08:47:02 +00:00
v-openrig-build 22cb1d7e6c fix(watchdog): preserve needs-input precedence 2026-09-02 08:43:07 +00:00
v-openrig-build 43485186ef fix(watchdog): consume arbitrated activity oracle 2026-09-02 08:43:07 +00:00
v-openrig-build 9be58e1baa fix(tui): make the execution +N more overflow a door to the whole group
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.
2026-09-02 08:35:38 +00:00
v-openrig-build 3ed93ceae6 fix(tui): render execution blocker rows as a relation, not [object Object]
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.
2026-09-02 08:35:34 +00:00
v-openrig-build 2c9180fca6 feat(tui): make the execution view answer DONE / NOW / NEXT / ATTENTION at a glance
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.
2026-09-02 08:35:29 +00:00
v-openrig-build 16e02c4500 test(daemon): mark policy restore occupants running 2026-09-02 07:59:19 +00:00
v-openrig-build 14db43712f fix(daemon): treat blank activity env as absent 2026-09-02 07:26:42 +00:00
v-openrig-build 612d7d0f65 fix(daemon): diagnose producer link from effective endpoint 2026-09-02 07:26:42 +00:00
v-openrig-build 4cb393af54 fix(cli): retain actionable watchdogs in bounded list 2026-09-02 07:25:06 +00:00
v-openrig-build e2c2494fdb fix(daemon): resolve execution mission compatibility tags 2026-09-02 07:22:19 +00:00
v-openrig-build 64952dcbdd test(s16): align built-in policy authority pins 2026-09-02 07:19:51 +00:00
v-openrig-build f5259b9ad4 Bound watchdog list diagnostics 2026-09-02 07:01:42 +00:00
v-openrig-build b9080739af test(daemon): keep idle-gate migration fixture-local 2026-09-02 06:36:16 +00:00
v-openrig-build 5ed8d87ead test(daemon): include idle-gate migration in full fixture 2026-09-02 06:36:16 +00:00
v-openrig-build 350eb22690 fix(watchdog): a shared job is not the timer backstop's to retire
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.
2026-09-02 04:51:40 +00:00
v-openrig-build ea0e80d217 fix(watchdog): refuse legacy terminal-row timers 2026-09-02 04:33:11 +00:00
v-openrig-build 257f47e93b fix(import): validate only workspace on workspace apply 2026-09-02 03:26:25 +00:00
v-openrig-build 5e567caa5b fix(upgrade): compare the whole inventoried file state, and verify seats across all rigs
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.
2026-09-02 03:08:17 +00:00
v-openrig-build 13531c176c fix(upgrade): preserve non-directory prefixes 2026-09-02 02:49:10 +00:00
v-openrig-build d66166ad71 fix(upgrade): contain managed plugin writes 2026-09-02 02:34:35 +00:00
v-openrig-build 600e86a3a0 fix(upgrade): inspect nodes across all rigs 2026-09-02 02:18:28 +00:00
v-openrig-build 2b507555a7 fix(upgrade): inspect the shipped daemon status surface 2026-09-02 01:51:46 +00:00
v-openrig-build 237a03fe37 fix(upgrade): keep cutovers agent-led 2026-09-02 01:47:32 +00:00
v-openrig-build 0f56b4bad2 feat(import): apply workspace to existing rig 2026-09-02 01:37:56 +00:00
v-openrig-build 586158e958 docs(daemon): clarify effective model selection 2026-09-02 01:37:56 +00:00
v-openrig-build b8a1170794 fix(daemon): ignore synthetic model records 2026-09-02 01:37:56 +00:00
v-openrig-build 8d3f9b0896 fix(export): preserve workspace declaration 2026-09-02 01:37:56 +00:00
v-openrig-build 213db2e12f fix(tui): bound quiet topology freshness 2026-09-02 01:37:56 +00:00
v-openrig-build 4822aa5c43 fix(cli): keep TUI reachable across Codex observation gaps 2026-09-02 01:37:56 +00:00
v-openrig-build 4b083a1e24 fix(daemon): bind body-only active slice 2026-09-02 01:37:56 +00:00
v-openrig-build a8d8f37a70 fix(daemon): select body-only active mission 2026-09-02 01:37:56 +00:00
v-openrig-build 04b122d663 feat(tui): add execution mission view 2026-09-02 01:37:56 +00:00
v-openrig-build 4ef47816da feat(context): deliver work install files 2026-09-02 01:37:56 +00:00
v-openrig-build 1e498b4825 docs: absorb 0.5.7 capability world 2026-09-02 01:37:56 +00:00
v-openrig-build f53047d27b fix(context): serve whole-file addresses 2026-09-02 01:37:56 +00:00
v-openrig-build d56ffc585f fix(watchdog): a substantive blocker transition is material at unchanged state
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).
2026-09-01 13:02:34 +00:00
v-openrig-build 0bbb9d9e22 fix(watchdog): bank the idle-gate condition receipt only on positive delivery
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.
2026-09-01 12:19:54 +00:00
v-openrig-build f610eec7d2 fix(watchdog): the idle gate fires once per material state, not once per window
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.
2026-09-01 11:50:44 +00:00
v-openrig-build fdd26628f3 fix(queue): retire a park timer on EVERY exit route, not just the generic update
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.
2026-09-01 11:16:56 +00:00
v-openrig-build 734d863f48 fix(queue): a new park episode supersedes the previous park-generated timer
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.
2026-09-01 11:16:56 +00:00
v-openrig-build 1c519d8b67 fix(queue): a park timer is one-shot and dies with its park episode
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.
2026-09-01 11:16:56 +00:00
v-openrig-build b156f42202 fix(OPR.0.5.8.14): correct the header invariant and name rows in refusals
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.
2026-09-01 10:36:43 +00:00
v-openrig-build 81fa2f4c6d feat(OPR.0.5.8.14): bind refocus to the seat's typed current work
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.
2026-09-01 10:36:43 +00:00
v-openrig-build f4cdb5b8db fix(queue): start an explicit --wake-after interval at arming, not at first scan
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.
2026-09-01 10:18:24 +00:00
v-openrig-build 22a87c1a13 docs(codex): repair two source residues and retract a false unreachability claim
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.
2026-09-01 09:56:03 +00:00
v-openrig-build f0a6b7b3dc fix(codex): refuse a config fragment whose keys precede its first table header
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.
2026-09-01 09:50:40 +00:00
v-openrig-build 600ee4fdf8 fix(codex): find table headers by document depth, not by a leading bracket
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.
2026-09-01 09:22:20 +00:00
v-openrig-build a760ec2737 fix(codex): validate a managed fragment standalone before collision filtering
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.
2026-09-01 09:10:05 +00:00
v-openrig-build ebfe60d90c fix(codex): let the parser decide table collisions, not a hand-written lexer
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.
2026-09-01 09:02:20 +00:00
v-openrig-build 4d2ad86c46 fix(codex): user-owned Codex TOML tables survive config projection
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.
2026-09-01 08:46:05 +00:00
v-openrig-build bda1bba537 docs(sdlc): preserve original user intent 2026-08-31 22:01:24 -07:00
v-openrig-build 9cb5a00e45 docs(release): record 0.5.7 verification 2026-08-31 22:01:24 -07:00
v-openrig-build 4ebd57a360 fix(context): include current work progress 2026-08-31 22:01:23 -07:00
v-openrig-build 068da34e7b fix(walk): forward slice profile grant 2026-08-31 22:01:23 -07:00
v-openrig-build 2067c79834 fix(context): validate markdown address fragments 2026-08-31 22:01:23 -07:00
v-openrig-build 03606ea373 fix(release): restore work-install boundary guards 2026-08-31 22:01:23 -07:00
v-openrig-build 7366e9fca8 Prepare 0.5.7 release lineage 2026-08-31 22:01:23 -07:00
v-openrig-build 0aa9c26c8e fix(context): catalog public plugin-only skills 2026-08-31 22:01:23 -07:00
v-openrig-build 4ecb70b4ec chore(docs): relocate slice planning records 2026-08-31 22:01:23 -07:00
v-openrig-build 6fcb88715c fix(skill): shorten software-for-agents metadata 2026-08-31 22:01:23 -07:00
v-openrig-build 8f3a1af8cb fix(context): refuse duplicate project ids 2026-08-31 22:01:23 -07:00
v-openrig-build f52c7978a0 test(context): admit work-install grammar 2026-08-31 22:01:23 -07:00
v-openrig-build 48a9ce65dd feat(context): select multi-project work installs 2026-08-31 22:01:23 -07:00
v-openrig-build 5914188672 fix(skills): preserve loader import mode 2026-08-31 22:01:23 -07:00
v-openrig-build d13fe67e21 fix(skills): resolve loader entrypoint symlinks 2026-08-31 22:01:23 -07:00
v-openrig-build 0b86028671 feat(skills): ship addressable markdown loader 2026-08-31 22:01:23 -07:00
v-openrig-build 50a51e18a7 fix(context): warn on invalid project intent 2026-08-31 22:01:23 -07:00
v-openrig-build 7f1a095be1 fix(context): warn on invalid project context 2026-08-31 22:01:23 -07:00
v-openrig-build 381db3910d feat(context): compose project manifest enrichment 2026-08-31 22:01:23 -07:00
v-openrig-build a499d65850 feat: scaffold minimal work manifests 2026-08-31 22:01:23 -07:00
v-openrig-build 751a64061f fix(workspace): scaffold project spec 2026-08-31 22:01:23 -07:00
v-openrig-build a2d9b750cf chore(skills): retire obsolete Obra SDLC chain 2026-08-31 22:01:23 -07:00
v-openrig-build 5675e65d88 fix(context): compose legacy work defaults 2026-08-31 22:01:23 -07:00
v-openrig-build d9f27f76e1 docs: ship composable product journey sdlc 2026-08-31 22:01:23 -07:00
v-openrig-build 9517768767 docs: plan composable product journey sdlc 2026-08-31 22:01:23 -07:00
v-openrig-build d2de58e58e docs: design composable product journey sdlc 2026-08-31 22:01:23 -07:00
v-openrig-build 92575c104f fix: verify fallback tmux liveness before removal 2026-08-31 22:01:23 -07:00
v-openrig-build 6e33611968 feat: drain queue work during explicit shrink fallback 2026-08-31 22:01:23 -07:00
v-openrig-build 5bbec015e6 test(daemon): expect session source migration tail 2026-08-31 22:01:23 -07:00
v-openrig-build 46c520b4b9 fix(daemon): preserve member session source on export 2026-08-31 22:01:23 -07:00
v-openrig-build 857b62f8f8 fix(daemon): guard seat removal with active queue work 2026-08-31 22:01:23 -07:00
v-openrig-build 8dc5968d20 fix(cli): disclose partial grow results 2026-08-31 22:01:23 -07:00
v-openrig-build 7633199edd feat(cli): grow existing and new pods without YAML 2026-08-31 22:01:23 -07:00
v-openrig-build 900351141c feat(cli): add the smallest useful rig lifecycle doors 2026-08-31 22:01:23 -07:00
mvschwarz 209030f047 chore(test): refresh AUTHORITY_SHA256 for standard + yolo policy files
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.
2026-08-30 01:39:59 -07:00
mvschwarz 4c68a0f1da docs: v0.5.6 release notes (CHANGELOG entry + docs/releases file)
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).
2026-08-30 01:39:59 -07:00
mvschwarz 2967124039 chore: bump version to 0.5.6
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.
2026-08-30 00:19:43 -07:00
v-openrig-build de9b29d875 test(OPR.0.5.6.1 proof-gap correction, from the R1 HOLD e22e804f at 7ae380ad8, test-only): the exclusivity discriminator's healed phase tracks A, B, AND C across reconstruction and requires each to appear in exactly ONE posted digest with exactly ONE posted receipt — the prior firstRows-only count with a <=1 receipt bound could not refuse a zero-C outcome; every product byte untouched 2026-08-30 00:18:03 -07:00
v-openrig-build 9b708037e8 fix(OPR.0.5.6.1 GREEN, the R1 HOLD c7818ceb repair): pending digest membership is EXCLUSIVE — the flush reads the durable dispatch buffer's pending digest:* decisions and excludes their members from any new mint (the buffer is the recovery-safe truth: a mint lost pre-enqueue leaves no pending entry so its members stay mintable, an enqueued mint redrives to transport truth), making overlap structurally impossible while never-zero holds; a growing member set now mints a non-overlapping digest for the NEW members only; every green v3 seam untouched 2026-08-30 00:18:03 -07:00
v-openrig-build e16e7f7a18 test(OPR.0.5.6.1 RED, from the R1 HOLD c7818ceb at fc952ff64): MEMBERSHIP EXCLUSIVITY — with A+B pending receiptless, adding C must mint a NON-OVERLAPPING digest (pending decisions never share a member; the set-hash identity mints an overlapping A/B/C decision today — the caught double-delivery); after healing, reconstruction replays both exclusive decisions to one human-visible post and at most one receipt per member episode; the preserved same-membership/deferral/ladder-key/transport-truth branches stay untouched 2026-08-30 00:18:03 -07:00
v-openrig-build 7cdb4bb29e test(OPR.0.5.6.1 instrument fix, visible, from the W9 HOLD): the digest-wire fixture counts only chat.postMessage requests as posts — the replay path's reconcile-by-marker channel search rides the same fetch (correct S14 behavior: reconcile before re-send) and was inflating the eventual-post count; non-post requests answer an empty message list 2026-08-30 00:18:03 -07:00
v-openrig-build 3f0905d7aa fix(OPR.0.5.6.1 GREEN v3, the dual-rebind repair — locked exactly-once restored): the dispatcher accepts a caller-supplied DURABLE EPISODE-STABLE decision id (idempotent accept when already pending; the delivered-store re-ack makes replay and re-dispatch converge on ONE post) and the gateway subsystem passes it through; the deferral holds its job ACTIVE through delivery with a distinct dispatching pending state, retries on failure/death (redrive), and goes fired+terminal ONLY on observing the episode's own posted receipt — never zero, never two, with the stable id deferral:<key>; the digest flush posts THROUGH THE GATEWAY on digest:<stable-hash-of-member-keys> — receipts are stamped by the delivery seam's onPosted digest branch ONLY AFTER transport truth (zero false receipts on failure; members stay flushable; the buffer redrives the same durable decision after reconstruction; the S14 retain-and-repair owns the post/stamp boundary); readLadder derives the dispatched key BEFORE evaluating resolution notes and resets provisional resolution at each dispatch marker (the R2 pre-marker discriminator: an older receipt can never close a new rung); startup wires both stable identities and routes the digest through the one gateway transport (callWebApi digest path removed) 2026-08-30 00:18:03 -07:00
v-openrig-build e07846b59c test(OPR.0.5.6.1 v3 RED follow-up, disclosed): §5's original deferral contract updated to the same v3 shape the dual rebind rules — dispatch at T+30 with the job active, fired/terminal only on the observed episode receipt, exactly one delivery across a restart, never zero never two (the v2 fire-equals-terminal expectation was the at-most-once shape both reviews rejected; missed in the first v3 pass, mine) 2026-08-30 00:18:03 -07:00
v-openrig-build f490ad8e59 test(OPR.0.5.6.1 v3 RED, from the dual rebind BLOCKINGs — R1 76a8cfd1 + R2 003f4786): the at-most-once v2 contracts are REPLACED by the locked exactly-once (receipt prose cannot lower the lock, and my S24-doctrine citation was invalid — that contract governs wakes, not this lock): the deferral holds the job ACTIVE through delivery with a distinct dispatching pending state, terminal ONLY on the observed episode receipt, and a death mid-delivery redrives after reconstruction to exactly one delivery (never zero, never two); the digest posts through the REAL wire with receipts landing only AFTER transport truth (zero false posted receipts on failure, members stay flushable, reconstruction replays the SAME durable decision to one eventual post with correctly keyed receipts); the ladder derives the dispatched key BEFORE evaluating resolution notes (R2's pre-marker discriminator: an older keyed receipt already on the row never closes a new keyed rung) 2026-08-30 00:18:03 -07:00
v-openrig-build 7b7383256f fix(OPR.0.5.6.1 GREEN, the R2+R1 BLOCKING union repair): the production composition is LIVE — NEW gateway/operator-delivery-engine.ts (makeOperatorDeliveryEngine: the real operator-rung port deciding via the one engine, dispatching the escalation on the ADVERTISED OUTBOUND_OP to the registered human with the current-episode notification key — synthetic stable operator-rung episode for batons without owner transitions — and resolving only on the episode's own receipt evidence; buildDeferralFirePayload: the shared fire payload carrying the episode key and the consult bypass); index.ts wires the port from the live gateway subsystem into the production tick (gateway-less variants keep the honest unwired floor); startup's fire adapter uses the builder + advertised op + episode key; the deferral RESERVES BEFORE DELIVERING (job terminal + fired transition before the enqueue — no restart boundary can mint two decisions; delivery failure is loud row-side, at-most-once per the S24 doctrine); the delivery seam gains the fire-guard (an already-receipted episode posts nothing — the replay/second-mint belt); containment RECORDS the message-time digest decision as a row transition and the flush consumes those records (never re-deciding from later registry state) with the same reserve-before-deliver order and loud per-member post-failure; the ladder binds operator-rung resolution to the dispatched episode's key (stale receipts never close a new rung; unkeyed injected ports keep the legacy shape) 2026-08-30 00:18:03 -07:00
v-openrig-build 568bc6935e test(OPR.0.5.6.1 §8/§4 RED completion, from the R1 BLOCKING e37ff706 union with R2): the deferral-fire receipt is EPISODE-keyed (never the bare qitemId fallback); ladder resolution BINDS to the dispatched episode key (a stale receipt never closes a new rung; unkeyed injected ports keep the legacy any-note shape); the digest flush consumes RECORDED message-time decisions (containment records delivery-decision transitions; later prefs drift neither drops nor reclassifies members) and reserves-before-delivering (member receipts stamped BEFORE the post, loud per-member digest-post-failed on failure, no silent re-post); §4 lossless test now records containment decisions first per the same design 2026-08-30 00:18:03 -07:00
v-openrig-build de2ef3bd6b test(OPR.0.5.6.1 §8 RED, from the R2 BLOCKING 57abf60f): the production composition must be LIVE, pinned at the repair base dbb405511 — B-1 the deferral-fire payload dispatches the ADVERTISED op through the REAL dispatcher and reaches the delivery seam (plus the structural no-outbound_post pin on startup); B-2 the index composition supplies a production makeOperatorDeliveryEngine port and the REAL tick drives an operator escalation to dispatched-to-engine, a landed post, and receipt-resolved exhaustion; B-3 reserve-before-deliver order (job TERMINAL before the delivery enqueue), the crash-window stays at-most-once with loud row evidence, and the delivery-seam episode-receipt guard posts nothing for an already-receipted episode (the replay/second-decision belt) 2026-08-30 00:18:03 -07:00
v-openrig-build 0f4b4a7b96 fix(OPR.0.5.6.1, from the W4 diagnostics): import the OutboundPostPayload type in slack-subsystem (three use sites, vitest transform could not catch the missing type import) and hoist the lateGatewayDispatch declaration to function scope — its binding site at gateway activation sits outside the sessionTransport block the declaration had landed in 2026-08-30 00:18:03 -07:00
v-openrig-build dcf157e8e2 test(OPR.0.5.6.1): strip the four provenance-comment lines beside the shared fixture per orch-lead — the commit message and queue disposition carry the why; the fixture line stands alone as authorized 2026-08-30 00:18:03 -07:00
v-openrig-build 7cf651a80c test(OPR.0.5.6.1 floor fixture, enumerated, orch-lead disposition 04:59Z): the S14 owner-notification floor's shared registry fixture moves deliveryClass B -> A — prefs now DECIDE (AM-F5 replaced the default stub), and this floor's intent is an interrupt-class human's ALERT mentioning while NOTICE demotes quiet through the dial; one line, nothing else 2026-08-30 00:18:03 -07:00
v-openrig-build 64a970b219 fix(OPR.0.5.6.1, from the W2R HOLD): the focus arm honors register A's interrupt-always for normal traffic (the A-exception the away arm already had — one rule for both quiet modes, exactly as the committed matrix pins); the delivery-state table header reads in the exact lowercase spelling the AM-F4 documentation pin checks 2026-08-30 00:18:03 -07:00
v-openrig-build 9f34da8182 test(OPR.0.5.6.1 fixture corrections, visible, from the W2 HOLD): (1-3) LEVEL IS NOT A MATRIX AXIS — matrix cells run at ALERT (the human-required traffic); the NOTICE demotion semantic keeps its own dedicated test (the A x normal cells were failing on my fixture's level choice colliding with my own dial-demotion pin, not on the engine); (4) the off-cell wire fixture carries the escalation tag — the delivery-termination is the ESCALATION record per A1.1/F-7, never a normal-traffic stamp; (5-7) the structural pins strip comments before grepping — the module docs legitimately NAME the banned literals; zero product bytes changed 2026-08-30 00:18:03 -07:00
v-openrig-build 204d4f7fc1 test(OPR.0.5.6.1 floor fixture, enumerated): the ladder floor's operator-marker assertion follows the pointer retirement — with no engine port wired the marker states the unwired floor honestly (delivery engine not wired) instead of citing S11 as a future leg; one assertion changed, every other floor byte untouched 2026-08-30 00:18:03 -07:00
v-openrig-build 23ca979ef9 feat(OPR.0.5.6.1 GREEN): the delivery rules engine — ONE decision per message (interrupt/notify/digest/log) from the stored A-D register + availability, consuming the F-8 level enum and both dials (no parallel classification, no minted transport literals): NEW gateway/delivery-rules-engine.ts (the 32-cell matrix semantics with F-7 off-is-respected verbatim, the M1 away+B T+30 deferral as a decision property, the single-human termination record, the delivery-state table documented at the seam, whole-field-absent legacy away inference); human-registry gains the validated availability enum with loud conflict refusal; slack-subsystem's two dial-reads collapse into one engine consult (mention = the decided loudness; log/digest contained BEFORE the transport with zero post attempts; away-escalation arms the watchdog deferral; away/off escalation records delivery-termination exactly once per episode; the T+30 fire bypasses the consult so it can never re-defer); policies/delivery-deferral.ts (one-shot on watchdog_jobs, transitions-are-the-state arming/fire records, terminal after the single fire) + policies/delivery-digest-flush.ts (lossless re-derived member set, ONE unmentioned digest post, member receipts ride the S14 posted literal digest-tokened, exactly-once by receipt) both registered in PHASE_D_POLICIES and wired at startup (idempotent 4h/daily digest jobs, late-bound gateway dispatch); queue-wake-ladder's operator rung dispatches through the injected engine port with dispatched-to-engine markers, exhausts only on outcome resolution (posted receipt or termination — the AM-F3 resolution pass), never re-dispatches within an episode, and the S11 cited-not-built pointer text RETIRES; delivery-leg records join the pickup-evidence skip list so receipts never pull a row out of its ladder 2026-08-30 00:18:03 -07:00
v-openrig-build 60bef242a7 test(OPR.0.5.6.1 RED): the delivery rules engine pinned whole at pristine 0d6e65743 — §1 the full 32-cell class matrix (4 prefs x 4 availabilities x 2 modes, arithmetic pin, per-cell ruling cites incl. the F-7 VERBATIM off+escalation cells and the M1 away+B T+30 deferral) + log below the posts dial + the S14 interrupt-dial demotion preserved through the engine; §2 availability as a validated registry key with whole-field-absent legacy away inference and loud conflict refusal; §3 gateway consults the engine before dispatch (off ALERT posts unmentioned with a delivery-termination row; focus quiet-cell absence; C-class rows never dispatched individually); §4 digest lossless/exactly-once (N rows -> ONE unmentioned digest, member receipts ride the S14 posted literal with a digest token, second flush empty) + both policies registered in PHASE_D_POLICIES; §5 the 30-minute deferral armed durably on watchdog_jobs, restart-surviving, firing exactly once then terminal; §6 the operator rung dispatches through an injected engine port (dispatched-to-engine + decision markers, NO exhaustion while the outcome is pending, posted-receipt resolution, exactly-once across the episode) and the cited-not-built pointer text retires; §7 structural pins — one outcome tuple, no transport-literal minting, no seen anywhere, no setInterval/setTimeout in the three new modules (AM-F1 anti-sprawl) 2026-08-30 00:18:03 -07:00
v-openrig-build deda7cb02d fix: reconcile zero-job continuity transitions 2026-08-30 00:18:03 -07:00
v-openrig-build 0a0b992d7a test: pin product zero-job reconciliation 2026-08-30 00:18:03 -07:00
v-openrig-build f6039ae82e test: pin post-custody send failure 2026-08-30 00:18:03 -07:00
v-openrig-build 6b46485708 fix: reconcile continuity jobs and retry cutover custody 2026-08-30 00:18:03 -07:00
v-openrig-build 97bccd96d8 test: pin S20 continuity retry and reconciliation 2026-08-30 00:18:03 -07:00
v-openrig-build e22979dd40 feat(continuity): wire runtime policy actions 2026-08-30 00:18:03 -07:00
v-openrig-build 57bf3e1600 test(s20): freeze continuity runtime seams 2026-08-30 00:18:03 -07:00
v-openrig-build 7d67deaf6c fix(continuity): keep one watchdog pair 2026-08-30 00:18:03 -07:00
v-openrig-build 56b227f763 test(continuity): pin relaunch idempotency 2026-08-30 00:18:03 -07:00
v-openrig-build 4ab2f79367 feat(continuity): ship parseable fire stacks 2026-08-30 00:18:03 -07:00
v-openrig-build 57d7fb8804 test(continuity): pin watchdog message parsing 2026-08-30 00:18:03 -07:00
v-openrig-build 220da29095 feat(continuity): ship apprentice fire stacks 2026-08-30 00:18:03 -07:00
v-openrig-build 6c5dd2d030 test(continuity): pin shipped fire stacks 2026-08-30 00:18:03 -07:00
v-openrig-build 6f36d8d8c6 docs(continuity): teach apprentice handover roles 2026-08-30 00:18:03 -07:00
v-openrig-build 634c1eead6 feat(continuity): materialize apprentice policy 2026-08-30 00:18:03 -07:00
v-openrig-build 057487d857 test(continuity): freeze P4-P6 contract 2026-08-30 00:18:03 -07:00
v-openrig-build 4cd3a35637 fix(daemon): preserve transport failure ledger writes 2026-08-30 00:18:03 -07:00
v-openrig-build 6c7ddd410a test(daemon): pin pending receipt store failure 2026-08-30 00:18:03 -07:00
v-openrig-build be6fb995fc fix(slack): retain failed delivery receipts 2026-08-30 00:18:03 -07:00
v-openrig-build c3e7a83776 test(gateway): pin failed receipt repair 2026-08-30 00:18:03 -07:00
v-openrig-build a4525572c2 fix(queue): clear consumed delivery episodes 2026-08-30 00:18:03 -07:00
v-openrig-build 0756db2c97 test(queue): pin consumed notification episode 2026-08-30 00:18:03 -07:00
v-openrig-build d98b005a0f fix(OPR.0.5.6.14): scope delivery truth to current episode 2026-08-30 00:18:03 -07:00
v-openrig-build c001ffd053 test(OPR.0.5.6.14): keep unchanged wake in episode 2026-08-30 00:18:03 -07:00
v-openrig-build d06274860e test(OPR.0.5.6.14): pin episode and paneless regressions 2026-08-30 00:18:03 -07:00
v-openrig-build 4f4f0d146d fix(OPR.0.5.6.14): preserve partial-schema queue lists 2026-08-30 00:18:03 -07:00
v-openrig-build 953bb35511 fix(OPR.0.5.6.14): complete row-face and pane-bound floors 2026-08-30 00:18:03 -07:00
v-openrig-build a5e4f67382 feat(OPR.0.5.6.14): GREEN — one resolver, one ledger, consulted. NEW gateway/destination-resolver.ts is THE classification seam (D3-extensible, the only route-decision site): @external -> gateway-routable (folded contract preserved); registry-resolved ALIASES (paneless human-*@kernel virtual identities, the live 4-row class) -> gateway-owned with the resolution named; known topology seats (sessions row or nodes-composed name; FAIL-OPEN on an empty topology so fixture/bootstrap DBs keep legacy behavior) -> pane-bound unchanged; neither -> structured teaching refusal naming both checks (tmux never consulted for an address it cannot hold). performWakeSend's inline external branch replaced by the seam. LEDGER: onTransportFailed callback writes slack-owner-notification-transport-failed transitions (subsystem-wired); receipt writes that THROW after a successful post are retained cleanly and repaired on the next tick via reconcile-by-marker with exactly one post (the 8f291c37 retain-and-repair proof branch, both call sites wrapped); deliveryOutcomeFor derives posted/transport-failed/never-posted (120s post window) from transitions; findUndelivered consults the ledger FIRST (posted rows never appear, whatever the nudge literal says - the 34a6ad0b contradiction dies; transport-failed + never-posted carry their own classes; candidates include ledger-failed rows with no nudge literal); getById surfaces deliveryOutcome (null for pane-bound - no key lies); the undelivered route prefers the repo's ledger class over the nudge-literal regex. Fixture repairs disclosed: founder entityId follows the address-local convention (human-founder - what makes the alias resolvable); the stubbed Slack scan serves actually-posted messages (the repair story requires channel truthfulness). Slice suite 10/10 2026-08-30 00:18:03 -07:00
v-openrig-build 227d2e6898 test(OPR.0.5.6.14): RED — fresh base-scoped pins for the REMAINING defect set (folded surfaces ride as floors, never absence REDs). 7 failing at exact defect layers: a registry-resolved human ALIAS (the live 4-row kernel-virtual specimen class) still falls through to tmux; an unknown destination gets raw tmux wording instead of a structured teaching refusal; no named single resolver seam exists (fs-read structural pin - the inline external branch is the only classification); a failed post writes nothing to the row ledger (onTransportFailed absent); the 8f291c37 shape live at the seam - a receipt write throwing after a successful post ESCAPES the deliver fn instead of retain-then-repair-with-exactly-one-post; a POSTED row still appears undelivered on the nudge literal alone (the 34a6ad0b contradiction); the row face carries no delivery outcome. 3 floors passing at base and pinned to stay: @external gateway-owned, pane-bound byte-identical dispatch, receipt-bound episode dedupe. RED at base 74a73c0de: 7 failed / 3 passed, zero environment failures (two instrument fixes disclosed en route: archive-table migration + ts_created column name) 2026-08-30 00:18:03 -07:00
v-openrig-build 6b19f7dbb7 test(OPR.0.5.6.15 fixture repair): MIXED COMMA asserts the verified member absent from the verification-targets CHECKLIST ('- <target>' line shape), not from the whole body - the body's last-transition line honestly echoes the custody-verified note verbatim, so the bare not-contains failed on the disposition's own echo (found live at the re-earn: RED-2 3 failed instead of the pre-registered 2, tip 29/30 with the module conforming; a fixture defect, mine, disclosed) 2026-08-30 00:18:03 -07:00
v-openrig-build b9429b0532 fix(s02-family R2): GREEN - both blockers closed structurally: (B1) custodyVerifiedTargets reads active UNION archived transitions (the daily archiver moves every transition of an aged terminal qitem - the exact class custody rows belong to - while the row keeps participating in the sweep; the archive is never deleted, so the union is the complete audit history); (B2) proof-at-write becomes a two-conjunct check - registered host AND the successor id recomputes via deriveCrossHostSuccessorId from the row's own (qitem_id, handed_off_to, host) - because the generic update route stores arbitrary closure targets and registered-host SYNTAX alone is never forward provenance; the sha256-derived key is what the close path alone establishes after its forwarded create succeeded. Comments narrowed to the honest claims. Unexecuted per the capacity ruling; re-earn recipe: RED child expects DISPOSITION-SURVIVES-RETENTION and FORGED-HOST-QUALIFIED to fail vs 191a10636, this child expects 31/31 2026-08-30 00:18:03 -07:00
v-openrig-build 540605674f test(s02-family R2): RED - final repair bytes for R2's two source-proven blockers: (B1) DISPOSITION SURVIVES RETENTION runs the SHIPPED archiveAgedTerminalTransitions on the aged terminal row (asserting the note is provably gone from the active table) and demands the sweep still honor it; (B2) the TRUSTED case now exercises the real close-write path with the deterministic deriveCrossHostSuccessorId key exactly as routes/queue.ts constructs it, a new FORGED case pins that a generic close writing a registered-host-shaped target WITHOUT the derived key stays verification-required (registered-host syntax is not forward provenance), and MIXED COMMA moves to the realistic disposition-verified member (the cross-host close writes one derived target, never a list). Unexecuted per the capacity ruling - expected RED vs 191a10636: B1 fixture fails (archived disposition forgotten, finding re-mints) and FORGED fails (false negative, no finding where one is owed) 2026-08-30 00:18:02 -07:00
v-openrig-build 403aae9523 fix(s02-family): GREEN — two durable dispositions end the eternal verification-required refresh: (1) proof-at-write, a host-qualified successor key naming a REGISTERED host (id or observed self-id from hosts.yaml, read lazily once per pass) is custody evidence because the cross-host close creates the successor on that host FIRST and records <id>@<host> only after that create succeeded; (2) a prefix-anchored 'custody-verified: <exact-target>' transition note written on the source row by the VERIFIER (never this detector) silences exactly its named target. Unregistered hosts and unverified local misses stay the honest indeterminate class; registry-unavailable degrades to verification-required with one logged line, never a false verdict; finding bodies now teach the disposition recipe; comma members keep member-by-member honesty; condition-cleared auto-close then retires satisfied findings via the existing resolution path 2026-08-30 00:18:02 -07:00
v-openrig-build e93d550043 test(s02-family): RED — final detector-family continuation bytes at pristine f1737b910: a REGISTERED host-qualified successor key is proof-at-write (routes/queue.ts creates the successor on that host FIRST, closes second) and mints nothing; an unregistered host stays the honest indeterminate class with does-not-exist/dangling pinned absent; a durable 'custody-verified: <target>' transition note on the closed row silences the bare-id local miss, closes an already-open finding on the next sweep (which taught the recipe in its body), and silences ONLY its exact target; mixed comma fan-out names only the unresolved member; hermetic default isRegisteredHost=()=>false so no fixture reads the live hosts.yaml 2026-08-30 00:18:02 -07:00
v-openrig-build 128b9880fa fix(OPR.0.5.6.2 fix-round): R1 HOLD repaired — F1: the url_private host check is ANCHORED (URL-parsed hostname, exact slack.com or .slack.com dot-suffix; the substring regex admitted evilslack.com with the Bearer token riding) with a rejection pin proving zero requests leave the boundary for a lookalike; F2: a THROWING file port (ENOSPC mkdirp, any crash) can no longer cost the ACKed message — the transfer await is wrapped at the SEAM and every file becomes a named 'file transfer crashed' failure while the row still lands, pinned with a throwing-port fixture. Both findings JUDGMENT-GAP per R1. 128/128 across the family; build clean 2026-08-30 00:18:02 -07:00
v-openrig-build b66c18334e feat(OPR.0.5.6.2): GREEN — inbound files land as work. Admission: file_share WITH files is the human-upload shape and is admitted; empty text admissible iff files present (a pure drop has no caption); every other subtype and all bot posts rejected exactly as before ('files' leaves IngestReason — files are work, not noise). The router transfers files BEFORE composing the row through an injected InboundFilePort (core stays pure); attachments ride the row body by LOCAL path with name/mimetype/bytes; failures are per-file and NAMED (FILE TRANSFER FAILED: name - error) and a missing port is itself a named failure - the message always survives. Subsystem wires the real port when the bot token exists: authenticated url_private download (Bearer, 25MiB bound, HTML-login-page detection), sanitized bounded filenames, media-dir containment under home/state/slack-inbound-media, no URL or token ever reaches a row. Unmapped threads ride slice 10's unrouted-signal path unchanged, file included. Cutover contract pins updated to the new admission (T1076 replaced by this slice). Gates: slice suite 11/11 + full slack family 126/126 across 17 files (slack-inbound-gate needed the worktree dist built - environment, verified not-my-change); daemon build 0 errors 2026-08-30 00:18:02 -07:00
v-openrig-build c848afabad test(OPR.0.5.6.2): RED — inbound files: 11 pins, 9 failing at the two defect layers. The T1076 seam rejects file_share/files so no row lands (admission pins + every row pin rides the OUTERMOST entry handleEnvelope - a direct route() call cannot hide an admission regression); the attachment mechanism is absent (local-path presence, hash fidelity, per-file named failure, healthy-sibling storage on partial failure, HTML-auth-failure detection, unmapped-thread unrouted-signal reuse with file included, and the URL-absence sweep with its presence half so it can never green vacuously). Two regression floors pass at base and must stay green: bot file posts and non-file subtypes still rejected. UNWIRED helpers ship in this commit so pins fail at the defect, never at module resolution: downloadPrivateFile (Bearer GET, 25MiB bound, HTML-login detection, message-not-exception errors) and makeInboundFilePort (validated slack.com https URLs only, sanitized bounded filenames, media-dir containment, per-file named failures, no URL/token leakage). RED at base 6247a3684: 9 failed / 2 passed, zero environment failures 2026-08-30 00:18:02 -07:00
v-openrig-build db4849f389 test(OPR.0.5.6.23 census, services repair): census REBOUND to current base f1737b910 (rows 1-13 hold — zero sessionSource code lines in the frontier deltas, derivation command in-file) and EXTENDED with the ruled second class member: family rows 14-15 disposition spec.services FIXED-HERE and the six remaining optional top-level families AFFIRMATIVELY-CLEARED by a one-command per-family serialize-hit grep 2026-08-30 00:18:02 -07:00
v-openrig-build 9eae69b444 fix(OPR.0.5.6.23 GREEN, services repair): the codec emits the optional top-level services family at the canonical serialize seam — a completeness-pinned emitter record ({[K in keyof Required<RigServicesSpec>]}) must name every services key, so a new field on the type fails compilation naming the missing key; nested wait_for/surfaces/checkpoints emit present keys only (presence-invariant, mirror of normalizeServicesBlock); no second codec, no schema change 2026-08-30 00:18:02 -07:00
v-openrig-build 6b0db5a59e test(OPR.0.5.6.23 RED, services repair): member (c) — the optional top-level services family dies at the serialize leg (desk ruling transition 45061 on row 0f8e2d7d; R2 HOLD B-1): parse accepts+normalizes services, RigSpecCodec.serialize reads no spec.services, so export-YAML -> re-import silently drops the whole block; fixture populates EVERY optional services field and pins explicit key-set + deep-equal values through the canonical serialize -> YAML -> parse -> normalize door, never mere presence 2026-08-30 00:18:02 -07:00
v-openrig-build 8657fbe293 fix(OPR.0.5.6.23 GREEN): the silent-erasure class closed at both ruled seams — rigspec-codec gains the presence-invariant sessionSource serialization (one enumerated optional-field list driving emission, plus a compile-time completeness check that NAMES any future unserialized ref field as a build error) and emits member compaction_strategy (the disclosed third member: parse carried it, serialize never did); the fork-ingress memberFragment forwards every node-carried optional — model (the 0.4.6.PI1 relaunch-on-default class), role, restore_policy, label — via ForkSourceShape + resolveForkSourceNode + the fragment spread 2026-08-30 00:18:02 -07:00
v-openrig-build 26d8145f80 test(OPR.0.5.6.23 RED correction 3): RT-COMPACTION uses the canonical vocabulary value managed-compaction — harness_native is a deprecated alias the parse leg normalizes, which masked the round-trip pin; defect assertion unchanged in kind 2026-08-30 00:18:02 -07:00
v-openrig-build 2d22695080 test(OPR.0.5.6.23 RED correction 2): the fork composer route replies 201-created on success — status assertion corrected; defect assertions untouched 2026-08-30 00:18:02 -07:00
v-openrig-build 14eb08bf39 test(OPR.0.5.6.23 RED correction): ALL_MIGRATIONS lives in db/all-migrations.js, not db/migrate.js — harness import fixed; assertions untouched 2026-08-30 00:18:02 -07:00
v-openrig-build 19984f5362 test(OPR.0.5.6.23 RED): silent-erasure class pinned at pristine 1f6675b09 with the committed leg census — RT-VERSION (session_source.ref.version dies at the serialize leg, the WAVE1 R2 HOLD shape, absence pinned in the YAML encoding too); KEYSET-PIN (every union arm's serialized ref key-set must equal its input key-set); RT-COMPACTION (disclosed third member at the same seam: parse carries compaction_strategy, serialize never emits); FORK-FRAGMENT (desk-ruled member-b seam: the fork memberFragment drops node-carried model/role/restore_policy/label — the model pin is the 0.4.6.PI1 class — with forwarded-field regression floor); census artifact dispositions all 13 leg groups incl. the bundle-router LOUD SKIP and the checked-absent S20 R-b occupant-generation trigger 2026-08-30 00:18:02 -07:00
v-openrig-build cb2552016b test(queue): bind heartbeat writer absence 2026-08-30 00:18:02 -07:00
v-openrig-build c4ef74c13a fix(queue): supersede row heartbeat writer 2026-08-30 00:18:02 -07:00
v-openrig-build 91672cb0e4 fix(s22): teach executable registered-human parks 2026-08-30 00:18:02 -07:00
v-openrig-build 1ef04a92bc test(s22): use public human host blocker 2026-08-30 00:18:02 -07:00
v-openrig-build 487f4888e8 test(s22): bind human blocker to registry identity 2026-08-30 00:18:02 -07:00
v-openrig-build 72d974752b feat(skills): complete public router coverage 2026-08-30 00:18:02 -07:00
v-openrig-build 5249702e40 test(skills): pin public router coverage 2026-08-30 00:18:02 -07:00
v-openrig-build c2bf3d3838 fix(slice27-repair2): GREEN — both second-HOLD seams at one child: Q1/Q6 consume SeatActivityService's arbitrated seat-keyed read (startup injects seatActivityService; the canonical working/idle-at-prompt/unknown vocabulary and separate needsInput {count,reason} pass through untranslated — no local re-arbitration, no legacy-store consumption, sessions.status untouched as roster-names-only; INDETERMINATE reserved for no-arbitrated-answer); Q4 candidate identity is commit-aware — sha tokens extracted from mixed production forms (abbreviated tags, full shas, annotated fields) and resolved via git rev-parse --verify token^{commit} through the repo context, legs joined by RESOLVED commit with malformed/ambiguous/non-resolving inputs excluded-with-reason (never prefix-matched, never poisoning), built rung carries token + resolved_commit, folded/adopted test the resolved id. 17/17 + view family green, daemon tsc clean 2026-08-30 00:18:02 -07:00
v-openrig-build 817f238ef9 test(slice27-repair2): RED — the two second-HOLD seams, causal at d95ad0c22e (6 failures): SEAM A, the locked one-oracle contract — Q1/Q6 consume SeatActivityService's ARBITRATED seat-keyed read (fake reshaped to getSeatStateBySession); working pinned against the superseded/stale-hook counterexample; idle-at-prompt and unknown pass through as canonical vocabulary (unknown is a member, never rewritten to INDETERMINATE, which is reserved for no-arbitrated-answer); needsInput {count,reason} rides beside activity, never folded in; Q6 capacity = arbitrated idle-at-prompt with zero needsInput (a needs-input seat is not capacity). SEAM B, commit identity — the fixture's built tag becomes ABBREVIATED (the production shape); the full-sha and annotated (S20-specimen form) artifacts must both join by RESOLVED COMMIT (legs=2); malformed (not-a-sha) and non-resolving (all-zeros) inputs floor excluded-with-basis, neither clearing nor poisoning; the built rung carries the tag token plus resolved_commit 2026-08-30 00:18:02 -07:00
v-openrig-build eab6ad40b6 fix(slice27-repair): GREEN — the five HOLD seams repaired at one child: Q1/Q6 consume the injected agent-activity ORACLE (deps.activity, startup wires agentActivityStore; sessions.status never consulted — roster names only; absent evidence floors INDETERMINATE with the wiring-vs-evidence distinction named); Q4 reviewed scopes legs+verdict to the BUILT candidate sha (off-sha history excluded both directions, no-candidate floors INDETERMINATE); Q2 counts blockedOn only on LIVE rows (terminal-row blockedOn is record, not state) and own-completion INDETERMINATE yields next_up INDETERMINATE with the rung's basis carried — never true from ignorance; Q5 park_kind is the closed lowercase enum (deliberate-with-wake|stalled|indeterminate) by construction. 15/15 + view family green, daemon tsc clean 2026-08-30 00:18:02 -07:00
v-openrig-build 7519b60f5d test(slice27-repair): RED — five live-truth seams from the Guard HOLD, each causal at candidate 2c54c2205: (1) Q1 activity must come from the agent-activity ORACLE (the superseded-label specimen pinned: sessions.status superseded while the oracle says running) and an oracle flip lands next read with never-seen seats flooring INDETERMINATE; (2) Q6 idle capacity counts oracle-idle seats, not sessions.status; (3) Q4 reviewed scopes legs/verdict to the BUILT candidate sha (off-sha BLOCKING excluded; no-candidate floors INDETERMINATE); (4) Q2: own-completion INDETERMINATE never yields next_up=true and terminal-row blockedOn cannot suppress dispatch; (5) Q5 park_kind constrained to the DESIGN's closed lowercase enum, exercised on a working-pickup row. Plus the RECEIVER discriminator: GET /api/views/execution?mission through the HTTP route — CONFIRM-OR-REFUTE: predicted green at candidate (connectivity was reviewer-verified; the gap was coverage) and confirmed green at run 2026-08-30 00:18:02 -07:00
v-openrig-build d7aa062dbf feat(slice27): GREEN — rig view show execution: a built-in document view (the S04 pickup-family pattern) deriving Q1-Q6 at read time from queue DB + slice frontmatter + per-lane git + statfs + the daemon's own build stamp; EC-3 worktree_path body-line is Q1's join key (legacy batons marked fragile_join with the basis named); EC-2 wave-map-v1 rows + approved-spec-dial frontmatter derive Q3; EC-1 depends_on/SOFT-AFTER derive Q2 with out-of-mission deps resolved to their own release dirs; the completion ladder (locked/built/reviewed/folded/adopted) is the schema — no done boolean, folded from live merge-base in any reachable EC-3 worktree (refs shared), adopted INDETERMINATE on an unstamped dev daemon; severed/unconfigured sources floor INDETERMINATE with a named basis, never idle/dead/done; derived_at + per-source asof + cell-level source ids; no cache, no scheduler, no new settings keys (review artifacts via the shipped shared-docs root precedent); mission scoping threads CLI --mission -> route query -> projector opts; sdlc-conventions gains the DISPATCH DATA row (EC-1/2/3); view name-list test extended to 8 2026-08-30 00:18:02 -07:00
v-openrig-build f0de59011b test(slice27): RED — the execution view answers Q1-Q6 in one derived read: EC-3 worktree_path is Q1's join key (legacy batons marked fragile_join); EC-2 wave-map-v1 row + approved-spec-dial frontmatter derive Q3 with the wave row cited as source; EC-1 depends_on + SOFT-AFTER lines derive Q2 with absent-EC-1 flooring INDETERMINATE; the completion ladder derives folded from live merge-base and floors adopted INDETERMINATE on a dev daemon with no 'done' boolean anywhere; liveness and park-wake flips derive on the next read with zero authored updates; severed sources floor INDETERMINATE never idle/dead/done; derived_at + per-source asof + cell-level source ids on every response. RED at base: view 'execution' is not registered (view_not_found) — the view does not exist 2026-08-30 00:18:02 -07:00
v-openrig-build e1e3c6e798 test(mirror): permission-drift assertions accept both supported rsync itemization widths — the live-edge regex at the three-edge authoring test becomes /^\.f...p(?:...|.....) scripts\/run\.sh$/ (exactly 9- or 11-char fields, p fixed at index 5, path anchored; a 10-char neither-width form still rejects), and a new injected-exec discriminator pins the production seam: checkModeAbsolute detects the long rsync-3.x and short openrsync-2.6.9 permission forms identically and stays silent on time-only drift in both widths plus noise lines. Production untouched — parseChanges and the regeneration mode classifier already key on .f+p@5, width-agnostic (verified at source, both filter sites). Repro at base on this openrsync host: 26 pass / 1 fail at the rigid regex; at candidate: 28/28. Base exact main 2297cff60 2026-08-30 00:18:02 -07:00
v-openrig-build a09a39c5de fix(s11): reject inline manifest comments 2026-08-30 00:18:02 -07:00
v-openrig-build e46da016f4 test(s11): reject inline manifest comments 2026-08-30 00:18:02 -07:00
v-openrig-build 941a1f5c08 fix(s11): census manifest prose comments 2026-08-30 00:18:02 -07:00
v-openrig-build 9caedd310f test(s11): reject uncensused manifest comments 2026-08-30 00:18:02 -07:00
v-openrig-build aaef628794 fix(s11): cover authored manifest claims 2026-08-30 00:18:02 -07:00
v-openrig-build a5adb3f3da test(s11): pin manifest claim universe 2026-08-30 00:18:02 -07:00
v-openrig-build 37f4166bf5 fix(s11): freeze authored claim census 2026-08-30 00:18:02 -07:00
v-openrig-build 217e645374 test(s11): isolate claim drift fixtures 2026-08-30 00:18:02 -07:00
v-openrig-build e146034f31 test(s11): pin complete claim census 2026-08-30 00:18:02 -07:00
v-openrig-build 54a905ff95 docs(s11): teach software-shaped worlds 2026-08-30 00:18:02 -07:00
v-openrig-build 215a7909f8 test(s11): pin software-shaped bridge 2026-08-30 00:18:02 -07:00
v-openrig-build 11e3ba0413 fix(s11): prove shown verifier path 2026-08-30 00:18:02 -07:00
v-openrig-build 089ad64ef9 test(s11): pin shown verifier relation 2026-08-30 00:18:02 -07:00
v-openrig-build fa2ba17ac1 fix(world): verify public pack from agent perspective 2026-08-30 00:18:02 -07:00
v-openrig-build 51c5ee5d3d test(world): expose public pack truth seams 2026-08-30 00:18:02 -07:00
v-openrig-build ba28400941 fix(context): clean unindexed verifier probe 2026-08-30 00:18:02 -07:00
v-openrig-build a7c5f03910 test(context): pin partial-sync cleanup 2026-08-30 00:18:02 -07:00
v-openrig-build a34da78712 fix(context): verify public world install effect 2026-08-30 00:18:02 -07:00
v-openrig-build 20c82f7a0d test(context): pin public world add failure 2026-08-30 00:18:02 -07:00
v-openrig-build 7e9d03b309 fix(context): make public world checks truth-sensitive 2026-08-30 00:18:02 -07:00
v-openrig-build dca8fe0f94 fix(context): make world claims truth-sensitive 2026-08-30 00:18:02 -07:00
v-openrig-build 364d8113ec feat(context): ship public world pack 2026-08-30 00:18:02 -07:00
v-openrig-build 4a10a0328a test(context): use supported world pack assets 2026-08-30 00:18:02 -07:00
v-openrig-build 579893609f test(context): define public world pack RED floor 2026-08-30 00:18:02 -07:00
v-openrig-build 53fc4bde71 docs(F7-13 fix-round): Edit 1a closing sentence matches the amended authority — 'When the ground disagrees with the map' replaces the superseded 'terrain diverges from the sketch' (vocabulary stays inside the map/ground pair the paragraph establishes; authority amended ~22:17Z between builder read and commit, desk-owned race). One sentence, one file; line-anchored verbatim proof re-earned against current crux lines 220-227 2026-08-30 00:18:02 -07:00
v-openrig-build 89a2185d1c docs(F7-13): append the two ruled wave-sdlc amendments verbatim — DISPATCH gains map-not-territory (brief is never the receiver's only context; self-onboard from routed reading, raise terrain divergence as a finding); WAVE REVIEW gains context-gap/judgment-gap disposition (one word in the verdict artifact; the rate is the calibration signal). Exact texts from the crux EXECUTION appendix lines 220-236, word-verbatim (line-anchored whitespace-normalized proof); obsolete never-re-distills formulation proven absent at base (zero distill-matches); base exact main 1f6675b09 2026-08-30 00:18:02 -07:00
v-openrig-build 09cfff5286 fix(S24-R2b): consumer-ladder eligibility survives the ladder's own overwrite — origin moves to the row's durable FAILED transition note (the ladder retry legitimately rewrites last_nudge_result with generic transport detail; the column now carries failed-CLASS only, the note carries ORIGIN); the B2 fixture becomes the real-persistence discriminator (entry -> generic overwrite proven -> re-entry -> cap -> escalation-rung reachable); the two stale startup comments now teach the repaired behavior (row-transition receipts; born-armed) 2026-08-30 00:18:02 -07:00
v-openrig-build 68e65a7f39 fix(S24-R2 run): three green-run repairs — keyOfNote anchors on the note PREFIX so body words can never masquerade as the episode key, and all four writer sites drop the redundant word (the close-parse bug that kept re-parks reading already-woken); B3 fixture registers a REAL watchdog job (history rows are FK-bound); B2 fixture back-dates the failed attempt past the retry interval (the ladder due-gate working as designed) 2026-08-30 00:18:02 -07:00
v-openrig-build e9a636a6dd feat(S24-R2): approved four-blocker repair — receipts become ROW-SIDE transitions written reserve-before-deliver (at-most-once stated honestly; durability = the retention active-frontier invariant, telemetry back to telemetry); B1 delivery-boundary recheck via a fresh listOpenIds read + terminal-race append guard producing the exact SPEC cell; B2 failures reconcile onto the row in the ladder native failed:-vocabulary and ONE ladder scan-arm (advisor-ruled one-engine) takes claimed in-progress consumer-failure rows into the SAME flow (usage-park mutation guarded pending-only); refusal cell durable on the row, matched only against the transport interactive-prompt literal; late-rig born-armed via RigRepository.onRigCreated wired to the same ensure (birth-property ruling) 2026-08-30 00:18:02 -07:00
v-openrig-build 0ea4edc4f2 test(S24-R2): the five hard-check fixtures at their ACTUAL seams + the unit contract reshaped to row-side receipts — B1 boundary-read race + terminal-race guard (zero reserve); B4 crash-after-reserve restart over a shared durable store (at-most-once, no duplicate; lost-wake arm stated, exactly-once unclaimed); B3 real pruneWatchdogHistory run (15d-aged + 60 newer rows) with the row receipt surviving and deduplicating; B2 real runWakeLadderTick attempting the wake for a failed-consumer-nudge row; late-rig born-armed createRig fixture; episode ordinal/churn/set-change, starvation guard, refusal-vs-generic reconciliation onto rows, cells, pins, quiet floor 2026-08-30 00:18:02 -07:00
v-openrig-build 548a9c4cef test(S24-F14): fixture repair per the 18:35Z release, ponytail two lines — P1 inspects notes.skippedSeats (the rig-level contract's per-seat cell home); SEAT2 constant renamed review-r9@test-rig once so deterministic iteration reaches the receipted seat before the eligible one in both same-pass cases 2026-08-30 00:18:02 -07:00
v-openrig-build 99757f03b7 feat(S24-F14): GREEN v2 per the ruled plan (41985) — history-derived episodes, whole-diagnosis inheritance, honest cells, rig-job ensure: the policy consumes diagnoseRigParked whole (open items + unhealthy HELD; S21 indeterminate inherited); episode identity = seat|idsHash#ordinal derived purely from durable history rows (open sent receipts survive restart; not-parked observations record closures riding same-pass send notes or one bounded episode-ended skip; obligation-set change = new key); refusal cell reads the persisted deliveryReason and matches only the transport interactive-prompt literal; deterministic iteration past receipted/deferred/refused seats (no starvation, activeWakeInterval null); per-rig job ensured idempotently at startup on the (policy, parked-owner-consumer@rig) tuple; engine gains exactly the two ruled effects (no-parked-owner QUIET entry + delivery.error persisted into sent-row notes) with both engine-side pins; consumer tests rewritten to P1-P5 + retained R-cases 2026-08-30 00:18:02 -07:00
v-openrig-build b8f464559e feat(S24-F14): GREEN — the parked-owner consumer as one policy on the existing engine: arbitrated-verdict x open-obligations join, one send per park episode with named row ids, honest skip cells (indeterminate/not-parked/usage-limit-defer-s16/no-open-obligation/already-woken), in-process episode receipts with the engine throttle as restart backstop; one PHASE_D_POLICIES name; startup injection adapting seatActivityService + queueRepo (the activity.ts join reused, zero raw evidence reads) 2026-08-30 00:18:02 -07:00
v-openrig-build 29115db6d1 test(S24-F14): contract correction — four expectation literals toBe("fire") -> toBe("send"), the native PolicyEvaluation action (ruled 18:06Z; names/comments/other assertions and every production byte untouched; the bought missing-module RED transfers without rerun) 2026-08-30 00:18:02 -07:00
v-openrig-build 20511d8800 test(S24-F14): RED — opening test bytes for the parked-owner consumer (ruled R1-R5): the 2026-08-29 three-row specimen (claimed-rows x arbitrated-idle -> one fire naming every held row), once-per-episode both directions, honest skip cells (obligation-closed, usage-limit S16 deferral, S21 indeterminate inheritance), arbitrated-only + no-second-scheduler structural pins, quiet-scan floor; RED at base d1d8f7059 by missing module, assertion contracts activate at GREEN (one naming pass, the S20 convention) 2026-08-30 00:18:02 -07:00
v-openrig-build 7cf1a17674 docs(F-15): land the founder-authored ceremony-ratchet amendments verbatim — wave-sdlc DISPATCH step body replaced (the brief is the unit: goal + acceptance criteria + context routes + territory + standing autonomy grant; whole-spec default scope, named-reason smaller chunks); WAVE REVIEW gains the drift-watch duty (plot-loss is a first-class finding); product-management-pass 'Addressing the wide-angle seat' gains the bidirectional protocol paragraph (context-not-instructions applies orchestrator-to-builder in mirror). Exact texts from CEREMONY-RATCHET-AND-BUILDER-AUTONOMY-20260829.md EXECUTION section, word-verbatim (whitespace-normalized proof in candidate receipts); base exact main a7667c474 2026-08-30 00:18:02 -07:00
v-openrig-build a490a9e0b3 fix(OPR.0.5.7.1 ponytail static correction per 18:04Z ruling): the pre-continuity D1 resolution is a single top-scoped const in restoreNodeWithCompensation; the later pre-launch block reuses it and its duplicate resolve+ambiguity branch is removed (only resolved/none reach it) — net one resolver call per node, policy/fresh checks and restoring/degraded semantics byte-preserved, test bytes frozen 2026-08-30 00:18:02 -07:00
v-openrig-build 907cf71b81 fix(OPR.0.5.7.1 continuity-order GREEN per baton 0efd154d): D1 present-map ambiguity is detected BEFORE the continuity_state restoring/degraded short-circuit in restoreNodeWithCompensation — an ambiguous occupant truth returns the existing shared loud failure (zero resume, zero launch, zero replacement) instead of the silent 'fresh' skip that downstream classified as running; resolved/none/legacy resolutions fall through unchanged, so restoring/degraded semantics are byte-preserved for every non-ambiguous state; one file, test bytes frozen 2026-08-30 00:18:02 -07:00
v-openrig-build 96380d3c7b test(OPR.0.5.7.1 continuity-order RED per baton 0efd154d): one focused fixture pinning that D1 ambiguity failure PRECEDES the continuity_state=restoring skip — pod node + live restoring row + present authoritative relation in explicit-null two-running state must fail loudly with the shared ambiguity wording naming both rows, zero resume, zero NodeLauncher createSession, zero replacement; base returns the silent 'fresh' skip (~:796-816) before the resolver runs. New file only; every existing D1/D6a byte and test untouched 2026-08-30 00:18:02 -07:00
v-openrig-build 1dd16208e9 test(OPR.0.5.7.1 R2 proof-gap repair per baton a32faf4c): R-E2's title now claims only the several-running case its setup constructs; new R-E2b drives the ZERO-running live no-snapshot case through the existing harness — session rows registered with none running and a token on a historical row, asserting for both fresh variants that the preview renders awaiting-decision + freshRequired=false + tokenState missing + a relation/occupant reason, proving explicit-null zero-running behavior and that the historical token is never borrowed and --fresh cannot override; test-only, one file 2026-08-30 00:18:02 -07:00
v-openrig-build 5dce71baee test(OPR.0.5.7.1 P3a fixture adaptation per ruling): setup-only occupancy premises in restore-plan-preview.test.ts — row() defaults status running (m1), the pre-45 hand-cast literal states it too (m2), and the live-path fixture calls updateStatus running after registration (m3); zero assertion, name, production, or other-test bytes 2026-08-30 00:18:02 -07:00
v-openrig-build d7c31e383d fix(OPR.0.5.7.1 consumer-alignment GREEN): ONE active-occupant truth for execution, preview, usability, and lifecycle — new pure leaf active-occupant.ts holds the four-way ladder (generic core resolveActiveOccupantRow + the byte-faithful resolveActiveSnapshotSession wrapper + the ONE ambiguity wording + the shared deriveActiveSessionIdByNode capture rule); restore-orchestrator extracts/imports/re-exports (D6a bytes and all three call sites untouched); snapshot-capture swaps its local derivation loop for the shared helper (no semantic change); restore-plan-preview resolves the occupant per node (relation from the snapshot, or the SAME would-capture derivation live), intendedAction/tokenState derive from the occupant only, broken relations render awaiting-decision + freshRequired=false + the shared ambiguity wording and BEAT --fresh, dead byNode reduce removed, stale newest-wins comment corrected, live SELECT gains status; rig-repository usability = at-least-one node resolving to a token-carrying occupant (any-historical-token retired); node-inventory recoverability follows the resolved occupant, first-row find retired. No route/schema/service/dependency change; committed tests byte-frozen 2026-08-30 00:18:02 -07:00
v-openrig-build 3ba6cf8d24 test(OPR.0.5.7.1 consumer-alignment RED): final bytes at fdf27ed27 — one new file pinning the occupant truth onto the three reporting consumers: R-A preview explicit relation beats a newer-superseded-first row for action AND token state; R-B table (present-null / missing-key / dangling) renders awaiting-decision + freshRequired=false + relation-naming reason + tokenState missing, with --fresh unable to override; R-C lifecycle recoverability follows the explicitly related row that is not first; R-D present-null + historical token: findLatestUsableSnapshot null and never recoverable; R-E1/E2 live no-snapshot preview selects the exactly-one-running occupant / renders explicit-null ambiguity (capture parity semantics); R-E3 captured-vs-live parity floor; R-F1 legacy single-row floor; R-F2 legacy uniquely-running alignment (titled honestly: RED at base, the divergence under repair). Real exported consumers driven (buildRestorePlanPreview, collectPreviewSessionRows, findLatestUsableSnapshot, deriveNodeLifecycleState); no production edit 2026-08-30 00:18:02 -07:00
v-openrig-build 62e21053f6 test(OPR.0.5.7.1 comment-only per baton c9e631e0): seven stale fixture comments corrected to the actual premise — the three single-node and three default-seed lines now state the captured running occupant deliberately replaced by fresh-listed operation B, and the D3 line states that resumeType 'none' records an occupant without a usable native resume source; comment bytes only 2026-08-30 00:18:02 -07:00
v-openrig-build a32e24433f test(OPR.0.5.7.1 adjacent repair, test-only per baton 96066719): the ten remaining fixture premises state the OCCUPANT as well as the fresh intent — resumeType: 'none' added to each failing seedRigAndSnapshot call (3 bare calls -> object form; 7 object calls gain only the property), so the helper registers exactly one running session per node before capture, the active relation resolves, and the already-present freshLogicalIds exercises operation-B deliberate fresh; every assertion and every d248 fresh-list/comment byte preserved; no production, helper, or focused-test change 2026-08-30 00:18:02 -07:00
v-openrig-build d12733d3cc test(OPR.0.5.7.1 adjacent adaptation, test-only per baton 267146b7): 18 legacy premises adapted to locked A1/D6a semantics with every behavior assertion byte-identical — Cluster A (8): no-session launches state DELIBERATE fresh via the existing freshLogicalIds option (single 'worker'/'agent-a' nodes and the default-seed trio); Cluster B (4): the startup-file validation tests exercise a replay-CONSUMING deliberate-fresh path via the same option (incl. the validation-block test whose resumable session would otherwise correctly skip replay-only validation); Cluster C (6, one hunk): seedSeat states occupancy — updateStatus running before capture. One file; no production, helper, or fixture-framework change; the four-way fail-loud tests remain byte-identical 2026-08-30 00:18:02 -07:00
v-openrig-build c6008f4ffd fix(OPR.0.5.7.1 repair GREEN, static corrections per desk ruling on e42420990): (1) zero session rows returns none ONLY under whole-field absence — a PRESENT map's null / missing-key / dangling state is loud regardless of history; (2) prevalidation validates replay files IFF the node will consume replay: none=>validate (fresh), ambiguity=>skip (stops loudly), explicit fresh or non-resume policy=>validate, resume_if_possible without token=>skip (stop-and-ask), usable type+token=>skip (exact), token without usable type=>validate (current fresh path). Tests, types, capture, and the opt-in absence untouched 2026-08-30 00:18:02 -07:00
v-openrig-build f9fdf47725 fix(OPR.0.5.7.1 repair GREEN D1+D6a): four-way occupant truth — a PRESENT activeSessionIdByNode map is the authority, never collapsed by truthiness: whole-field-absent alone admits legacy inference (single row, else uniquely-running, else loud); present-null, missing-node-key, and dangling-id each fail loudly with the reason and every candidate named (dangling names the offending relation id); ambiguity errors carry a detail clause. D6a: exact-resume zero replay is UNCONDITIONAL and validatePreRestore is path-aware — replay-only required-file blockers skip the exact-resume path (a resumed history consumes no replay inputs; the incident's own discriminator) while deliberate fresh and every other consuming path validates in full (freshLogicalIds threaded into prevalidation). The replay opt-in surface is deleted WHOLESALE: options/pass-through on restore + subset + compensation + restoreNode, the result decoration, and the RestoreNodeResult type field — no opt-in exists before D6b lands in D4 with its durability primitives. Production: restore-orchestrator.ts + types.ts only; snapshot-capture and both committed test files byte-unchanged 2026-08-30 00:18:02 -07:00
v-openrig-build 2539a711da test(OPR.0.5.7.1 repair RED, fixture correction per grant): the D1-roundtrip session literals rt-a-seat / rt-b-seat / rt-b-seat-v2 violate the r{NN}-{suffix} naming contract registerSession enforces, killing the test before its assertion — renamed to conforming distinct names r78-seat / r79-seat / r79-seat-v2; assertions and all production bytes untouched 2026-08-30 00:18:02 -07:00
v-openrig-build e7206a3625 test(OPR.0.5.7.1 repair RED, premise correction per desk ruling on 0ecd79a3): occupancy is a STATED fixture premise — roundtrip(a) marks its single session running before capture (exactly-one-running capture semantics stand; no snapshot-capture widening) and the adjacent seedRigAndSnapshot helper states the same occupancy for its resume fixtures; test-only, authored-only for the adjacent file 2026-08-30 00:18:02 -07:00
v-openrig-build 85065c3be0 test(OPR.0.5.7.1 repair RED): D1 four-way occupant truth + D6a unconditional zero replay, final bytes at 9f2db88d7 — fixture premise made EXPLICIT (every rewrite declares its relation mode: field-absent / explicit-null / missing-node-key / explicit-id / dangling-id; nothing cloned accidentally from capture); new discriminators: present-null, missing-key, and dangling each loud-unrecoverable with zero resume and zero replacement (dangling NAMES the offending relation); capture+JSON roundtrip pins the valid explicit relation AND the explicit null; D6a: exact resume with a MISSING required replay-only file succeeds with zero content while the same missing file still fails a deliberate fresh launch; zero-content and fresh-replay floors preserved; the former opt-in test is REPLACED by an every-encoding absence pin over the child's five surfaces with a non-contiguously-constructed needle; adjacent restore-orchestrator.test.ts:1678 corrected from test-side newest-ULID reduce to identity selection (single running row) — that file is AUTHORED ONLY here, never executed locally per the isolation ruling 2026-08-30 00:18:02 -07:00
v-openrig-build 1e769799b8 fix(OPR.0.5.7.1 GREEN follow-up): launchNodeSubset's own opts type gains the startupReplayOptIn field its call site already threads — the G2 tsc gate caught the omitted subset-path signature (TS2339 at :424); one type field, no behavior change 2026-08-30 00:18:02 -07:00
v-openrig-build d6cb7f87d5 fix(OPR.0.5.7.1 GREEN D1+D6): restore selects the ACTIVE OCCUPANT, never max-row-ID — one shared resolveActiveSnapshotSession ladder (explicit activeSessionIdByNode relation captured live at snapshot > single row / uniquely-running legacy invariant > ambiguity fails LOUDLY naming every candidate, zero resume, zero replacement occupant) consumed at both cite sites; the max-ULID reduce is retired from both; a dangling relation (post-capture mutation) falls through to the ladder whose ambiguity guard still refuses to guess. D6: exact resume delivers ZERO startup/onboarding content by default — the enclosure's projection entries, startup files, and actions empty on the resume path (the D2 empty-plan discriminator proved the launch leg never needed them); replay into a resumed history requires the explicit versioned startupReplayOptIn threaded through restore/subset/compensation opts and RECORDED on the node result as the audit trail; fresh-primed launches keep their deliberate replay. Additive only: SnapshotData.activeSessionIdByNode + RestoreNodeResult.startupReplayOptIn; snapshot-capture derives the relation from the uniquely-running row at capture 2026-08-30 00:18:02 -07:00
v-openrig-build fd544b5572 test(OPR.0.5.7.1 RED correction): R8 fixture modeled the wrong path — nulled resume metadata under resume_if_possible enters the intentional awaiting-decision stop (no launch, no replay by design), so it could not observe fresh-primed replay. Corrected to the existing deliberate-fresh operation-B trigger freshLogicalIds:['seat'] with metadata intact; assertions and test name unchanged; the wrong fixture is retired visibly here, not silently rewritten 2026-08-30 00:18:01 -07:00
v-openrig-build ed3ad7c41b test(OPR.0.5.7.1 RED): D1+D6 restore honesty pinned at pristine b958743e0 — R1 stale-newer-ULID never defeats the active occupant; R2 explicit activeSessionIdByNode consumed with no status inference; R3 two-running ambiguity fails LOUDLY naming both rows, zero resume, zero replacement occupant; R4 uniquely-running interim invariant on legacy snapshots; R5 structural absence of the max-ULID reduce at both cite sites; R6 exact resume delivers ZERO projection/startup content while the launch leg stays intact; R7 replay only under the explicit versioned opt-in, recorded on the result, delivered exactly once; R8 fresh-primed replay preserved (containment is resume-scoped) 2026-08-30 00:18:01 -07:00
v-openrig-build eeecd0fd84 fix(s12): preserve review disposition multiplicity 2026-08-30 00:18:01 -07:00
v-openrig-build 1741726bad test(s12): cover duplicate gate candidates 2026-08-30 00:18:01 -07:00
v-openrig-build 3c1f41da48 fix(s12): genericize derived host examples 2026-08-30 00:18:01 -07:00
v-openrig-build 0ac75f4c3e docs(s12): make remaining public skills self-contained 2026-08-30 00:18:01 -07:00
v-openrig-build c08b556c3e fix(s12): sync reviewed public skill mirrors 2026-08-30 00:18:01 -07:00
v-openrig-build f4708d3dbd docs: remove internal-only SDLC references 2026-08-30 00:18:01 -07:00
v-openrig-build 08eebfb5a6 docs: make shipped references self-contained 2026-08-30 00:18:01 -07:00
v-openrig-build 92d787bebb test(s12): observe named guard execution 2026-08-30 00:18:01 -07:00
v-openrig-build ca6727557c fix(s12): restore named full scan wiring 2026-08-30 00:18:01 -07:00
v-openrig-build af91e923b1 fix(s12): classify packaged derived artifacts 2026-08-30 00:18:01 -07:00
v-openrig-build 7091112b0f test(s12): pin substance gate tree classes 2026-08-30 00:18:01 -07:00
v-openrig-build 7f4e42a5c5 docs(s12): remove remaining internal queue teaching 2026-08-30 00:18:01 -07:00
v-openrig-build 80d5290687 docs(s12): remove private substance from shipped skills 2026-08-30 00:18:01 -07:00
v-openrig-build 44ea56a675 fix(s12): apply reviewed public skill mirror 2026-08-30 00:18:01 -07:00
v-openrig-build 5949918204 docs(fixtures): make VM preview examples public-safe 2026-08-30 00:18:01 -07:00
v-openrig-build 034606a4a3 feat(packaging): enforce substance gate across ship surfaces 2026-08-30 00:18:01 -07:00
v-openrig-build 2ac1f5d075 fix(S20-B4): GREEN — restore_policy absence preserved through normalizeLifecycle (types field optional); the defaults-level call site materializes resume_if_possible; the narrowing resolver's existing guard now sees honest absence, so a profile specifying only compaction_strategy no longer fabricates a broadening 2026-08-30 00:18:01 -07:00
v-openrig-build 2d6bab13ac test(S20-B4): RED — real-ingress mixed-field fixture (R2's YAML verbatim): a profile lifecycle block specifying only compaction_strategy must not fabricate a restore_policy broadening; both outputs proven together (apprentice-handover + checkpoint_only) 2026-08-30 00:18:01 -07:00
v-openrig-build 60266aa3ee fix(S20-B3): GREEN — a non-specifying level does not participate: normalizeLifecycle preserves compaction_strategy ABSENCE (types field optional); only the defaults-level call site materializes the F-6 default, so a partial profile lifecycle block no longer defeats an explicit spec strategy; resolver untouched (its truthy check now sees honest absence) 2026-08-30 00:18:01 -07:00
v-openrig-build 04397e89fc test(S20-B3): RED — real-ingress mixed fixture: a profile lifecycle block omitting compaction_strategy must not defeat the spec-level strategy (yaml -> normalize -> resolve; reproduces R1's flagship advisor failure); F-6 absent-everywhere floor pinned green 2026-08-30 00:18:01 -07:00
v-openrig-build 5d8e6e82e1 fix(S20-A5): GREEN — B-1 null branch returns the {errors,advisories} object; B-2 member compaction_strategy validated+normalized at live YAML ingestion through the one canonical site (validateMember leg + normalizePod mapping; the ruled two pass-through lines thread the existing advisories sink through validatePod); absent stays undefined so the resolver F-6 default remains the one authority 2026-08-30 00:18:01 -07:00
v-openrig-build 3eedbd187a test(S20-A5): RED — member-level compaction_strategy live ingestion: canonical survives, alias advisory + normalize, invalid names the member, custom_prompt teaching text byte-preserved, absent-stays-undefined floor 2026-08-30 00:18:01 -07:00
v-openrig-build b72c2fd63e test(S20-P3): honest test-correction — the precedence fixture's dead ctx key (resolved:) becomes the live baseSpec: at the two call sites; name/assertions/body untouched (desk-authorized 22:28Z) 2026-08-30 00:18:01 -07:00
v-openrig-build e77e2da328 fix(S20-P3): defect A repair — defaults.lifecycle always materializes, making the F-6 default-compaction default visible on the absent-lifecycle path 2026-08-30 00:18:01 -07:00
v-openrig-build 17a5040a0e feat(S20-P3): GREEN candidate — compaction_strategy wired: four canonical modes, A1 alias compat, most-specific-wins resolution
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.
2026-08-30 00:18:01 -07:00
v-openrig-build 90366b6d0c test(S20-P3): RED — compaction_strategy four-mode wiring + A1 compat aliases + most-specific-wins resolution
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.
2026-08-30 00:18:01 -07:00
v-openrig-build 25725fadfa fix(slice26): GREEN — the live cross-host terminal close joins the one propagation site: closeCrossHostHandoffSource calls propagateBlockerCompletion inside its existing transaction at its actual terminal state (handed-off or done) and routes the dependent events through its existing post-transaction notification array; absorbed redrives return before the transaction and can never re-actuate (R2 HOLD B-1, discriminator verbatim) 2026-08-30 00:18:01 -07:00
v-openrig-build 1840842d82 test(slice26): RED 3 — the live cross-host terminal close (closeCrossHostHandoffSource, the routes-invoked third handoff-family path) actuates attached rows through the one propagation site with full effect parity, and an absorbed redrive never double-actuates (exactly one auto-unpark transition, ever); pinned base reason = the parked row stays blocked (R2 HOLD B-1 discriminator, verbatim) 2026-08-30 00:18:01 -07:00
v-openrig-build 253a9a9977 fix(slice26): GREEN — every terminal closure of a blocker actuates auto-unpark through the ONE propagation site: propagate-completion extracted into a shared helper (byte-faithful semantics, isBlockerLive guard stays at the update call site) and invoked inside the handoff and handoff-and-complete transactions with the update path's exact effect set (state, blocker cleared, naming transition, staged wake intent, blocked->pending event pushed to each verb's post-txn notification); S26 RED fixtures relocated to their own describe (mechanical test-only move, own harness) 2026-08-30 00:18:01 -07:00
v-openrig-build dd6231b313 test(slice26): RED — every terminal closure of a blocker actuates auto-unpark: the handoff family (handed-off via handoff; done via handoff-and-complete, confirm-or-refute per R-2) must run the one propagate-completion site with full effect-set parity (state, blocker cleared, naming transition, wake intent, event); pinned base reason = the parked row stays blocked (specimen-1 shape + the derived prediction) 2026-08-30 00:18:01 -07:00
v-openrig-build 4a98f155f0 test: make hermetic tmux cleanup idempotent 2026-08-30 00:18:01 -07:00
v-openrig-build d337ab5b8e test(slice25): give the shared runHook harness an honest runtime occupant identity (session_id: refocus-context-ref-fixture) per desk ruling — the content-contract fixtures were exercising the no-identity sentinel arm instead of on-demand delivery; one line, no per-test setup or assertion changed 2026-08-30 00:18:01 -07:00
v-openrig-build 501c7ba221 test(slice25): adapt refocus-context-ref delivery-state fixtures to occupant-keyed paths per desk ruling — fixture 1 pre-seeds {lastBytes:0} at seat@test__transcript.json (models an existing occupant; every pending/delivery/repeat assertion byte-untouched); fixture 2 carries a fixed session_id and reads seat@test__postcompact-occupant.json; no behavior assertion changed 2026-08-30 00:18:01 -07:00
v-openrig-build cd36709676 fix(slice25): GREEN — refocus state keys to the occupant: identity from firstString(session_id, sessionId, guarded transcript basename); uniform bounded collision-stable seatKey__key.json paths (64-bound + stable 8-hex suffix on lossy/truncated keys, always inside the state dir); first observation baselines at the occupant's OWN current size; shrink clears pending and resets baseline BEFORE due computation with a once-per-reset advisory; no-identity tier records via the '#'-separated active-episode sentinel (structurally collision-free by key character class) with record/silence/clear/re-surface semantics, zero measurement, zero pending, zero fires; zero-size reads are instrument absence (no false baseline, reset, or fire-time overwrite); legacy seat-keyed JSON never read, imported, or rewritten 2026-08-30 00:18:01 -07:00
v-openrig-build 6e6d2887cf test(slice25): RED — refocus state keys to the OCCUPANT: fresh occupant measures from its own first bytes and never inherits lastBytes or pending from the legacy seat-keyed file (bytes preserved, never imported); shrink clears pending and resets baseline BEFORE due computation with a once-per-reset advisory; absence tiers exact (transcript-keyed fallback path; no-identity diagnostic sentinel with active-episode record/silence/clear/re-surface semantics, zero measurement, zero fires); same-occupant Stop->UserPromptSubmit and SessionStart non-emission floor pinned; hostile identities bounded, contained, collision-stable via stable suffix 2026-08-30 00:18:01 -07:00
v-openrig-build 0bf5f14600 docs: product-management pass gains the right-layer check 2026-08-30 00:18:01 -07:00
v-openrig-build 9b19f49ca1 docs: align component menu with settled progress direction 2026-08-30 00:18:01 -07:00
v-openrig-build a12543015f docs: graduate the four SDLC components to stable reference paths (slice 04 Leg A)
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.
2026-08-30 00:18:01 -07:00
v-openrig-build a9fea3143b docs: component menu gains the product/mission-preparation phase
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.
2026-08-30 00:18:01 -07:00
v-openrig-build 09d0760261 docs: sdlc-conventions gains the component menu; slice templates stop restating one flow
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.
2026-08-30 00:18:01 -07:00
v-openrig-build 1eaa32799c fix(slack): reconcile-by-marker recovers the REAL Slack ts — the scan retains each message's ts, and the marker-found branch mirrors the normal-post order (root-open into ThreadSeatMap + rebuild stamp, same-row receipt with the real anchor, then delivered/seen/release), so a reply to the real root routes to the row owner; a matched message without ts degrades loudly to the synthetic receipt, never silently 2026-08-30 00:18:01 -07:00
v-openrig-build fa57f53d92 test(slack): pin reconciled root timestamp routing 2026-08-30 00:18:01 -07:00
v-openrig-build a7593ea61f fix(gateway): require receiptable Slack delivery 2026-08-30 00:18:01 -07:00
v-openrig-build cc95a8ec9c test(gateway): pin receipt boundary repair 2026-08-30 00:18:01 -07:00
v-openrig-build d48183e2b5 fix(gateway): bind notification dedupe to row receipts 2026-08-30 00:18:01 -07:00
v-openrig-build c5e4b3bb1e feat(gateway): route structured owner notifications 2026-08-30 00:18:01 -07:00
v-openrig-build 37cf4beb4e test(daemon): pin owner notification routing 2026-08-30 00:18:01 -07:00
v-openrig-build ddefa45c40 docs: repair public reference antecedent 2026-08-30 00:18:01 -07:00
v-openrig-build 6596fe8e04 docs(slice07 B1 repair): the compose command prints its complete valid shape — required --situation flag included, with situation (which composition menu: fresh/handover/post-compaction) stated as a separate input from the FULL vs HIGH-ALTITUDE depth choice; literal real-pack run exits 0 at this shape 2026-08-30 00:18:01 -07:00
v-openrig-build f245ed7dc2 docs(slice07): fold the first stranger-run findings into the convention — arrangement never skipped at any depth; empty profile slots derived or named-empty, never authored; section-scoped pointers for mixed files; dispatcher names the delta's destination; two-party fact-check degradation stated; underivable volatile facts carried UNVERIFIED with an ask-instruction 2026-08-30 00:18:01 -07:00
v-openrig-build 3fba402991 docs(slice07): ship the mission-install convention at docs/reference/mission-install.md — the project half of agent onboarding: five-layer anatomy (pieces, arrangement, position, contract, derive-your-own-delta), two depth profiles, push/pointer-walk delivery, composition-not-generation, the exclusion rule, contract volatility markers, the fidelity law, the layer-5 verification loop, and the mission-root boundary; generalizable to any project a mission tree can hold 2026-08-30 00:18:01 -07:00
v-openrig-build 840965fae1 docs: define stable seat lore routing 2026-08-30 00:18:01 -07:00
v-openrig-build 24246cee6a fix(slice09 B3): regenerate the public attention-queue skill mirror from live canon — the maturity-model annotation now carries public standalone wording (the ladder concept is normative; tier counts are not), spec and canonical twins byte-identical at their new shared hash, edge digests regenerated from on-disk repo truth 2026-08-30 00:18:01 -07:00
v-openrig-build f6bd7280d7 docs(slice09): recover the epistemic maturity ladder into docs/reference/knowledge-maturity.md — faithful re-authoring from the 2026-08-10 context-distribution design page (ladder, timescale rule, promotion rungs, demotion-loud, warrant rule), multi-dimensional framing per founder rulings 2026-08-28, two-encodings-one-ladder section with the source-verified stage enum, orthogonals fenced; annotate the shipped attention-queue skill's dangling skill-maturity-convention cites toward the recovered home 2026-08-30 00:18:01 -07:00
v-openrig-build f5fd42e833 fix(ps): count active assigned work by state 2026-08-30 00:18:01 -07:00
v-openrig-build 1d2594d5af feat(daemon): park usage-limited seats until reset 2026-08-30 00:18:01 -07:00
v-openrig-build aaa07e81d2 fix(slice03 final presence amendment): GREEN — the seam preserves KEY PRESENCE end to end: route tracks hasSessionSource ('sessionSource' in m || 'session_source' in m), valid shapes normalize typed exactly as before, and ONE raw-carry fallthrough sends every other present value (null, primitive, mode-only, ref:null, unknown mode) to the canonical validator; member emission and the service mapper's session_source emission are presence-governed, never truthiness, so null/false/primitives cannot vanish pre-validation; only a truly absent key stays absent; fork/rebuild valid bytes unchanged 2026-08-30 00:18:01 -07:00
v-openrig-build 2161d436c3 test(slice03 final presence amendment): RED — the invariant is KEY PRESENCE: mode-only agent_image, ref:null, non-object ref, primitive session_source, null session_source, and unknown mode must each reach the canonical validator (structured 400 naming session_source, zero persistence, never preflight); only a truly absent key stays absent. At eb8a37b2f these erase at the outer ref/object guards 2026-08-30 00:18:01 -07:00
v-openrig-build fdc65cf6f2 fix(slice03 repair amendment): present-invalid agent_image shapes preserve RAW presence through route and service to the ONE canonical validator (permission_policy precedent) — route constructs typed only for the valid v0 shape (schema-parity String() version coercion unchanged); service mapper carries session_source FAITHFULLY instead of re-mapping field-by-field (the original wave-1 drop site; no field list to forget, no crash on raw); fork/rebuild output bytes identical 2026-08-30 00:18:01 -07:00
v-openrig-build 85c0d08f37 test(slice03 repair amendment): RED — present-invalid agent_image shapes (empty value, wrong kind, object/boolean version) must return the structured 400 from the ONE canonical validator with zero persistence, never be erased into session_source-absence; valid numeric coercion pin retained as its own test. At b8dc6f7fc the invalid shapes erase silently and the expansion proceeds 2026-08-30 00:18:01 -07:00
v-openrig-build 8cdf079fe3 fix(slice03 repair): GREEN — expand route normalizer constructs the agent_image session source (image_name kind, non-empty string value) with schema-parity version coercion (string|number via String(), matching rigspec-schema normalize; orch-lead ruling 2026-08-28 12:06Z), so the source and its pin reach the service mapper through the REAL ingress; invalid shapes (empty value, wrong kind) remain unconstructed 2026-08-30 00:18:01 -07:00
v-openrig-build 4f13c3c13c test(slice03 repair): numeric-version assertion flipped to schema-parity coercion per orch-lead ruling 2026-08-28 12:06Z — YAML version: 3 must reach the canonical shape as version: '3'; string-only omission recreates the silent-default defect. These are the FINAL test bytes; all three agent_image pins remain RED at the held tip (source absent at the boundary) 2026-08-30 00:18:00 -07:00
v-openrig-build 14d94f325a test(slice03 repair): RED — agent_image session_source (pinned + unpinned) must survive the REAL expand ingress to the materializeStructured boundary; invalid shapes not widened (empty value / wrong kind drop; non-string version omits the pin). At the held wave-1 tip the route normalizer constructs only fork/rebuild, so the source is absent at the boundary 2026-08-30 00:18:00 -07:00
v-openrig-build bed663c6f3 test(slice10): stamp the generator's synthetic static-pack fixture (mini-world) — REF COLLISION + STATIC PACK PROJECTED legs parse through the taxonomy-requiring parser 2026-08-30 00:18:00 -07:00
v-openrig-build eef615222f feat(slice10): pack-level taxonomy first-class on every context pack — parser requires taxonomy from the ONE shared enum with teaching refusals (field + legal values + one sentence each, shared TAXONOMY_TEACHING); library entry projects it; compose stamps fixed 'mission' (desk-ruled; caller flag deferred to slice 08); new daemon export surface ./context-pack-taxonomy; CLI add teaches the same refusal at install time (ruled T2) and list surfaces the column (human + --json); generator stamps 'skills' on every skill projection; static packs (onboarding-width, world-example) + capability-delta template stamped world; no-grandfather sweep across daemon/cli test fixtures + 13 eval fixture packs 2026-08-30 00:18:00 -07:00
v-openrig-build df718b97d8 test(slice10 RED): pack-level taxonomy pinned — teaching refusal on missing/non-enum taxonomy (field + all legal values + one sentence each), per-enum + lore admission (slice-08 seam), atoms may differ from pack class, entry projection + fail-visible scan refusal, composeFromFiles stamps mission (desk-ruled), generator stamps skills; 9 legs fail at base 843e3557 2026-08-30 00:18:00 -07:00
v-openrig-build c9ac1ab0ad fix(slice03): GREEN — expand fragment->spec map carries session_source.ref.version (union audited: sole dropped optional field); retry guidance matches the live pod-aware /launch route (managed subset launch with full orchestration) and advertises per-node rig launch, naming no removed mechanisms 2026-08-30 00:18:00 -07:00
v-openrig-build 79933b2795 test(slice03): RED — pin session_source.ref.version survival through the expand fragment->spec map, the no-widening exact shape for unpinned sources, per-node relaunch guidance matching the live pod-aware /launch route, and the absence of the removed pod_aware_launch_unsupported claim in every output encoding 2026-08-30 00:18:00 -07:00
v-openrig-build 5f5bc741f2 fix(scope): restore audit finding parity 2026-08-30 00:18:00 -07:00
v-openrig-build 9c582fb837 test(watchdog): select pending history receipt 2026-08-30 00:18:00 -07:00
v-openrig-build 90816c119c fix(watchdog): wait for successor transcript sample 2026-08-30 00:18:00 -07:00
v-openrig-build 0306a85475 test(watchdog): pin successor sample wait 2026-08-30 00:18:00 -07:00
v-openrig-build 99ff83d990 fix(scope): centralize refocus notes resolution 2026-08-30 00:18:00 -07:00
v-openrig-build 1f282e341d fix: bind context watchdog to occupant transcript 2026-08-30 00:18:00 -07:00
v-openrig-build 1e78723983 test: pin context watchdog handover binding 2026-08-30 00:18:00 -07:00
v-openrig-build 36884f5b85 fix: unify mission notes resolution 2026-08-30 00:18:00 -07:00
v-openrig-build a595f8c745 test: pin mission notes read precedence 2026-08-30 00:18:00 -07:00
v-openrig-build f06c30de76 fix: refuse empty sends and return API JSON 404s 2026-08-30 00:18:00 -07:00
v-openrig-build db8a22558e feat: add context usage watchdog policy 2026-08-30 00:18:00 -07:00
v-openrig-build fa65e8cdd7 test: pin one-rung watchdog passes 2026-08-30 00:18:00 -07:00
v-openrig-build 7e8db7a15d test: pin context usage watchdog behavior 2026-08-30 00:18:00 -07:00
v-openrig-build 3af67b8cc6 fix(queue): make stuck sweep findings trustworthy 2026-08-30 00:18:00 -07:00
mvschwarz 42700c4be7 docs: v0.5.5 release notes (CHANGELOG entry + docs/releases file)
The 'ambient-attention release': rig parked derives silent-stuck diagnosis;
self-retrying wake ladder + escalations view; standing stuck-sweep runs
without you; queue block --on park-with-wake; rig ps typed ACTIVITY column;
seat handover fork:/rebuild execute; Slack human layer live; fresh installs
onboard themselves; refocus fires only when it should; one-file scope
convention; daemon bind provenance.

Two new migrations: 072_thread_seat_map + 073_queue_transition_wakes
(head advances 071 to 073).
2026-08-27 12:30:32 -07:00
mvschwarz 236c7fff21 chore: bump version to 0.5.5
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.
2026-08-27 12:28:54 -07:00
v-openrig-build bde1f969b4 fix: make public onboarding context retrievable 2026-08-27 12:28:38 -07:00
v-openrig-build ce5c32e821 fix: rehome private world context 2026-08-27 12:28:38 -07:00
v-openrig-build e09ae460f1 fix(S01 fix-round): GREEN — identity resolution rides the durable session binding: session_name -> node (registry precedent), handover_at from the bound node, delegates_to parent returned as its CURRENT canonical session binding (never a synthesized logical_id@rig, never a string transform); self-skip operator floor now produces the open per-destination escalation row (operator seat else obligation creator) visible on the escalations view + health count; live acceptance 22/22 running sessions incl. 15/15 v-openrig-build managed seats resolve, zero skipped [pre-wave-review] 2026-08-27 12:28:38 -07:00
v-openrig-build 58eefecf0f test(S01 fix-round): RED — production identity resolution pinned at the durable session binding (dotted logical_id x dash-form canonical session, defined independently; live fleet has zero equal pairs) for F2 suspension + F4 orchestrator resolution in BOTH the ladder and the folded S02 resolver; self-skip operator floor pinned end-to-end through ViewProjector.show('escalations') + health status; 5 legs fail at candidate 38e5db05b [pre-wave-review] 2026-08-27 12:28:38 -07:00
v-openrig-build ae01648d89 feat(S01): GREEN — wake-or-escalate on batons: ladder state derived entirely from transition markers (restart-safe by construction); bounded config-keyed retries with outcomes on every marker; unconfirmed class never re-nudged yet escalates on the confirmation path; suspension derived from post-swap grace with recorded suspend/resume and operator-override env; per-destination aggregated escalation row refreshed on detection; rungs deliver-and-advance with honest operator floor (S11 cited); escalations lens carries open aggregates; healthz heartbeat; 4 config keys in full lockstep [pre-wave-review] 2026-08-27 12:28:38 -07:00
v-openrig-build f9694988d3 test(S01): RED — wake-or-escalate on batons: all seven locked proof groups (retry recorded, rungs in order + view, F1 unconfirmed both properties + negative controls, F2 derived suspension + burst specimen + operator override, F3 storm aggregation + destination bound, F4 deliver-and-advance + self-skip + honest operator floor, F5 seam joint legs, F6 restart derivation); 21/21 fail by name at exact base, CLI parity leg RED [pre-wave-review] 2026-08-27 12:28:38 -07:00
v-openrig-build 2dfea0caaa feat(refocus): ship core re-grounding channel 2026-08-27 12:28:38 -07:00
v-openrig-build abefbf21b5 test(refocus): pin S18 feature contract 2026-08-27 12:28:38 -07:00
v-openrig-build 4ab5b80b69 fix(queue): wake auto-unparked owners honestly 2026-08-27 12:28:38 -07:00
v-openrig-build fb1dfcb56b test(queue): expose auto-unpark wake and teaching gaps 2026-08-27 12:28:38 -07:00
v-openrig-build 13d669e828 fix(queue): route stalled pickups and validate settings 2026-08-27 12:28:38 -07:00
v-openrig-build 2ff2673fdf test(queue): expose sweep and integer validation gaps 2026-08-27 12:28:38 -07:00
v-openrig-build aa0d685c76 fix(package): genericize R23 provenance 2026-08-27 12:28:38 -07:00
v-openrig-build 984fc9ceec feat(S02): GREEN — standing stuck sweep: both verb halves as libraries + unclaimed-obligation net + dangling-closure custody class swept on config-keyed cadence; findings route as idempotent rows to owning seats; clean sweeps heartbeat on healthz; failures loud; S01 ladder seam vocabulary exported [pre-wave-review] 2026-08-27 12:28:38 -07:00
v-openrig-build 8ad87c215c test(S02): RED — standing stuck sweep: both halves + custody class + A1 unclaimed net + S01 ladder seam + idempotent refresh/close + quiet heartbeat + loud failure; 14/14 fail by name at exact base, CLI parity leg RED [pre-wave-review] 2026-08-27 12:28:38 -07:00
v-openrig-build 06e9720f3e feat(S05 fix B2+B3): GREEN — sourceOutcome persists on the durable seat.handover_completed event (exact primed/gaps/empty-chain + fork provenance in the same shape); production rebuild chain includes the latest restore packet via the compaction seam's seat-keyed restore-pending marker (valid marker names the recorded packet dir; invalid marker honestly labeled; absent marker leaves shipped precedence untouched) [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 1afcbadaef test(S05 fix B2+B3): RED — durable-event discriminators (persisted seat.handover_completed must carry exact primed/gaps/empty-chain and fork provenance) + production chain builder extracted to ONE implementation with restore-packet-leg pins (valid marker named in precedence, gone packet dir still declared, invalid marker honestly labeled); regression pins on shipped precedence green [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 9173556aa0 fix(scope): match capability delta identity exactly 2026-08-27 12:28:37 -07:00
v-openrig-build 650d4261ef test(S05): e2e — fork carries the resolved native id to the real launch in the preserved pane; rebuild resolves a real on-disk chain, delivers the priming packet through real tmux, and records the executed set + gap [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 1504fe8e06 feat(S05): GREEN — fork and rebuild handover sources execute the plan they print: fork carries the incumbent conversation through the native-fork launch seam (pre-mutation honest refusal when no native id resolves); rebuild primes a fresh successor from the seat's durable chain with primed/gap/empty-chain recording; ONE shared source-capability table drives plan and executor; continuity_outcome records forked/rebuilt; production wiring + CLI truth [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 4475dc6899 test(S05): RED — fork/rebuild handover sources pinned at the behavior layer: execution, honest no-token refusal, priming record with gaps, empty-chain honesty, plan/executor equality, induced mid-swap failure honesty; all 8 fail on the v0 source_not_supported refusal [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 55b3e71c91 feat: make held wake state observable 2026-08-27 12:28:37 -07:00
v-openrig-build dede384dca test: pin park wake contracts 2026-08-27 12:28:37 -07:00
v-openrig-build b415b57de9 feat(scope): scaffold release capability deltas 2026-08-27 12:28:37 -07:00
v-openrig-build 977b75b3b4 feat(S04): GREEN — pickup receipts derive unclaimed/working/stalled-after-claim/parked from existing facts; pickup lens; config-keyed threshold; doctrine joins retired to the projection [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build ffbdb58649 test(S04): RED — pickup receipts derive unclaimed/working/stalled/parked from existing facts; 10/11 fail by name at exact base (schema-proof leg green) [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 2b7d2e903e fix(S20 r2-repair-2): GREEN — probe error provenance through the listener gate [pre-wave-review]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build 846bd02075 test(S20 r2-repair-2): RED — probe error provenance: tri-state gate probe (indeterminate never promotes to missing-listener), start-level one-shot-timeout no-kill control beside the refused-kills control (reviewer's reproduction shape) [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 63e34789bd fix(S20 r2-repair): GREEN — start + up auto-start route through the shared intent seam [pre-wave-review]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build 536e620525 test(S20 r2-repair): RED — up + start auto-start pins: env-sourced OPENRIG_HOST creates NO bind intent and never crosses (the stale positive pin REQUIRED the incident; replaced with the ruling cited); start gains the preflightExec seam mirroring up (harness enablement only) [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build e04a8eedbd fix(S20): GREEN — bind intent has unambiguous provenance; routing env never becomes bind policy [pre-wave-review]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build 25fda9d2de test(S20): RED — bind provenance pins: incident shape multi-bind survives injected routing env (visible ignore), dedicated surfaces only create intent (env-sourced config excluded — route A), routing vars scrubbed from daemon env with coherent re-export on declared intent, restored listener gate fails loudly on the 0.5.3 dropped-listener shape with binding evidence over config echo [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 512203b070 test(onboarding): pin both R23 teaching surfaces 2026-08-27 12:28:37 -07:00
v-openrig-build 135d7acc15 fix(onboarding): retire operator routing gate 2026-08-27 12:28:37 -07:00
v-openrig-build 483b5cf599 fix(F1): GREEN — blocker refusals name rejectedBlocker; no error payload carries the success-shaped blockedOn field [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 650c83c169 test(F1): RED — blocker refusal payloads must name rejectedBlocker and never carry success-shaped blockedOn; 3/4 fail by name at pre-fix bytes [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build d0e75f89ec fix(external-nudge): GREEN — @external wakes are gateway-owned, never tmux [pre-wave-review]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build a1db399cf4 test(external-nudge): RED — @external wakes are gateway-owned, never tmux; undelivered surface healed; agent + human-@kernel controls [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 5a06e02045 fix(slack): keep routine deliveries visually quiet 2026-08-27 12:28:37 -07:00
v-openrig-build 93e6661295 fix(scope): span blank lines in owned frontmatter values 2026-08-27 12:28:37 -07:00
v-openrig-build f7ec4ba125 test(scope): reproduce multiline repair corruption 2026-08-27 12:28:37 -07:00
v-openrig-build ddcc68631f fix(S10 L2 root): GREEN — local stamping deleted at all three roots (create, pane envelope, CLI send twin); origin triple constructed only at the cross-host forwarding boundary; interim Slack localizer removed; superseded pins flipped [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build d20436e457 test(S10 L2 root): RED — local bare-identity pins (create row, handoff wake envelope, wrapper render); 3/5 fail by name, boundary-preserve legs green [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build 22d1a6d124 fix(S10 L2): GREEN — mapped seats resolve at the route-address seam: self-host triples canonicalize to the bare local session, foreign hosts route honestly unrouted, null self-id never guesses [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build de80c3b776 test(S10 L2): RED — thread-return route-address discriminators (self-host triple, remote triple, null self-id); 3/4 fail by name at pre-fix bytes [pre-wave-review] 2026-08-27 12:28:37 -07:00
v-openrig-build a7828c4e1e fix(scope): make repair frontmatter additive 2026-08-27 12:28:37 -07:00
v-openrig-build 677e4e9bc4 test(scope): pin additive repair and stamp generations 2026-08-27 12:28:37 -07:00
v-openrig-build dc7f7e55d4 fix(S19 waveO-fix1): trailing coalescer keeps rejection propagation; old overlap pin updated to the R2-ruled contract (the join-the-flight semantics WAS the B3 defect; cited in-pin) [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 346a7cf83c fix(S19 waveO-fix1): GREEN — three production seams closed [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build 5e52835073 test(S19 waveO-fix1): RED — R2's three production-seam discriminators preserved [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build 54926c9651 fix(S10 shape-fix): GREEN — read methods (conversations.info/history/replies) use GET+query, the measured supported shape; JSON-POST family untouched; fixtures follow the shape [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 2a162ac2cd test(S10 shape-fix): RED — read methods pinned to GET+query (the measured live shape); 3/5 fail by name at pre-fix bytes, both JSON-POST-family legs green [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 8985213a4b fix(S10 fix-r3): GREEN — reconciliation identity is a bounded decision-scoped token reserved outside the clamp budget; exact-token match replaces the bare substring scan; exactly-once holds on ordinary data (R2) [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build cb071f2399 test(S10 fix-r3): RED — clamp-survival and prose-collision reconciliation discriminators; 3/3 fail at pre-fix bytes (R2 exactly-once) [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build b2561524ab fix(S10 fix-r2): GREEN — footer qitemId joins the uniform escape (exemption and premise removed); reconcile marker compares the same escaped bytes on both sides (R1 F-B1r) [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build ff5068ff82 test(S10 fix-r2): RED — hostile caller-supplied qitemId discriminator + escaped-marker effect pin; 2/2 fail at pre-fix bytes (R1 F-B1r) [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 7a45b65be1 fix(S10 fix-r1): GREEN — queue-controlled fields structurally neutralized (Slack three-char escape) before placement; the one escalation mention composes after; R2 B1 closed by construction [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 496bf672bb test(S10 fix-r1): RED — mention-injection discriminator through the real delivery seam; 3/3 fail at pre-fix bytes (R2 B1) [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 8cd10997cd test(S10): add 072_thread_seat_map to the curated full-test-db migration list (P24 parity) [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 9c6d29f5a2 feat(S10 atom H): GREEN — timeout retries reconcile by qitem marker before send; persist/dispatch crash replays exactly once; duplicate absence pinned [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 85321cc90a feat(S10 atom G): GREEN — local screenshots ride the external-upload flow into the thread; files.upload stays dead; upload failure fail-visible without duplicate posts [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build f190054f6d feat(S10 atom F): GREEN — A1.2 attribution header in one honest bot identity; customize absence pinned at posted bytes; escalation-only mentions [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build de8358ae4f feat(S10 atom E): GREEN — deterministic thread↔seat routing; four classes receipted; wrong-seat absence pinned; map rebuilds from queue stamps [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 20ec357fca feat(S10 atom D): GREEN — relay cuts over to the in-daemon subsystem path; slice-11 receipts ported; dual-path absence pinned [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 4010074115 feat(S10 atom C): GREEN — gateway activates as an in-daemon subsystem at boot; honest failure; durable wire receipts [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build a467836539 test(S10): RED — activation contract pins the landed-not-activated class; 3/3 fail by name at tip [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build 5e5e585d8e test(onboarding): seed required assets in real adapters 2026-08-27 12:28:37 -07:00
v-openrig-build ae7cef2d69 fix(S19 AM-R18): SSE leg honors S16's cadence + one-HTTP-module pins [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build d0278c9afb feat(S19 AM-R18+ruling-01530): GREEN — the open view updates itself from the one oracle [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build 92d73ce4c8 test(S19 AM-R18+ruling-01530): RED — push substrate, SSE stream, swap wiring, self-updating view [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build 7db03e4f6b test(S19 A9): rejects pinned — closed rung set, no transcript evidence producer (scope-named grep), pane reading last, dated doc receipts with the permitted S18 consumer [NOT guard-accepted] 2026-08-27 12:28:37 -07:00
v-openrig-build aa6efddd28 feat(S19 A8): consumers render from the served taxonomy state [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build f129e151d4 feat(S19 A5): per-harness rung inventories + the Claude self-report rung, production-wired [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build e0bdae7417 feat(S19 A7): GREEN — the parked query: rig + seat, both-sided confidence [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build b42b8ca2ec test(S19 A7): RED — pin the parked query, both-sided per AM-3 [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build 4ccdeffb27 feat(S19 A4): GREEN — hook ingest feeds the one oracle through the adapter seam [NOT guard-accepted]
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).
2026-08-27 12:28:37 -07:00
v-openrig-build 7617e5c5be test(S19 A4): RED — pin the ingest seam: hooks feed the ONE oracle [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build b2942da75a feat(S19 A3): GREEN — the ranked evidence ladder arbitrates on the one oracle [NOT guard-accepted]
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.
2026-08-27 12:28:37 -07:00
v-openrig-build 2946752b8e test(S19 A3): RED — pin the ranked evidence ladder on the one oracle [NOT guard-accepted]
Contract types land (declarative): EvidenceRungId + rank, RungTrust with
AM-2's four levels, AdapterRungInventory (re-declared at swap),
ActivityEvidence (per-source monotonic seq, self-dated), RungHealthEvent
(AM-1 visibility), ArbitratedSeatState (decidedBy + rungs + lastSwap).
Service surfaces stubbed UNWIRED: declareRungInventory / reportEvidence /
declareOccupantSwap / getSeatState / waitForSeatState / onRungHealth.
Arbitration constants declared with chosen numbers (hook authority 15s,
contradiction window 10s, sampling-idle debounce 2 ticks cap 2.5s vs our
1Hz/3s cadence, promotion 50 agreements over >=1h).

Pins (behavior layer, injected clock, non-inference untouched): ladder
ranks (self-report > hooks > sampling) and falls silently to unknown;
stale per-source seq never revives idle; chrome outranks self-report for
needs-input ONLY; time-bounded hook authority expires through; AM-1
alive-and-partial (orch-lead specimen shape) degrades to identity-only
with a visible rung-health event; sampling debounce with authoritative
turn-boundary bypass; monotonic seq + wait-after-seq surviving a fast
transient; AM-2 trial-not-authority with measured promotion over the
stated window then rank takeover; seat-keyed swap (own event, unknown not
flicker, zero bleed, trust reset, re-declaration starts unpromoted); seat
independence. Watched RED: 11/11 failed at the arbitration layer.
2026-08-27 12:28:37 -07:00
v-openrig-build e9e1a089a0 feat(S19 A1): GREEN — the three-axis taxonomy module + reference doc [NOT guard-accepted]
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.
2026-08-27 12:28:36 -07:00
v-openrig-build b4bae03465 test(S19 A1): RED — pin the three-axis taxonomy + binding exclusions [NOT guard-accepted]
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.
2026-08-27 12:28:36 -07:00
v-openrig-build 3958ddc916 test(onboarding): pin existing rig immutability 2026-08-27 12:28:36 -07:00
v-openrig-build 4ea4f6b189 test(onboarding): pin config route default 2026-08-27 12:28:36 -07:00
v-openrig-build 506a795783 feat(onboarding): prime fresh seats by default 2026-08-27 12:28:36 -07:00
v-openrig-build bd841b29d6 test(s15): pin default onboarding pack contract 2026-08-27 12:28:36 -07:00
v-openrig-build 3343472224 fix(scope): address S17 review blockers 2026-08-27 12:28:36 -07:00
v-openrig-build 93d1b9316c test(scope): pin S17 review fixes 2026-08-27 12:28:36 -07:00
v-openrig-build 0ee08b8845 fix(S16): refresh fleet only on operator activity 2026-08-27 12:28:36 -07:00
v-openrig-build 42ebc54afb test(S16): RED pin idle-open load and active refresh 2026-08-27 12:28:36 -07:00
v-openrig-build cf379bdf27 fix(skills): publish the operating-model projection 2026-08-27 12:28:36 -07:00
v-openrig-build 2c4267635f chore(skills): refresh scope teaching mirrors 2026-08-27 12:28:36 -07:00
v-openrig-build 9110c8d4b9 chore(skills): register the operating-model edge 2026-08-27 12:28:36 -07:00
v-openrig-build 622323dbee feat(scope): ship the one-spec work convention 2026-08-27 12:28:36 -07:00
v-openrig-build 787ce63747 test(scope): preserve legacy bytes during repair 2026-08-27 12:28:36 -07:00
v-openrig-build 88e2e1e5e2 test(scope): reject the retired PRD gate 2026-08-27 12:28:36 -07:00
v-openrig-build ac066bca7b test(scope): define the one-spec convention 2026-08-27 12:28:36 -07:00
v-openrig-build fb2dea6002 fix(S12 fix-r1): GREEN — F1 add is the single-human boundary; F2 removal enumerates to exhaustion [NOT guard-accepted]
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.
2026-08-27 12:28:36 -07:00
v-openrig-build bf6f250d15 test(S12 fix-r1): RED — pin F1 add boundary + F2 exhaustive removal enumeration [NOT guard-accepted]
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.
2026-08-27 12:28:36 -07:00
v-openrig-build a00cff1205 feat(S12): GREEN — gateway human lifecycle: list/show/set/remove, single-human per A1 [NOT guard-accepted]
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.
2026-08-27 12:28:36 -07:00
v-openrig-build 013047cf79 test(S12): RED — pin gateway human lifecycle (list/show/set/remove) [NOT guard-accepted]
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").
2026-08-27 12:28:36 -07:00
mvschwarz 0a1718c76d docs: v0.5.4 release notes
The honesty release: classified transport resolution, delivery verification by effect, supported single-seat lifecycle controls, and state-preserving truth surfaces.
2026-08-26 11:28:55 -07:00
mvschwarz 4504989f7f chore: bump version to 0.5.4
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.
2026-08-26 11:28:55 -07:00
v-openrig-build c1a05211ce fix(S5b): preserve running-name refusals across import routes 2026-08-26 03:10:45 -07:00
v-openrig-build a0ce8763dd test(S5b): cover all import running-name refusal surfaces 2026-08-26 03:10:45 -07:00
v-openrig-build 279e98e5d3 fix(S5b final-fix F1): GREEN — rig_name_running survives /api/up as a 409 with top-level code + teaching error; CLI renders the locked refusal verbatim (r2 artifact 7959607c)
/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.
2026-08-26 03:10:45 -07:00
v-openrig-build a2270f3af5 test(S5b final-fix F1): RED — the guard refusal dies at the real /api/up (500, code buried) and the CLI renders fallback noise (r2 row, artifact 7959607c)
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.
2026-08-26 03:10:45 -07:00
v-openrig-build 3eb33cf7c2 fix(queue): refuse envless owned listing 2026-08-26 03:10:45 -07:00
v-openrig-build 92e61c8d61 chore(skills): sync S9a mirror digests 2026-08-26 03:10:45 -07:00
v-openrig-build ab486a32d1 fix(cli): make remaining truth surfaces honest 2026-08-26 03:10:45 -07:00
v-openrig-build a859391875 feat(running-name-guard S5b): GREEN — one guard on every instantiator create path; a RUNNING same-name rig refuses with a teaching error, spends nothing (OPR.0.5.4.11)
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.
2026-08-26 03:10:45 -07:00
v-openrig-build cb5c5addf9 test(running-name-guard S5b): RED — helper ships unwired; pod create paths accept a duplicate running-name rig (OPR.0.5.4.11)
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.
2026-08-26 03:10:45 -07:00
v-openrig-build 7640560c97 fix(plugin): align openrig-core skill contract 2026-08-26 03:10:45 -07:00
v-openrig-build e270457b0e fix(cli): wave-1 S3 fix round 3 — fan-out JSON aggregate derives from classified outcomes (OPR.0.5.4.6, r2 row 00fb3a68)
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.
2026-08-26 03:10:45 -07:00
v-openrig-build cad7fdf30d fix(cli): wave-1 S3 fix round 2 — one verdict per recipient, identity before placeholder attribution (OPR.0.5.4.6, r2 row b5ad5131 F1-F2)
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.
2026-08-26 03:10:45 -07:00
v-openrig-build d4c512cd58 fix(cli): wave-1 S3 fix round 1 — effect classification before encoding, input-region-only staged evidence (OPR.0.5.4.6, r2 row 91b29490 F2-F4)
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.
2026-08-26 03:10:45 -07:00
v-openrig-build 6f5be3d279 fix(cli): make queue create preview advisory truthful 2026-08-26 03:10:45 -07:00
v-openrig-build 22aff32b6b fix(plugin-discovery): retire stale lab refocus hooks 2026-08-26 03:10:45 -07:00
v-openrig-build 7cfb7f590d fix(daemon): advance openrig-core vendor authority 2026-08-26 03:10:45 -07:00
v-openrig-build 2f1d932061 test(seat-lifecycle S5 fix r2-F4): real-codex e2e — set-model then a REAL managed successor runs the canonical model, lineage preserved (row 30045f39)
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).
2026-08-26 03:10:45 -07:00
v-openrig-build ba9e9c3a4c fix(seat-lifecycle S5 fix r2-F3): GREEN — clean's safety covers EVERY session it mutates + the binding's session; refuse on any live or indeterminate (row 30045f39)
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.
2026-08-26 03:10:45 -07:00
v-openrig-build 9143b76773 test(seat-lifecycle S5 fix r2-F3): RED — older-live/newer-dead pin: clean probes only newest yet mutates all (r2 row 30045f39) 2026-08-26 03:10:45 -07:00
v-openrig-build f056dbc88e fix(seat-lifecycle S5 fix r1): GREEN — stop/clean consume classified probeSession; transport blip is an INDETERMINATE refusal, never absence (row 9baac99f) 2026-08-26 03:10:45 -07:00
v-openrig-build 2c89d169a1 test(seat-lifecycle S5 fix r1): RED — real-adapter blip pins: clean/stop must refuse INDETERMINATE, not fabricate absence (inverts r1 evidence, row 9baac99f) 2026-08-26 03:10:45 -07:00
v-openrig-build b7c2a2ac6f fix(queue): make notes state-preserving 2026-08-26 03:10:45 -07:00
v-openrig-build ae19100f53 feat(seat-lifecycle S5): GREEN — set-model / single-seat stop / dead-session clean, one resolution path, audited (OPR.0.5.4.7)
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.
2026-08-26 03:10:45 -07:00
v-openrig-build 5ea7b347ad test(seat-lifecycle S5): RED — pins for set-model/stop/clean incl. the already_bound defect (OPR.0.5.4.7) 2026-08-26 03:10:45 -07:00
v-openrig-build 48b1ad8add fix(refocus): preserve due state until visible delivery 2026-08-26 03:10:45 -07:00
v-openrig-build 334851c310 feat(cli): send --verify discriminates staged from consumed by pane effect (OPR.0.5.4.6)
'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/).
2026-08-26 03:10:45 -07:00
v-openrig-build adeb6d7616 test(s3): RED-first staged-vs-consumed proof assets at proof-item altitude (OPR.0.5.4.6 pre-build atom — no product edits)
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/.
2026-08-26 03:10:45 -07:00
v-openrig-build 43e5dbf021 fix: make startup boundaries hermetic 2026-08-26 03:10:45 -07:00
v-openrig-build 62c155b8db feat(transport): unknown-sender honesty — the three attribution refusals become two-ended honest delivery (OPR.0.5.4.3)
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.
2026-08-26 03:10:45 -07:00
v-openrig-build ec90a52305 fix(cli): make recovery output actionable 2026-08-26 03:10:45 -07:00
v-openrig-build 659a445be4 fix(transport): S1 guard-round amendment — teaching prose, no-down-rank assertions, captured evidence (OPR.0.5.4.2)
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.
2026-08-26 03:10:45 -07:00
v-openrig-build f7b54a4945 fix(transport): one honest resolution path — classified session probe ends the blip-reads-as-dead-seat conflation (OPR.0.5.4.2)
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/).
2026-08-26 03:10:45 -07:00
mvschwarz e6add8b86b chore(test): rename rig-policy packaging guard to rig-mode
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.
2026-08-25 19:53:25 -07:00
mvschwarz 2c6e9e509d docs: v0.5.3 release notes (CHANGELOG entry + docs/releases file)
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.
2026-08-25 18:46:15 -07:00
mvschwarz 7a72f4081f chore: bump version to 0.5.3
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.
2026-08-25 18:44:16 -07:00
v-openrig-build 0e7eb68bb6 fix(router): prefer pull for natural cross-host discovery 2026-08-25 18:42:22 -07:00
v-openrig-build 00a3a65fa4 fix(walk turn-pacing round 2): N-of-N closure gate — the final piece waits too (r2 row b268b89b)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 08bc46a29e test(walk turn-pacing round 2): RED — the closure gate must be N-of-N; the final piece cannot bypass it (r2 NOT-CLEAR row b268b89b)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build e2f78c5148 feat(walk turn-pacing): piece N+1 waits for piece N's turn CLOSURE — the capture atom's boundary becomes walk's inter-piece gate (desk BLOCKING row 2ff16fa1)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build a9257072f5 test(walk turn-pacing): RED pins — piece N+1 waits for piece N's turn CLOSURE (desk BLOCKING row 2ff16fa1)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 0c4e479f81 fix(walk round 6): exact-equality boundary join — the ±1 tolerance was unsupported by the renderer evidence (r2 R5)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 041c9712ee test(walk round 6): RED — exact-equality boundary pins, +139 accepts, +138/+140 refuse (r2 R5, row 98a9a82c)
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).
2026-08-25 18:42:22 -07:00
v-openrig-build fd7e8f66b1 fix(walk round 5): join the suffix anchor to the placeholder sum via the hidden source boundary (r2 R4 HIGH-1)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build eaa265aff2 test(walk round 5): RED — the suffix anchor must join the placeholder sum to the hidden source boundary (r2 R4 HIGH-1, row e039e8d1)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build a8da19118f fix(walk round 4): suffix-anchored identity only — no identity-free acceptance, fail closed (r2 R3 HIGH-1) + fixture EOF whitespace
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).
2026-08-25 18:42:22 -07:00
v-openrig-build 332c4adc1a test(walk round 4): RED probes — identity-free acceptance must refuse (r2 R3 HIGH-1, row 7435d61b)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 5b3ca3eaca fix(walk round 3): rendering-true staged identity — contiguous-residual containment + segment-sum sanity, specimen-pinned (r2 R2 HIGH-1)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build b28c22b5c8 test(walk round 3): specimen-pinned RED — the real staged rendering (multi-placeholder single piece) must ACCEPT; the coalesced region must refuse (r2 R2 HIGH-1, row d95b2ea7)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build ef983f19ca fix(walk round 2): current-input piece-identity binding for submitOnly + terminal bearer auth on the generation-record surface (r2 R1 HIGH-1/HIGH-2)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build b6fc2ecd14 test(walk): RED-first round-2 pins — current-input piece-identity binding for submitOnly + terminal bearer auth on the generation-record surface (r2 R1 HIGH-1/HIGH-2, row 66e74676)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 0ea90ea7b8 feat(walk): per-piece consumption verification by effect — generation-record polling, guarded single-Enter retry, reconcile-by-effect on client timeout (desk BLOCKING ruling d9b3989a)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build dcc4a013aa test(walk): RED-first pins W1-W4 — per-piece consumption verification by effect (desk BLOCKING ruling d9b3989a)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 2bb01e7b86 fix(test-a b3 round 10): causal-field input classification + turn-closure completion boundary (r2 R9 HIGH-1/HIGH-2)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 40f5390cd6 test(test-a b3 round 10): RED discriminators for r2 R9 HIGH-1/HIGH-2 — native Skill continuation + terminal-thinking truncation (specimen-traced)
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 97b750fc1d fix(test-a b3 round 9): JSONL-schema-aware capture — output-only, native-turn completion, intervening-input fail-closed, raw probe delivery
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 697bf316ef test(test-a b3): RED-first pins 4-5 on the capture atom — intervening-input fail-closed + envelope neutralization (desk custody ruling 6fa281f1)
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.
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 e77845a3fd test(test-a b3): RED-first pins for the harness-correction atom — output-only capture + native-turn completion
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]>
2026-08-25 18:42:22 -07:00
v-openrig-build 9635df4a30 docs(s09): absorb the 0.5.3 capability delta into the world-install canon (dc57a5d1 patch section, SOP 4.2)
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).
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 bf6315bb66 fix(test-a b3 round 8): bind the record generation for the session lifetime (r2 round-7 HIGH-1)
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]>
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 8e37ffcd18 fix(test-a b3 round 7): wire the current-generation reader into the shipped runner + entry discriminator
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]>
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 85157db9c4 fix(test-a b3 round 6): generation-bound append-only boundary (desk ruling Option B)
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]>
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 8a85e247d5 test(test-a b3): lock r2 HIGH-1 — bounded-rotation repeated-command discriminator (permanent pin)
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]>
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 83d0e26153 fix(test-a b3 round 5): transcript-boundary capture — remove the forbidden marker send
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]>
2026-08-25 18:42:22 -07:00
v-openrig-build 717960a5ce feat(s05): wire run-evals --provider rig to a live persistent seat (Test-A preflight blocker 3, row testa-provider)
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]
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 e93a4bdbec test(resume-metadata): guard — periodic snapshot refresh is single-attempt per codex seat (no x8 burst)
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]>
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 6f14e7773b test(model-divergence): guard — one process census per pass, never per unresolved seat
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]>
2026-08-25 18:42:22 -07:00
v-openrig-build 39a2f2ca38 feat(s05): production world/install atom graph — Test-A preflight repair (row 0ac358a9; r2 verdicts 224fc30c + a5a63659 repaired in-candidate)
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]
2026-08-25 18:42:22 -07:00
v-openrig-build 2d0d5a630e docs(conventions): scratch-daemon DB hermeticity rule (desk ruling 7960f4dd)
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.
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 b9a7795d55 fix(seat-activity): single-flight guard on the whole-fleet sweep (mirror the structural sibling)
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]>
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 e5f2d72096 test(seat-activity): RED — a slowed whole-fleet sweep must not overlap another (single-flight)
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]>
2026-08-25 18:42:22 -07:00
v-openrig-build 9a4df1e1e0 feat(s05): framing amendment per founder ruling a6f7a1db + desk D1-D6 (row 42e42bec, ruling 4c8644a3) — snapshot title, first-screen non-normative Status section, description qualifier; body bytes frozen; membership comment re-stamped to post-framing canon hash [NOT guard-accepted] 2026-08-25 18:42:22 -07:00
v-openrig-build 4b9ec17262 fix(s05): r1 0823c8e6 repair — forward-port stale canon (51-09 sender-identity supersession restored verbatim; openrig-architect spec-audit line restored); session-source-fork rewording confirmed SANCTIONED (rig fork shipped 2026-08-07, dated canon Update note); EXTERNAL_CANON_PENDING self-destructed (both skills landed) [NOT guard-accepted] 2026-08-25 18:42:22 -07:00
v-openrig-build bdc404fdd4 feat(skills): graduate context-engineering to product + the ruled charged-term sanitization + canon-drift regeneration [NOT guard-accepted]
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.
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 b0187508b0 docs(authority): retire overstated "founder-gated" apply wording — mechanics unchanged
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]>
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 04047ce68c fix(detector): isolate each seat's check so a per-seat throw fails loudly, not silently
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]>
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 61a5577f19 test(detector): RED — a per-seat throw must not silently truncate the divergence pass
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]>
2026-08-25 18:42:22 -07:00
v-openrig-build 77a0cb4d0a test(daemon): repair the three stale pins to the one-home law; add the true-absence case (desk ruling 9acb0aae) [NOT guard-accepted]
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.
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 dcbbbaf689 fix(KI-5.3-2): one logical-checkbox grammar via a twin — proof-add stops mispairing evidence
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]>
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 82c38b2d2f test(KI-5.3-2): RED — proof-add item grammar must agree with the review composer
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]>
2026-08-25 18:42:22 -07:00
v-openrig-build 54be226162 fix: KI-5.3-2 source-selection one-home — selectProofContractBody in the scaffold-placeholder twins; all readers consume it [NOT guard-accepted]
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 1c63d7fe1e test(daemon): RED — KI-5.3-2 source-selection split-brain discriminators (committed ahead of the post-Slice-07 re-ground per row e69daaef) [NOT guard-accepted]
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 060ceacdb3 fix(cli): KI-5.3-2 second face — a pristine-scaffold contract is never the canonical index [NOT guard-accepted]
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 0123f4734f test(cli): RED — KI-5.3-2 second face: a pristine-scaffold contract is never the canonical index [NOT guard-accepted]
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.
2026-08-25 18:42:22 -07:00
v-openrig-buildandClaude Opus 4.8 a8cd1cf249 fix(repairs re-review HIGH-1): preflight exempts behavior cases from production-ref resolution
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]>
2026-08-25 18:42:22 -07:00
v-openrig-build 2a756f9ab1 fix(evals): OPR.0.5.3.7 re-review r2 — runnable eval entry + no temp-package leak
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 2c52712a15 fix(evals+router): OPR.0.5.3.7 re-review — hermetic production-ref resolution + truthful instructions
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 6e66267335 fix(evals+router): OPR.0.5.3.7 Repairs 1+2 — router ask->ref discovery + canonical production refs
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 03cf24600b feat: Test-A pre-drive — session-persistent RigSeatProvider mode + the walk/profile identity join [NOT guard-accepted]
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 9ffe666415 test: RED — Test-A pre-drive pins: session-persistent provider mode; the walk/profile identity join [NOT guard-accepted]
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 198cc10c41 fix(daemon): parser unification — resolveAuthoredRecapPointer one-homed on canonical parseSessionName [NOT guard-accepted]
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.
2026-08-25 18:42:22 -07:00
v-openrig-build 9caa1e40cf test(daemon): RED — parser-unification pins: resolveAuthoredRecapPointer on canonical parseSessionName [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build d492482aea feat: OPR.0.5.3.5 recap-write atom — the boundary write verb + the authored-recap pointer leg in the boot packet [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 2a43b0ea9c test: RED — recap-write atom pins: the authored recap leg in the boot packet (by ADDRESS); the boundary write verb [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build c946140f6c feat(daemon): OPR.0.5.3.5 Q3 harness bridge — the behavior category + probe/EvalCase reconciliation + atoms-as-case-data [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build e020439ec5 test(daemon): RED — OPR.0.5.3.5 Q3 harness bridge pins: behavior category, probe/EvalCase reconciliation, atoms-as-case-data, one invocation grades both slices [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build a11edfc6cc fix: RECAP atom r1 repairs — collision-safe chain naming; explicit runtime vocabulary with a voice on unknown [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 70045fc296 test: RED — RECAP atom r1 pins: same-ms supersession loses nothing; claude-code maps explicitly; unknown runtime speaks; failed gate leaves state untouched [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 4f97f5498d feat: OPR.0.5.3.5 RECAP atom — seat recap store (superseded chain, addressability gate, advisory contract) + r1's 4d observations landed [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build b1e4af8321 test: RED — OPR.0.5.3.5 RECAP atom pins: seat recap store (superseded chain, addressability gate, advisory contract) + r1's 4d observations [NOT guard-accepted]
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).
2026-08-25 18:42:21 -07:00
v-openrig-build 4448b299dd feat: OPR.0.5.3.5 Atom 4d — profile CLI verb + mission-root on the ruled key + r1's rec pins named in place [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 5e14d42cc7 test: RED — OPR.0.5.3.5 Atom 4d pins: r1's 4c recs + mission-root on the ruled key + the profile CLI verb [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build e7e8993a65 feat(cli): OPR.0.5.3.5 Atom 4c — rig context get accepts the address form: one name#H2/H3 grammar across the verb family [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 5c335250a1 feat(daemon): OPR.0.5.3.5 Atom 4c — resolve-address route: name#H2/H3 through the ref grammar, one resolver home [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 7e96ed8b65 test(daemon): RED — OPR.0.5.3.5 Atom 4c pins: name#H2/H3 through the ref grammar; ambiguous addresses fail loud [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 0326c684e5 fix(daemon): 4b r1 F1 — escape predicate compares path SEGMENTS: '..' exact or '..'+sep prefix, never a bare startsWith [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 01c5921ce3 test(daemon): RED — 4b r1 F1 pin: '..'-prefixed FILENAME inside the root is not an escape; compare path segments, never prefixes [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 38ef51b32f fix(daemon): Atom 4b round 2 — byte provenance per piece; the rig/seat grant semantics; dangling symlinks named [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 7fd451a7eb test(daemon): RED — Atom 4b round-2 pins: r1's 4a riders — byte provenance per piece; the rig/seat grant semantics [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 3ffe69989b feat(daemon): OPR.0.5.3.5 Atom 4b — the profile route: situation-composed delivery over library + seat tree [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build f46c66bd43 test(daemon): RED — OPR.0.5.3.5 Atom 4b pins: the profile route composes situation delivery with a seat-tree recap by address [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build e68de15f9d feat(daemon): OPR.0.5.3.5 Atom 4a — two pre-# resolvers behind one grammar: seat:/mission: tree refs from config-resolved roots [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build f5d90a55fd test(daemon): RED — Atom 4a ingest pin: tree-prefixed atom addresses are legal; traversal and non-markdown header rules still bind [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 6ec58f88c4 test(daemon): RED — OPR.0.5.3.5 Atom 4a pins: two pre-# resolvers behind one grammar (seat:/mission: tree refs, config-resolved roots) [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build b1266f0458 feat(refocus): resolve context library refs 2026-08-25 18:42:21 -07:00
v-openrig-build be47006776 fix(daemon): Atom 2 r1 repairs — iterative cycle walk (no RangeError escape at any depth); unknown atom keys reject loud [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build e98c831ca3 test(daemon): RED — Atom 2 r1 pins: deep requires chain never escapes the fail-loud channel; unknown atom keys reject [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build e620447406 feat(daemon): OPR.0.5.3.5 Atom 3 — composeProfile: the situation algebra over one atom graph [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build b8204d817c test(daemon): RED — OPR.0.5.3.5 Atom 3 pins: the situation algebra composes profiles from ONE atom graph [NOT guard-accepted]
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).
2026-08-25 18:42:21 -07:00
v-openrig-build ad4168ea7a feat(daemon): OPR.0.5.3.5 Atom 2 — install atoms in the pack manifest: address + composition metadata [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build ed55a2c51e test(daemon): RED — OPR.0.5.3.5 Atom 2 pins: install atoms declare composition metadata in the pack manifest [NOT guard-accepted]
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).
2026-08-25 18:42:21 -07:00
v-openrig-build eb3431a8ee fix(daemon): Atom 1 r1 repairs — unterminated-fence finding; null-scope test keeps children under an unaddressable H2 [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 9caadab528 test(daemon): RED — Atom 1 r1 pins: unterminated fence is a validator finding; empty-slug H2 never promotes its children [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 2f3ace1117 feat(daemon): OPR.0.5.3.5 Atom 1 — addressable-markdown resolver core (Q1 grammar, one home) [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build e3bf03adfa test(daemon): RED — OPR.0.5.3.5 Atom 1 pins: addressable-markdown resolver core (Q1 grammar, fence protection, fail-loud, validator) [NOT guard-accepted]
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).
2026-08-25 18:42:21 -07:00
v-openrig-build 616c2a6b85 fix(router): project canonical trigger net 2026-08-25 18:42:21 -07:00
v-openrig-build 78c8846474 feat(router): OPR.0.5.3.7 R3 — thin router stub teaches the pull
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 4bdd979bb1 fix(evals): OPR.0.5.3.7 R6 — recorded grades explain their own verdict
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.
2026-08-25 18:42:21 -07:00
v-openrig-build e0fb1796d4 feat(evals): OPR.0.5.3.7 R6 — run-evals CLI + live-seat provider seam
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 27b25a44b4 feat(evals): OPR.0.5.3.7 R6 — seed fixture context-library
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.
2026-08-25 18:42:21 -07:00
v-openrig-build caa8619a77 feat(evals): OPR.0.5.3.7 R6 — runner + provider seam (harness runnable end-to-end)
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).
2026-08-25 18:42:21 -07:00
v-openrig-build 56b034f5f8 feat(evals): OPR.0.5.3.7 R6 — eval-case schema + the authored selection/loading set
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 4e928432bc feat(evals): OPR.0.5.3.7 R6 — deterministic door grader (RED-first)
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.
2026-08-25 18:42:21 -07:00
v-openrig-buildandClaude Opus 4.8 5c83308672 fix(cli): OPR.0.5.3.7 R4 — HIGH-1: keep URL-pack files inside the manifest origin+directory
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]>
2026-08-25 18:42:21 -07:00
v-openrig-buildandClaude Opus 4.8 c59cfa33f9 fix(cli): OPR.0.5.3.7 R4 — MEDIUM-1: resolve URL-pack files from the manifest's FINAL url
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]>
2026-08-25 18:42:21 -07:00
v-openrig-buildandClaude Opus 4.8 50d2aae6b7 feat(cli): OPR.0.5.3.7 R4 — rig context add <url> (external load) + config-resolved landing zone
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]>
2026-08-25 18:42:21 -07:00
v-openrig-buildandClaude Opus 4.8 10ff521c78 fix(cli): OPR.0.5.3.7 R2 — HIGH-1: project the FULL mirror ship set, not a narrower suffix set
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]>
2026-08-25 18:42:21 -07:00
v-openrig-buildandClaude Opus 4.8 f0e5c5a0a4 feat(cli): OPR.0.5.3.7 R2 — content ships WITH the CLI (the staleness kill)
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]>
2026-08-25 18:42:21 -07:00
v-openrig-buildandClaude Opus 4.8 5dcea470be feat(cli): OPR.0.5.3.7 R1 — rig context get, the agent pull verb
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]>
2026-08-25 18:42:21 -07:00
v-openrig-buildandClaude Opus 4.8 d267465b7b feat(cli): OPR.0.5.3.1 item 4 — point-of-use hints for the Claude scrollback renderer
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]>
2026-08-25 18:42:21 -07:00
v-openrig-buildandClaude Opus 4.8 13d2165bd4 feat(daemon): OPR.0.5.3.1 item 1 — classic-renderer launch env for Claude scrollback
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]>
2026-08-25 18:42:21 -07:00
v-openrig-build 054dffeedd fix(daemon): r2 BLOCKING-1 — CANONICAL_MODEL_PINS is null-prototype; arbitrary string pins can never read inherited Object members [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build e6d0d61987 test(daemon): RED — r2 BLOCKING-1 pins: prototype-key pins are unknown aliases — diverge, never throw, never blind the pass, never fabricate advisories [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build ac76e9f7ee fix(daemon): f7dfca0c — end the alias-pin false-positive class: modelsMatch canonicalizes the pin through the ONE shipped map [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build 754ec813cc test(daemon): RED — f7dfca0c pins: alias pin canonicalizes through the ONE shipped map; canonical-effective alias seat makes NO proclamation [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build fa173f44cf fix(daemon): e91d7a94 — settled-permanence for identity-keyed HOME entries kills the expiry herd; probe START time is the predicate [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-build fe752e3f6a test(daemon): RED — e91d7a94 pins: settled-permanence herd pin, W-5.3-1 collision pin, deferred-probe start-time pin [NOT guard-accepted]
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.
2026-08-25 18:42:21 -07:00
v-openrig-buildandClaude Opus 4.8 22af3e7b3b fix(daemon): S10 follow-on — CodexThreadIdResolver identity REQUIRED (restacked onto fix-2 TTL split)
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]>
2026-08-25 18:42:21 -07:00
v-openrig-build 31b9b74e1e fix(daemon): OPR.0.5.3.10 soak finding — per-entry TTL split: identity-keyed entries outlive the poll cadence [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 8f881a25b0 fix(daemon): OPR.0.5.3.10 r2 round-6 — the ambiguous start second fails CLOSED (strict ts > startTs) [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 277366795b fix(daemon): OPR.0.5.3.10 r2 round-5 — exact reuse boundary: no slack on the time gate [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 0a333fdf7e fix(daemon): OPR.0.5.3.10 r2 round-4 — time-gated log reads: a retired occupant's rows never resolve for a reused pid [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 7c8203721f fix(daemon): OPR.0.5.3.10 r2 round-3 — (pid,identity)-keyed cache and in-flight coalescing [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 73aa5f2f54 fix(daemon): OPR.0.5.3.10 — process-identity invalidation (pid+start-time from the census) [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 2bc3287538 fix(daemon): OPR.0.5.3.10 — pid-home TTL (fallback bound) + strict census lister [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build f9dc279ae3 test(daemon): RED — process-identity invalidation + start-time-carrying census rows [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 4304e80d7b test(daemon): RED — OPR.0.5.3.10 dual-gate blockers pinned: pid-home TTL, strict census lister [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 4dd7ec0a1b fix(daemon): OPR.0.5.3.10 — pid-coalesced home resolution; default-home answers cached [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 862748c3d2 fix(daemon): OPR.0.5.3.10 — one process census per cycle; zero pid-home subprocess on the default path [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build da5d2f4c36 fix(s06): close shipped context review concerns [NOT reviewer-accepted] 2026-08-25 18:42:21 -07:00
v-openrig-build 211865a1b7 fix(s06): r2 dual-gate blockers — door wiring, total best-effort, traversal rejection + THE DOOR TEST [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build efb1a16169 test(s06): RED — r2 dual-gate blockers pinned: rig-up door install, total best-effort, traversal rejection [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build b42ccc8ffc test(s06): config-route key census 54 -> 55 (topology.root) [NOT guard-accepted]
The third key-list pin (after the two twin VALID_KEYS pins) — the /api/config
route census. Caught only by the honest full-suite capture.

Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
2026-08-25 18:42:21 -07:00
v-openrig-build 71259b4dce test(s06): context grammar admits trace — delivery-free library member [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build e80a4dec19 feat(s06): demo worked project-tree example + legacy topology migration path [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 9dff601b9c feat(s06): promote the refocus channel into openrig-core — configurable content, no project residue [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 2c4c0bbddc feat(s06): shipped topology chain-file defaults, installed at rig-up [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 8315b2d005 docs(s06): chain-file convention SSOT — names, altitudes, trace, legacy advisory, curation path [NOT guard-accepted]
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
2026-08-25 18:42:21 -07:00
v-openrig-build 85b147f657 fix(s06): legacy topology root resolves where legacy code actually wrote [NOT guard-accepted]
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
2026-08-25 18:42:21 -07:00
v-openrig-build 127739e7ce feat(s06): productized chain-file trace — rig context trace, keyed off topology.root [NOT guard-accepted]
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
2026-08-25 18:42:20 -07:00
v-openrig-build 86d73e6566 feat(s06): topology.root typed config key in both settings twins [NOT guard-accepted]
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
2026-08-25 18:42:20 -07:00
v-openrig-buildandClaude Opus 4.8 d0856a8e2a fix(daemon): OPR.0.5.3.3 r2 HIGH-1 round 2 — legacy alias advisory before the id guard
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]>
2026-08-25 18:42:20 -07:00
v-openrig-buildandClaude Opus 4.8 a115a7a534 fix(daemon): OPR.0.5.3.3 r2 HIGH-1 — alias-pin advisory on the LEGACY validate path too
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]>
2026-08-25 18:42:20 -07:00
v-openrig-buildandClaude Opus 4.8 876db83ce7 feat(daemon,cli): OPR.0.5.3.3 items 1+2 — expire the model-pin alias bridge + spec-validate advisory
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]>
2026-08-25 18:42:20 -07:00
v-openrig-build 1a93c6fe6b test(daemon): mechanism pin — constructing the production drift observer execs nothing [NOT guard-accepted]
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
2026-08-25 18:42:20 -07:00
v-openrig-buildandClaude Opus 4.8 eff94da17a fix(daemon): transcript ingest-health — persistence-gated liveness + generation guard
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]>
2026-08-25 18:42:20 -07:00
v-openrig-buildandClaude Opus 4.8 cab57e2c2e fix(daemon): decouple transcript ingest-health from file mtime
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]>
2026-08-25 18:42:20 -07:00
v-openrig-build 06065ac48e fix(daemon): test hermeticity — no claude exec from constructed-but-unconsulted drift observers [NOT guard-accepted]
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
2026-08-25 18:42:20 -07:00
v-openrig-build b6cb25ab36 test(daemon): RED — hermeticity pin: createTestApp must never exec a real runtime binary [NOT guard-accepted]
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
2026-08-25 18:42:20 -07:00
v-openrig-build 198c038645 fix(KI-14): r2-B1 — shared shell classifier; the launcher-selected shell is authoritative blank [NOT guard-accepted]
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
2026-08-25 18:42:20 -07:00
v-openrig-build b32558cff0 test(KI-14): RED — r2-B1 pins: configured/arbitrary default shells and login-prefix are valid blanks [NOT guard-accepted]
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
2026-08-25 18:42:20 -07:00
v-openrig-build 1a8a94a640 fix(KI-14): fresh handover = verified blank shell + honest continuity label [NOT guard-accepted]
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
2026-08-25 18:42:20 -07:00
v-openrig-build da27db503c test(KI-14): RED — fresh handover pins: verified-blank respawn + honest continuity label [NOT guard-accepted]
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
2026-08-25 18:42:20 -07:00
v-openrig-buildandClaude Opus 4.8 1b16e34d6b fix(daemon): transcript rotation — suppress unchanged-content rewrites
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]>
2026-08-25 18:42:20 -07:00
v-openrig-build 9d937a3023 test(daemon): model-fidelity e2e — readiness headroom sized to the MEASURED loaded boot [NOT guard-accepted]
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
2026-08-25 18:42:20 -07:00
mvschwarz 029acda80a docs: v0.5.2 release notes (CHANGELOG entry + docs/releases file)
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.
2026-08-22 10:15:31 -07:00
mvschwarz 7b7178dda8 Release 0.5.2
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.
2026-08-22 10:14:05 -07:00
v-openrig-build 5dcf5cd795 fix(B1): r1 LOW — drop the stale adopt-failure node when the launcher proves the seat alreadyRunning [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build f5099b2af3 test(B1): RED — r1 LOW pin: adopt-failed seat proven alreadyRunning must not reach triage [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build d45cd9d145 fix(B1): GAP 2 — surviving-panes ADOPT branch per stamped Amendment 2 [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 491893ccb0 test(B1): RED — Amendment-2 adopt-branch pins fail against the pre-amendment conductor [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 6d44bc78b8 fix(B1): ROUND 10 gap 1 — render the ⏎ confirm banner IN the cockpit (was invisible) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 96ddde7e5f fix(B1): ROUND 9 — unanimity over FULLY-RESOLVED terminals only (walk cap/cycle = unknown) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 487816306e fix(B1): ROUND 8 — refused as a POSITIVE-form predicate (terminal-attempt unanimity) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 2b4b780a28 fix(B1): ROUND 7 — refused requires UNAMBIGUOUS refusal; mixed aggregate stays unverified [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 09f0b6b435 fix(B1): ROUND 6 — the crash-cart probe now recognizes the REAL Node refusal shape [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build d0b7f141c1 fix(B1): ROUND 5 — the whole-unit composition GUARD found: reachability, honesty, scope [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 3e6025d9a1 fix(B1): ROUND 4 — close the advertise-without-wire CLASS across the restore surface [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build b50f32dcfa fix(B1): ROUND 3 — detach-to-reattachable lifecycle + keyboard-walkable triage [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 27073ea4a7 fix(B1): ROUND 2 — the operator OWNS the restore lifecycle (cancel/progress/triage) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build c153189a29 fix(B1): async rollup-stream rework — wire the conductor to the production path [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 47c5cf3906 feat(cli,tui): B1 Atom D — crash-cart restore-fleet CLI verb + main.ts enter drive [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 6521c94839 feat(tui): B1 Atom D — the enter RESTORE EVERYTHING drive (start daemon then conductor) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 0c21a8f18e feat(daemon): B1 Atom C — attention/triage wiring into the fleet rollup (R5) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 32d5b71a8d feat(daemon): B1 Atom C — fleet rollup aggregation (ARCH-RULING Q2) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 0a5b8b8ad2 feat(daemon): B1 Atom B — crash-cart conductor daemon route (POST /api/crash-cart/restore-fleet) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 8bb4a26da9 feat(daemon): B1 Atom B — kernel-first rig ordering (R2) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 8db4cc7323 feat(daemon): B1 Atom B — default restoreRig dep (compose findLatestRestoreUsable + restore) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 634914ce26 feat(daemon): B1 Atom B — crash-cart restore conductor core [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build ab6b0f95cd fix(testbed): committed real StagingDocker invoker — the L6 tar-hang cause dies [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build 84b2b18cbe test(testbed): RED — real StagingDocker invoker contract (pipe, EOF, dual-exit, timeout) [NOT guard-accepted]
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)
2026-08-22 10:11:58 -07:00
v-openrig-build a5a1198f04 gate: the web-UI leg is ruled out — the gate runs typecheck + vitest only
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)
2026-08-22 10:11:58 -07:00
v-openrig-build d35d8ce70c fix(daemon): retained id rebinds path/source to this poll's readable entry [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 6c556c9127 fix(daemon): cross-poll advancement with retention; bridge expiry is a behavioral sentinel [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build e457505215 fix(daemon): record selection proves advancement, never recency; the bridge expiry is executable [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build c55160565b fix(daemon): fail-closed record selection; exact pins + one self-expiring migration bridge [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 09679b9c21 fix(daemon): current claude record selected by RECORD-LIVENESS; pins match as aliases [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 319899c202 fix(daemon): detector reads the CURRENT generation via the live pane process — masked divergence ends [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 11a8a03270 fix(daemon): restore packet uses the shared paste-then-submit sequencing — no more staged-unsent handovers [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 8edbe89a7d docs(cli): name the stale-takeover release cascade tail beside LOCK_STALE_MS (r1 F2 review — named, not built for)
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit c6213350308870c1a1a52d1208c25111c407827f)
(cherry picked from commit e463aaa94d2d57ccfc3a910994c836367abe9762)
2026-08-22 10:11:57 -07:00
v-openrig-build fdf43915c6 fix(cli): host-bindings writes serialize under an advisory lock — conflict records survive concurrent writers [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 71fb923782 test(repo): root hermeticity rides test.projects, not the deprecated workspace file [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 45980c429f test(repo): root vitest workspace — package hermetic-env guards apply to root invocations [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 0feee48dd0 perf(daemon): resolve_home goes async — the dominant post-cut event-loop blocker [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 2385f6179c docs(test): a hang in the window-wide-line regression IS the failure (sync grind outruns every timer) [NOT guard-accepted]
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit e26474fce2da139d800e75911556676bc2b1622b)
2026-08-22 10:11:57 -07:00
v-openrig-build 0b993f459c fix(daemon): full-window step when a line exceeds the scan window — the grind terminates [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build b9006c26b7 fix(daemon): straddle overlap sized from the dropped fragment; comment records the measured truth [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 7a272019c6 fix(daemon): codex effective-read covers real rollouts; detection-pending is observable [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build eee03548eb feat(daemon): model-divergence detector + four-channel proclamation (slice-07 A3) [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build cde38eda97 fix(cli): rig policy carries PRESENCE, not just strings — a malformed ref can never read as absent [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 3353af6085 fix(cli): rig policy runs on the authoritative permission-policy semantics; custom halves land [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 062d4e0908 feat(cli): introduce rig policy as the permission-policy verb (list/show/current/apply)
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 66a7988677 fix(daemon): claude recap leg fires on live handover — pre-launch resolution, ownership guard, loud null [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 48522e6a22 docs(as-built): bump cli-reference verification stamp to the rename candidate
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)
2026-08-22 10:11:57 -07:00
v-openrig-build a98104ed1b feat(cli): complete the rig policy -> rig mode rename (verb, API path, identifiers)
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)
2026-08-22 10:11:57 -07:00
v-openrig-build f0378f203b record(cli): rig policy -> rig mode rename landed in a57319df0 (release-notes record)
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)
2026-08-22 10:11:57 -07:00
v-openrig-build d7ccb9d530 docs(test): B12-T header describes the ordering discriminator, not the dropped 50ms bound [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build bbe82215c7 test(daemon): discriminating test for the B12 async list_processes conversion [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build b48ae7a93f test(ui): isolate mobile polish router fixture (B13, partial fold)
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 8eeab3ac34 feat(scope): a plan-lock set must be chosen, not inherited [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build c698574f60 gate: amend the vitest:ui resident - slice-11 specimen struck (B13 fixed it), table-crash STRESS named as the covered specimen with r1's dual-runtime receipt
(cherry picked from commit fd8aad81041db6f83baef09c2ebcc3ef07dd6b4c)
2026-08-22 10:11:57 -07:00
v-openrig-build da148b95d8 fix(watchdog): gate compares the same normalization the validator accepted [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 267efa0a2e fix(cli): mirror hostSelfId into the ps fixtures, re-base the compact ratio guard, allow --fields hostSelfId [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 5f53893edd feat(watchdog): idle-gate auto-registration becomes opt-in, never default-on [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 4c92a1f0a0 fix(daemon): per-row host attribution on node records; resume_type derives from the runtime [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 0249c05f3e fix(hosts): name the deliberate resolveHost twin divergence; reject future sidecar versions [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build d56dbb2b12 feat(cli): learned host-identity sidecar + healthz population (slice-14 Source-1) [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 21cafa6ac7 gate: two load-margin residents enter the exclusion ledger for the interim cut
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 914c37c93e fix(daemon): raise the predecessor boot recap to the permanent claude leg [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 1c199c8e4a fix(cli): mirror the fanout-contract twin byte-identical to daemon [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 2df3b3eef5 docs(guidance): thin the default boot overlay to identity + peer verbs; pin the walk-back as test absences
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 090b213c8f fix(daemon): scenario runner refuses unknown flags before running anything [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build be2c11bd9e fix(daemon): async-wrap the two ps list_processes sampling sites [NOT guard-accepted]
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)
2026-08-22 10:11:57 -07:00
v-openrig-build fa9c630927 fix(scope): repair three SPEC.md compatibility defects now live in main
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)
2026-08-22 10:11:57 -07:00
v-openrig-build c7d652b76a fix(hosts): three final-review repairs on the slice-14 candidate
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)
2026-08-22 10:11:57 -07:00
v-openrig-build dc54b0af5a perf(hosts): derive the self-host-id source at boot, not per /healthz request
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 7e71698e47 feat(hosts): make identity state legible before a cross-machine failure
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 323b81363f feat(hosts): registry gains the hostId join key; resolution matches alias or id
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 72ca4bb850 feat(scope): read SPEC.md as the work-node file, keep README.md valid
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 37d2a76087 fix(bundles): repair three review findings on the drift guard
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)
2026-08-22 10:11:57 -07:00
v-openrig-build 374e945e7c fix(bundles): refuse to export a spec that would drop live topology
`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)
2026-08-22 10:11:57 -07:00
mvschwarz a4ce0d0dc5 docs: v0.5.1 release notes (CHANGELOG entry + docs/releases file)
Formal CHANGELOG [0.5.1] entry + docs/releases/v0.5.1.md file, both
authored from the intent-vs-delivered trace at
openrig-work/missions/release-0.5.1 (not derived from 362 commit
subjects). Content structure matches prior release-notes shape.

Known-limits wording carried verbatim per the trace: container mode is
scaffolded but not driven (scoped forward); three of the intended
seed scenarios run green, the tenth cannot be built without two
capabilities that do not yet exist (scoped forward).

Companion to the Release 0.5.1 version-bump commit below; GH Release
body will use docs/releases/v0.5.1.md via gh release create
--notes-file at ceremony time.
2026-08-11 23:12:32 -07:00
1508 changed files with 157742 additions and 17847 deletions

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
name: Bug report
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.
required: true
+14
View File
@@ -0,0 +1,14 @@
blank_issues_enabled: true
contact_links:
- name: Ask a question
url: https://github.com/mvschwarz/openrig/discussions/categories/q-a
about: Setup, usage and "is this expected" questions go to Discussions Q&A.
- name: Share an idea or a setup
url: https://github.com/mvschwarz/openrig/discussions
about: Ideas and Show and tell live in Discussions.
- name: Documentation
url: https://openrig.dev/docs
about: Getting started, CLI reference, configuration and troubleshooting.
- name: Security report
url: https://github.com/mvschwarz/openrig/security/policy
about: Read how to arrange a private report. Do not include vulnerability details in public issues.
@@ -0,0 +1,32 @@
name: Feature request
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.
validations:
required: true
- type: textarea
id: workaround
attributes:
label: How do you handle it now?
- type: input
id: version
attributes:
label: OpenRig version
placeholder: 0.5.14
+21
View File
@@ -0,0 +1,21 @@
<!-- 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
+17
View File
@@ -0,0 +1,17 @@
# Getting help with OpenRig
- **Docs:** https://openrig.dev/docs — getting started, CLI reference, configuration,
troubleshooting. An index linking to agent-friendly documentation is at
https://openrig.dev/llms.txt.
- **Agents helping a user:** read [docs/reference/help.md](../docs/reference/help.md), or run `rig context get help`.
- **Questions:** [Discussions › Q&A](https://github.com/mvschwarz/openrig/discussions/categories/q-a).
- **Bugs:** [open an issue](https://github.com/mvschwarz/openrig/issues/new/choose) with the bug
template. Include `rig --version`, your OS and Node versions, and relevant redacted command output.
Reports are public: remove credentials, private prompts and personal information.
- **Ideas and setups:** [Ideas](https://github.com/mvschwarz/openrig/discussions/categories/ideas)
and [Show and tell](https://github.com/mvschwarz/openrig/discussions/categories/show-and-tell).
- **Videos:** https://www.youtube.com/@openrig
- **Security:** see [SECURITY.md](../SECURITY.md); please report privately.
We aim to acknowledge issues and pull requests within one day. A substantive PR review decision
within seven days is a target, not a guaranteed deadline. Details in [CONTRIBUTING.md](../CONTRIBUTING.md).
+33
View File
@@ -0,0 +1,33 @@
name: Portability report
# Lists lines a pull request adds that contain values tied to one machine, network or account
# (credentials, home paths, temporary paths, network addresses, email addresses). Findings never
# fail the job; the author reads the report and decides what to change. An operational error (for
# example a git failure) does fail it. The repository is public, so this summary lists file and
# line only; authors see the matched text when they run the report locally or on push.
# See scripts/portability-report.mjs.
on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
portability-report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Report added lines with machine-specific values
run: >
node scripts/portability-report.mjs
--from ${{ github.event.pull_request.base.sha }}
--to ${{ github.event.pull_request.head.sha }}
--locations-only
--out "$GITHUB_STEP_SUMMARY"
+139
View File
@@ -0,0 +1,139 @@
name: Tests
# Builds, type-checks and runs every test suite on a clean GitHub runner, and records the file
# list each published package would contain. Each suite is its own job so one failure does not
# hide the others. No provider credentials or operator configuration are supplied.
# Package suites deny external networking; scenario runtime has no external route.
on:
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-package:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- name: Record the published file list of each package
run: |
set -euo pipefail
mkdir -p pack-lists
for p in cli daemon tui; do
(cd packages/$p && npm pack --dry-run --json --ignore-scripts) \
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{for(const f of JSON.parse(s)[0].files)console.log(f.path)})' \
| sort > pack-lists/$p.txt
echo "$p: $(wc -l < pack-lists/$p.txt) files"
done
- uses: actions/upload-artifact@v4
with:
name: pack-lists
path: pack-lists/
typecheck:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
repo-checks:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:repo
package-tests:
# macOS, the platform OpenRig runs on: several daemon and cli tests read macOS ps columns,
# file-inode behaviour and release tags.
runs-on: macos-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
package: [daemon, cli, tui, ui]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: brew install tmux
- run: npm ci
- name: Build the packages the tests import
run: npm run build
- name: Run the suite without ambient credentials or external network
run: |
suite_root=$(mktemp -d "$RUNNER_TEMP/opr-test.XXXXXX")
suite_root=$(cd "$suite_root" && pwd -P)
mkdir -p "$suite_root/home" "$suite_root/bin"
# Same system bytes, nonprivileged mode; no system file is changed.
/usr/bin/install -m 0755 /bin/ps "$suite_root/bin/ps"
# Only this job's temporary sockets (including tmux) are reachable.
env -i HOME="$suite_root/home" PATH="$PATH" TMPDIR="$suite_root" TMUX_TMPDIR="$suite_root" \
CI=true TERM=xterm-256color \
sandbox-exec -D IPC_ROOT="$suite_root" -p '(version 1)(allow default)(deny network-outbound)(allow network-outbound (remote ip "localhost:*"))(allow network-outbound (subpath (param "IPC_ROOT")))' \
/bin/bash -s "${{ matrix.package }}" <<'SUITE'
set -euo pipefail
node scripts/ci-macos-process-probe.mjs
# The production readers use bare ps. Select real system bytes only after
# self/owned-child observation and network/IPC confinement are proved.
export PATH="$TMPDIR/bin:$PATH"
test "$(command -v ps)" = "$TMPDIR/bin/ps"
exec npm run test -w "packages/$1"
SUITE
installed-scenario:
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Installed package, daemon restart, and seeded durability failure
run: bash scripts/run-pr-scenarios.sh
- uses: actions/upload-artifact@v4
if: always()
with:
name: installed-scenario-evidence
path: dist/pr-scenarios/
if-no-files-found: error
+7
View File
@@ -32,3 +32,10 @@ demo/proof/
# Build artifacts
*.tgz
# OPR.0.5.3.7 R2 — the shipped context-pack projection is DERIVED AT PACKAGE TIME
# (scripts/generate-context-packs.mjs), never committed or edited in place.
packages/daemon/context-packs/
# Internal build receipts; never commit
/.evidence/
+788 -5
View File
@@ -8,6 +8,789 @@ deprecations, and behavioral changes. Breaking changes are called out explicitly
---
## [0.6.3]
- Recognize running managed Claude Code seats behind OpenRig's shell wrappers
using pane lineage, foreground process and native session identity checks.
This repairs the 0.6.2 messaging refusal reported by
[@dmelo](https://github.com/dmelo) in
[#197](https://github.com/mvschwarz/openrig/issues/197)
([#220](https://github.com/mvschwarz/openrig/pull/220)).
- Clean up unusable snapshot helpers when their ownership is proven, instead
of accumulating them on repeated attempts. Thanks to
[@z4cc](https://github.com/z4cc) for
[#188](https://github.com/mvschwarz/openrig/issues/188)
([#189](https://github.com/mvschwarz/openrig/pull/189)).
- Keep archived duplicate rigs out of seat-reference resolution and preserve
another live seat's session and queue work when removing a stale node.
Thanks to [@Farkinell](https://github.com/Farkinell) for
[#174](https://github.com/mvschwarz/openrig/issues/174)
([#181](https://github.com/mvschwarz/openrig/pull/181)).
- Protect proof media from artifact writes: reject binary artifact bodies and
require an explicit `--replace` to overwrite an existing Markdown artifact
([#177](https://github.com/mvschwarz/openrig/pull/177)). Repository script tests
also run serially to prevent interference from shared build outputs
([#221](https://github.com/mvschwarz/openrig/pull/221)).
See [0.6.3 release notes](docs/releases/v0.6.3.md) for the verification limits.
Source regression coverage does not establish installed Claude/Fedora delivery.
## [0.6.2]
- Start with two Claude Code agents, two Codex agents, or a Claude owner and
Codex checker, using the same first-project task and review path. Agent-guided
setup recommends a scoped `rig` command allowance after your explicit choice;
broader permissions remain separate
([#147](https://github.com/mvschwarz/openrig/pull/147)).
- Install schema-version-2 bundles into `--target` and launch from those retained
files. Local CLI bundle paths resolve from your working directory; conflicting
target files are preserved ([#146](https://github.com/mvschwarz/openrig/pull/146)).
Without `--target`, `rig up <file>.rigbundle` installs into your current directory, so run it from the project folder you want.
- Refuse message delivery into a bare shell where an agent runtime should be
running ([#150](https://github.com/mvschwarz/openrig/pull/150), fixes
[#142](https://github.com/mvschwarz/openrig/issues/142)).
- Preserve the named target during managed launches after a tmux/host restart,
instead of confusing it with an old bare pane binding; existing identity checks
remain. Thanks to [@diaztunjano](https://github.com/diaztunjano) for reporting
[#141](https://github.com/mvschwarz/openrig/issues/141), addressed by
[#151](https://github.com/mvschwarz/openrig/pull/151).
- Repair self-host sender identity, tmux window parsing, Codex hook-path
canonicalization, Pi input editing, TUI health display and restore diagnostics;
carry runtime hints to Herdr and allow configured Anthropic endpoint forwarding.
See the [community fixes and credits](docs/releases/v0.6.2.md#community-fixes).
See [0.6.2 release notes](docs/releases/v0.6.2.md) for usage and compatibility.
Installed first-use and reboot/power-loss verification remain pending; these
source changes do not establish those outcomes.
## [0.6.1]
- One version-matched agent help guide: `rig context get help`, also available
inside the installed package when the CLI cannot run. It links setup, restart,
permissions and instance guidance, with a support route at hello@openrig.dev
([#113](https://github.com/mvschwarz/openrig/pull/113)).
- `rig view show execution --project <catalog-id> --mission <mission>` selects
a catalogued project's missions. Thanks to
[@dajiaohuang](https://github.com/dajiaohuang)
([#105](https://github.com/mvschwarz/openrig/pull/105)).
- Recognize the Codex `»` conversation prompt during startup/resume checks,
including after a dismissed hook-review panel. Unresolved menus remain gates.
Thanks to [@dajiaohuang](https://github.com/dajiaohuang)
([#111](https://github.com/mvschwarz/openrig/pull/111)).
- Repair the bundled Vault skill's frontmatter and check shipped skill headers
([#115](https://github.com/mvschwarz/openrig/pull/115)); remove historical
development evidence from the public source tree
([#118](https://github.com/mvschwarz/openrig/pull/118)) and the retired TUI
drivability prototype ([#119](https://github.com/mvschwarz/openrig/pull/119)).
- Run eight PR test jobs, including the UI suite and one installed queue-durability
scenario with an intentional failure control
([#117](https://github.com/mvschwarz/openrig/pull/117)). This does not cover all
historical scenarios or every platform.
- Parse SQLite boot timestamps as UTC on non-UTC hosts so current identity and
context readings are compared with the correct generation start time. Thanks
to [@Coder8124](https://github.com/Coder8124)
([#124](https://github.com/mvschwarz/openrig/pull/124)).
- Recognize headerless Codex conversations with custom status-line field order
and mixed-case model names ([#125](https://github.com/mvschwarz/openrig/pull/125));
thanks to [@Hexgunner69](https://github.com/Hexgunner69) for the report and
[@Aummadour](https://github.com/Aummadour) for regression cases. The separate
stale restore-warning issue is not fixed by this change.
- Resolve a linked worktree's Git metadata directories for Codex fresh launches
instead of passing its `.git` file as a directory
([#126](https://github.com/mvschwarz/openrig/pull/126)); thanks to
[@mgall-ibizdigital](https://github.com/mgall-ibizdigital) for the report and
suggested approach.
See [0.6.1 release notes](docs/releases/v0.6.1.md) for changes and compatibility
limits. Slack manifest/setup assistance and Rig Stream classification remain
experimental; no new validation of those experiments is claimed.
## [0.6.0]
- **Breaking:** OpenRig requires Node.js 22 or 24 and uses better-sqlite3 13.
Node 20 is no longer supported, and the install check refuses it; Node 26 is
untested. Switch Node, then reinstall the CLI; existing
data is migrated in place. See [Moving off Node 20](README.md#moving-off-node-20).
Thanks to [@jimallen](https://github.com/jimallen) for reporting the Node 26
install failure and proposing the upgrade ([#16](https://github.com/mvschwarz/openrig/pull/16)).
- Choose permissions per seat for future launches with
`rig seat set-permissions <seat> --mode <mode> --reason <text>`. Codex
`full_bypass` now also sets `-a never`; Claude Code modes such as `auto` are
accepted only when the managed executable supports them. Rig-level verbs move
to `rig policy permissions …`, with the old verbs kept as aliases. Thanks to
[@DoowanKang](https://github.com/DoowanKang) ([#30](https://github.com/mvschwarz/openrig/issues/30))
and [@djogss](https://github.com/djogss) ([#33](https://github.com/mvschwarz/openrig/issues/33)).
- Protect a seat where you type by hand with `rig seat set-typing-guard`. While it
is on, automatic messages and wakes are held instead of typed in; the default is
unchanged. Thanks to [@some-marketing](https://github.com/some-marketing)
([#48](https://github.com/mvschwarz/openrig/issues/48)).
- Experimental: `rig slack manifest` prints the Slack app manifest offline, with a
prefilled create-app link, and a new setup guide describes the manual steps.
Creating the app remains a step you do in Slack, and the steps have not been
confirmed against a real app creation. The existing Slack connector is not
experimental.
- Open a whole rig in Herdr from the TUI (`term ▸ rig <name>`), up to 16 seats per
tab in a workspace named after the rig; the empty starting tab is closed only
when that is confirmed safe. Thanks to [@shintaii](https://github.com/shintaii)
([#26](https://github.com/mvschwarz/openrig/issues/26)).
- Pi seat activity reports carry the occupant generation and are accepted only
for the seat's current occupant. Thanks to
[@DoowanKang](https://github.com/DoowanKang) ([#29](https://github.com/mvschwarz/openrig/issues/29)).
- The kernel starter summary identifies the library preview and explains automatic
runtime-variant selection. Thanks to [@tgrundtvig](https://github.com/tgrundtvig)
([#21](https://github.com/mvschwarz/openrig/issues/21)).
- Codex seats launch with `--no-daemon` when supported. Thanks to
[@reisalbuquerque](https://github.com/reisalbuquerque) ([#69](https://github.com/mvschwarz/openrig/issues/69)).
- Missions without a `metadata` block no longer fail the readiness reader. Thanks
to [@shravansumanthanan](https://github.com/shravansumanthanan) for the fix and
[@kainne44](https://github.com/kainne44) for the report
([#72](https://github.com/mvschwarz/openrig/issues/72)).
- Experimental and optional, off by default: a classifier seat can have Jev,
through OpenRouter, label Rig Stream observations. Check, turn on or turn off
with `rig project experimental status|enable|disable --config <file>`, then run
one bounded foreground `rig project wake … --experiment <file>`. Labels are
advisory, with no accuracy or reliability claim, and the feature may be
incomplete. See [stream classification](docs/reference/stream-classifier-worker.md).
- Report problems with either experiment through a
[GitHub issue](https://github.com/mvschwarz/openrig/issues/new/choose) or a pull
request ([CONTRIBUTING.md](CONTRIBUTING.md)).
Includes [#77](https://github.com/mvschwarz/openrig/pull/77),
[#84](https://github.com/mvschwarz/openrig/pull/84) by
[@mvdpoel](https://github.com/mvdpoel), [#91](https://github.com/mvschwarz/openrig/pull/91)
and [#94](https://github.com/mvschwarz/openrig/pull/94).
Release preparation: [#109](https://github.com/mvschwarz/openrig/pull/109).
An ordinary candidate-tarball install with real postinstall passed on fresh
macOS 15 ARM64 / Node.js 22.22.1, including SQLite 13, all 89 migrations,
write/reopen and unauthenticated daemon startup/shutdown. Fresh Node 24 install
and authenticated native fresh/resume/fork permission enforcement were not
completed. See the [verification scope](docs/releases/v0.6.0.md#verification-scope)
and [known limitations](docs/releases/v0.6.0.md#known-limitations). The final
artifact will be separately bound to the merged release commit.
## [0.5.17]
- Install the CLI with Bun as well as npm: `bun add -g @openrig/cli`. The package
no longer depends on the unpublished `@openrig/daemon`; it imports the daemon
copy it already ships, which also makes the package smaller. OpenRig still runs
on Node.js, and Bun may block the package's postinstall check. Thanks to
[@drewpayment](https://github.com/drewpayment) for reporting this
([#66](https://github.com/mvschwarz/openrig/issues/66)).
Includes [#68](https://github.com/mvschwarz/openrig/pull/68). The Node support
range and SQLite version are unchanged.
## [0.5.16]
- Let a rig write Claude Code's managed instruction blocks to `CLAUDE.local.md`
instead of a tracked `CLAUDE.md`, with `managed_blocks: { claude-code: CLAUDE.local.md }`.
The default stays `CLAUDE.md`, and Codex stays on `AGENTS.md`. Blocks already
written to `CLAUDE.md` are not moved; remove them by hand after switching.
Thanks to [@hvpaiva](https://github.com/hvpaiva) for reporting and proposing
this ([#25](https://github.com/mvschwarz/openrig/issues/25)).
- Add an advisory portability report for pull requests. It lists added lines
that contain machine-, network- or account-specific values. Findings never
fail the check; operational errors, such as a git failure, still do.
- Show badges and a short demo of agents working in the README.
Includes [#54](https://github.com/mvschwarz/openrig/pull/54) and
[#56](https://github.com/mvschwarz/openrig/pull/56) by
[@mvschwarz](https://github.com/mvschwarz). The Node support range and SQLite
version are unchanged.
## [0.5.15]
- Recognize Codex through shell and Node launchers during startup and recovery,
while keeping uncertain process identity visible.
- Skip recognized Codex update notices without installing provider updates, and
give clearer startup recovery guidance, including `rig up <name> --existing`.
- Add the OpenRig Software Factory recipe and worked example for continuing
reviewed work, with incremental team growth using `rig grow`.
- Guide agents through user-chosen command permissions, preserving existing rules
and explaining project versus user scope. Permission defaults are unchanged.
- Explain provider hooks, workspace trust and other machine changes before the
first launch.
- Show Pi replies and tool progress, retain managed OpenRig context in shell
tools, and report bounded provider errors without exhausted-retry duplicates.
- Submit pasted Pi messages explicitly and handle input beyond the terminal's
canonical buffer limit, including cancellation and oversized-input recovery.
- Add a guarded retry for an added seat whose first startup failed during resource
projection; retain its complete original configuration for recovery.
Pi support remains qualified and supervised: controlled coding and continuity
were verified, but ordinary useful-task completion and unattended teamwork remain
unverified. Shipped permission defaults, the Node support range and SQLite version
are unchanged; project work-policy features are outside this release.
Includes [#37](https://github.com/mvschwarz/openrig/pull/37) by
[@danielkuykendall23-boop](https://github.com/danielkuykendall23-boop), and
[#38](https://github.com/mvschwarz/openrig/pull/38),
[#39](https://github.com/mvschwarz/openrig/pull/39),
[#45](https://github.com/mvschwarz/openrig/pull/45) and
[#46](https://github.com/mvschwarz/openrig/pull/46) by
[@mvschwarz](https://github.com/mvschwarz).
**Known compatibility limitation:** on macOS arm64 with Node 24, SQLite dependency
installation can fail when a suitable prebuilt binary is unavailable, and
compiler-built SQLite has also shown runtime cleanup failures. Use Node 22 on
that platform for now; the declared Node support range is unchanged.
See [the release notes](docs/releases/v0.5.15.md) for starting commands and guidance.
## [0.5.14] - 2026-09-14
VM inspection was accepted: generally zero-to-two-second loading with
occasional timeouts, formatting, icon colors and default views. This is an
observed result, not a general latency or availability guarantee. The completed
single-consumer Slack/phone exercise remains evidence for unchanged notifications.
- Enter a confirmed running rig directly, with responsive Help, Skip and Local
during slow or unverified startup.
- Keep the navigator and labelled useful content during page/rig changes, slow
reads and failures; preserve scope identity, passive prior content and confirmed
removal semantics. Read the selected rig before unrelated fleet details.
- Send complete bounded human briefs with related thread detail, distinguish
quiet FYIs from decisions, and retain delivery/reply correlation.
- Find instance-wide Human requests and delivered Updates in Feed; open Health,
Configuration and Connections under System, with six top-level entries.
- Browse Feed categories, explicitly expand Specs kinds and Derived terminal
views, and find Saved views without expanding the whole catalog.
- Read mission outcomes, current or planned owner, blockers and next dependencies
above slice boxes at both terminal widths. Native accepted judgments drive
completion; assigned or reopened work and mission lifecycle remain distinct.
- Reduce repeated terminal inventory work while preserving saved-view preview
and explicit Herdr Open; no general latency improvement is claimed.
- Keep primary queue facts readable when optional blocker descriptions fail;
bound fleet reads across stages and avoid duplicate policy/receipt read work.
- Reduce restore/startup inventory scans with selective event indexes, preserving
history and projection results; private replay is not a live latency guarantee.
- Preserve nullable identity provenance during queue-history archival. Shared
history, RECENT and OWNER/receipt readers now include archived records;
unknown historical provenance stays null rather than being reconstructed.
Root, CLI, daemon and web UI are `0.5.14`; terminal TUI remains `0.1.0`.
Migration head advances to `084_inventory_event_indexes`, following review read
indexes in 083, archive identity provenance in 082 and explicit human-intent fields
in migration 081. Private installed review and controlled notification checks
retain their exact candidate and
runtime attribution. See [the release notes](docs/releases/v0.5.14.md) for
operator guidance and verification limits.
Known limitations: the inherited CLI health-check deadline can reject a queue
command before its request is sent; that defect and occasional server delay are
not fixed here. Same-candidate recovery did not establish durable responsiveness.
Bundled Workflow Specs can disappear from discovery when an upgrade changes the
install directory; that fix is explicitly deferred beyond 0.5.14. Recorded
shutdown/config-preservation uncertainties and the prior recorder-write failure
remain disclosed.
## [0.5.13] - 2026-09-10
**Status**: release candidate, unpublished. Exact-cut substance review,
release verification and publication remain pending; live adoption is separate.
- Enter the TUI with useful Help, Skip and local intent reading even while
the daemon is unavailable or its state is unverified.
- Choose a project, read its current sources and follow links, then return
to the same place without confusing projects that share a name.
- Separate requests needing human action from updates in ATTENTION, and
inspect their source without implying approval.
- Preview saved or derived terminal views before deliberately opening them;
navigate CONFIG, Help and health with readable values at narrow widths.
- Choose scoped human-led or delegated oversight and retain current context,
assessment and action evidence without claiming improved behavior.
- Update Git-backed context through an explicit merge that preserves local
authorship, conflicts and the distinction between selected and consumed bytes.
Root, CLI, daemon and web UI are `0.5.13`; terminal TUI remains `0.1.0`.
Migration head advances to `080_scoped_operating_posture`. Natural behavioral
effect remains **UNOBSERVED** and human time cost **UNMEASURED**. The default
multi-client control-socket limitation remains disclosed and unfixed.
See [the full release notes](docs/releases/v0.5.13.md) for usage and proof limits.
## [0.5.12] - 2026-09-09
**Status**: release candidate; exact-cut review, final verification and
publication remain pending.
- Record authorized proof judgments and corrections once; derive affected
slice, mission and project readiness with attributable evidence.
- Inspect authored versus running graphs, deliberately adopt supported
revisions, and reconcile ambiguous operations without duplicate work.
- Discover exception ownership before failure; admit dependency-failure
tasks/wakes atomically and recover one occurrence without hiding another.
- Keep healthy unchanged waits quiet while preserving change-driven wake,
bounded recovery and explicit expansion of compact reads.
- Enter through bare interactive `rig` for normal startup and selective
return, preserving conversation history or requesting an explicit fresh start.
- Browse general instance settings and provenance in CONFIG, with Slack as
one subsection and passive, secret-safe inspection.
Root, CLI, daemon and web UI are `0.5.12`; terminal TUI remains `0.1.0`.
Migration head remains `079_workflow_lifecycle_parallel`. Accepted development
and native evidence retains its original attribution; release preparation does
not imply a new human usability study, full-platform run or public adoption.
See [the full release notes](docs/releases/v0.5.12.md) for operational guidance
and known limits.
## [0.5.11] - 2026-09-07
**Status**: release candidate; final verification and publication remain pending.
- Start useful repository work with the focused `first-project` owner/checker
team and an attachable shared kernel TUI.
- Inspect workflow owners and waits, declared spec purpose, effective instance
configuration, and human connection state in the ordinary TUI.
- Navigate with command completion, readable Recent activity, and persistent
timezone selection. Unverified daemon probes no longer read as confirmed down.
- Reuse project release profiles that retain both the release ceremony and the
actual post-release boundary, independently of successor scope.
- Preserve context delivery and generation evidence; honor authored startup
proof selection; improve exception routing, bounded shutdown receipts,
startup child identity, and cross-host origin attribution.
- Diagnosis `show`/`list --json` now return summaries; use `--full --json`
for complete evidence. Queue previews also print an exact full-read command.
Root, CLI, daemon, and web UI are `0.5.11`; terminal TUI remains `0.1.0`.
Migration head remains `079_workflow_lifecycle_parallel`.
The health-agent experiment remains a POC, and the Herdr journey requires no
plugin. Neither is a new cold-agent architecture or a plugin/adoption claim.
See [the full release notes](docs/releases/v0.5.11.md) for operational guidance
and the retained verification limits.
## [0.5.7] - 2026-09-01
**Status**: release-cut candidate. Final release-wide verification and public
publish remain release-manager gates.
### Summary For Installing Agents
- **Package versions**: root, CLI, daemon, and web UI are `0.5.7`; the terminal
TUI remains `0.1.0`.
- **Migration**: head advances `076` → `077_node_session_source`; normal daemon
startup applies the ordered migration list automatically.
- **Theme**: elastic topology lifecycle and coherent work delivery.
### Elastic topology lifecycle
- Create a rig without hand-authored YAML; grow existing or new pods in single
or batch operations; and preserve faithful spec show/export, including a
member's declared agent-image source.
- Refuse unsafe shrink while work is active. Explicit drain fallback preserves
queue identity and state, checks that the destination session is live, and
removes only the intended target; ordinary cleanup stays direct.
### Coherent work delivery
- Compose product-journey SDLC context across world, project, mission, and
slice layers, with useful non-overwriting scaffold defaults.
- Accept optional project YAML enrichment with explicit warnings and default
fallback, load Markdown by section address, and support explicit project
selection, switching, root isolation, and ambiguity refusal.
- Project the complete catalog of public plugin-only skills together with the
helper files their instructions reference.
### Deferred and post-release
- Restore honesty is deferred to `0.5.8`.
- Cleaned parent-host two-project dogfood runs after release.
See [`docs/releases/v0.5.7.md`](docs/releases/v0.5.7.md) for the full notes.
## [0.5.6] - 2026-08-30
**Status**: shipped; **"world-building as context engineering."** OpenRig 0.5.6 ships the context system's next layer, makes long-running agents durable, and makes messages to humans trustworthy. Three themes: the context a seat runs on is structured, tagged, and verifiable; an agent seat can now outlive its own context window without a human managing the transition; and anything routed to a human carries proof of delivery. **v0.5.6 contains v0.5.5 in full.**
### Summary For Installing Agents
- **Package version**: bumps from `0.5.5`.
- **Migrations**: THREE new (apply automatically on daemon start; no manual steps). Head advances `073` → `076`:
- `074_context_usage_watchdog.ts`
- `075_context_usage_watchdog_generation.ts`
- `076_owner_notification_levels.ts`
- **Node engines**: unchanged.
- **API surface**: existing commands remain; additive verbs and configuration below.
- New verbs: `rig view show execution`, `rig scope resolve-notes`.
- New watchdog condition: `usage-threshold` (`rig watchdog`).
- New continuity policy: `compaction_strategy` in the AgentSpec (four modes) with a declared `mechanic`.
- New reference docs at stable paths: `docs/reference/sdlc-conventions.md`, `docs/reference/mission-install.md`, `docs/reference/lore-routing.md`, `docs/reference/planning-dial.md`, `docs/reference/wave-sdlc.md`, `docs/reference/release-boundary.md`, `docs/reference/product-management-pass.md`.
- **Context packs**: 41 ship (up from 39).
- **Delivery defaults**: deliberately wide open (post at `NOTICE` and above, interrupt at `ALERT` and above). Tune down from measured volume with the two dials rather than pre-filtering.
- **Continuity modes**: opt-in per seat; unconfigured seats keep today's behavior exactly.
### The context system: structured, tagged, verifiable
- **The mission install.** A documented five-layer convention (`docs/reference/mission-install.md`) for installing project context into a seat: the pieces, their arrangement, the seat's own position, the current contract, and a derive-your-own-delta step that proves the install was consumed rather than skimmed.
- **Lore routing.** What a seat learns locally routes into addressable, rig-local lore packs (`docs/reference/lore-routing.md`) — carried with a maturity stage from birth, and structurally excluded from anything that ships.
- **Taxonomy everywhere.** Every pack entering `rig context` carries its class — WORLD / LORE / SKILLS / MISSION — surfaced in listings, with a loud refusal for untagged packs.
- **A public world pack with checkable claims.** The public onboarding pack is authored as atoms whose claims each carry a check that can fail, with the verifier shipped inside the pack — public content that cannot rot silently.
- **A complete, self-retrievable skill index.** The skill router's index now covers the full shipped set, is retrievable through the routes it teaches, and carries a recurrence test so coverage gaps stay found.
- **A substance gate for what ships.** A whole-surface review pass over every packaged file (bundle and npm surfaces included) runs as a named release step; this release shipped with all 5,316 packaged files passing.
### Continuity: seats that outlive their context windows
- **Continuity policies in the agent spec.** `compaction_strategy` is now live configuration with four modes: `default-compaction`, `managed-compaction`, `handover`, and `apprentice-handover`. Modes apply to Claude seats (positive runtime matching — Codex seats keep their native behavior) and resolve through the same spec-default < profile < member precedence as the rest of the agent spec.
- **The apprentice handover, automated.** A seat declaring `apprentice-handover` gets two armed triggers: near its context threshold, the system creates a fresh successor seat and runs its onboarding install; nearer the wall, it mints an owned handover work item for the seat that executes the swap. The executing seat is declared configuration (`mechanic:` on the policy) — never inferred from topology, and arming refuses loudly if it is missing.
- **Managed compaction with honest restores.** Seats in `managed-compaction` mode get a pre-compaction prep nudge (deposit your state before the window closes) and, after a configured restore, a durable receipt recording how much usable context the restore actually recovered — so a restore that silently refills the window instead of recovering width is visible.
- **Context-usage triggers as a primitive.** `rig watchdog` gains a usage-threshold condition: fire any command when a seat's context usage crosses a calibrated per-seat threshold. The continuity modes are built on it; your own automation can be too.
### The human layer: delivery you can prove
- **No more silent fall-through.** One destination resolver classifies every dispatch: terminal-bound seats use the terminal; registered humans and virtual seats route through the gateway; nothing falls through to a terminal lookup that cannot succeed. Unregistered external addresses get a structured, teaching refusal.
- **Delivery receipts on the work item.** Every gateway post writes its receipt atomically on the queue row: `posted` (with the message timestamp and channel), `transport-failed` (with the error), or — derivably — `never-posted`. `rig queue undelivered` now consults that ledger first and returns only genuine failures. "Did this reach them?" is one read.
- **Notification levels, one vocabulary.** Queue transitions classify as `RECORD` < `NOTICE` < `ALERT` at write time — a human-required decision is `ALERT`; system ceremony events are `NOTICE`; the rest is durable record. Two independent dials configure the minimum level that posts and the minimum level that interrupts.
- **A delivery-rules engine.** Stored per-human preferences and availability decide how each message class is delivered. An `off` preference suppresses interruption — never the durable record, the post, or the receipt. Away windows defer routine traffic into digests (4-hour and daily), with exactly-once delivery by construction: durable episode identities, retry until a real receipt, and structurally non-overlapping digests.
- **Files dropped in Slack become work.** A file or image upload lands as a work item with the file transferred to local storage and referenced by path — authenticated download, size-bounded, per-file failures named, and no URL or token ever written to a row. A park blocked on a registered human automatically enters the alert path and posts exactly once per episode.
### Operational honesty
- A rate-limited seat gets exactly one timed wake at its stated reset.
- A seat holding claimed live work while sitting idle gets woken by the system, consuming the arbitrated activity oracle — with exactly one wake per park episode.
- Refocus state keys to the current occupant of a seat; a fresh occupant never inherits a predecessor's baselines.
- `rig ps` counts claimed in-progress work as assigned work.
- Sends refuse empty bodies; unknown daemon API routes return JSON 404s.
- Stuck-sweep findings stop re-minting after closure; cross-host custody verification completes durably instead of refreshing forever.
- Blocker actuation is unified: one helper, cross-host closure honest, delivery after the transaction commits.
- Rig expansion and serialization preserve what they were given: `session_source` versions, `services`, and member overrides survive round-trips; fork forwards model, role, restore policy, and label.
- `rig view show execution` derives who is building what, what is next, and each item's completion rung (locked / built / reviewed / folded / adopted) at read time — unknowns render as INDETERMINATE, never as idle or done.
### Process and reference
- The SDLC conventions document is now a component menu: choose per mission from the simple flow, the wave model, or the rigorous overlay, with planning rigor as a dial. Four full references ship at `docs/reference/`: `planning-dial.md`, `wave-sdlc.md`, `release-boundary.md`, and `product-management-pass.md`, joined by `mission-install.md` and `lore-routing.md`.
---
## [0.5.5] - 2026-08-27
**Status**: shipped; **"the ambient-attention release"** — the fleet notices, retries, escalates, diagnoses, and onboards on its own so you can stop hand-babysitting the work in flight. **v0.5.5 contains v0.5.4 in full** — one reconciled lineage.
### Summary For Installing Agents
- **Package version**: bumps from `0.5.4`.
- **Migrations**: TWO new. Head advances `071` → `073`:
- `072_thread_seat_map.ts`
- `073_queue_transition_wakes.ts`
- **Node engines**: unchanged.
- **API surface**: existing commands remain. New verbs: `rig parked [seat]`, `rig seat handover --source fork:|rebuild` (execute path, was dry-run), `queue block --on <blocker>` (park-with-wake), `rig gateway human` fragment-lifecycle verbs, `rig view show escalations`. `rig ps` gains a typed `ACTIVITY` column.
### Headline
**Stop babysitting the work.** `rig parked <seat>` diagnoses "is anyone silently stuck" with confidence and teaching inline; the standing stuck-sweep runs without you and routes findings. Baton wakes retry, aggregate, and escalate on their own — hand off the row and stop chasing nudges. Parks carry their own wake and read HEALTHY. `rig ps` gains a typed ACTIVITY column from one oracle. The Slack human layer is live end-to-end, and fresh installs onboard themselves.
### What you can now do
#### Diagnose silent-stuck without capture arithmetic
`rig parked [seat]` answers "is anyone silently stuck" as a derived diagnosis — activity × open obligations, with per-input confidence and remedy taught inline. **Reach for it when:** a seat looks idle and you suspect dropped work; before any `claimedAt` arithmetic or pane capture.
#### Hand off work; stop babysitting nudges
Failed baton wakes retry, aggregate, and escalate on their own (transitions ARE the ladder state, restart-safe; per-destination aggregation; rungs deliver-and-advance; unconfirmed-with-no-pickup escalates without re-send). **Reach for it when:** you hand off work — the row is enough. Watch `rig view show escalations` for the aggregates.
#### Consume the standing stuck sweep instead of running it
Overdue + undelivered become routed findings with derived evidence inline. **Reach for it when:** you used to hand-run `queue overdue` / `undelivered` on a timer — stop; consume its findings instead.
**Caveat (5.6 backlog):** cross-host successor visibility is a proven false-positive class; treat cross-host-lineage findings as unverified until the 5.6 detector fix. Contained fleet-wide one-per-condition; delta teaches the caveat.
#### Park a row on a real blocker without waking the attention machinery
`queue block --on <blocker>` records the wake; held-with-live-wake is not flagged; auto-unparked owners get an honest wake. **Reach for it when:** imminent-but-blocked work; for not-imminent work use the workspace instead (the queue is a conveyor).
#### Read seat activity from one oracle
`rig ps` carries a true `ACTIVITY` column — typed taxonomy (working / idle-at-prompt / needs-input-as-count+reason / unknown) with evidence ladder + visible rung degradation, seat-keyed across swaps. **Reach for it when:** any "is it thinking or stuck" question — the oracle beats capture; the TUI reads the same truth.
#### Execute seat handover, not just plan it
`rig seat handover --source fork:` (carries live context) or `rebuild` (primes from the durable chain and names its priming artifacts). Mid-swap failures record honestly. **Reach for it when:** replacing an occupant — no more dry-run-only planning surface.
#### Reach human decision owners directly through Slack
Gateway running, thread-per-seat, exactly-once inbound reconciliation, escalation loudness (mention) distinct from routine. Humans are addressable members; `rig gateway human` has full fragment-lifecycle verbs. **Reach for it when:** anything must reach a human decision owner — an escalation-class send arrives loud on their phone.
#### Onboard fresh installs without hand-walking
Fresh installs get focused onboarding packs by default (config off; existing rigs untouched). **Reach for it when:** standing up a new rig or seat — stop hand-walking the eight pieces.
#### Trust refocus to fire only when it should
Refocus fires at a context threshold or on demand — NEVER at session start (post-compaction both runtimes; usage threshold Claude-only). **Reach for it when:** a long session loses the plot — trigger it. A fresh seat getting a refocus is now a bug to file, not noise to ignore.
#### Author scopes with the one-file convention
`scope mission create` → `NOTES.md` + intent-bearing `SPEC`; `slice create` → `SPEC`-only (no IMPLEMENTATION-PRD) + acceptance `PROGRESS`; release missions get a capability-delta scaffold. `repair` is ADDITIVE — stamps append, never rewrite author frontmatter. **Reach for it when:** creating any scope node — stamp verification is a mechanical strip again.
#### Bind daemons safely from any managed environment
Daemon bind intent has provenance: an inherited `OPENRIG_HOST` never selects single-bind; adoption gates test ALL required listeners. **Reach for it when:** daemon maintenance from any managed environment — no more `env -u` ceremony.
### What to STOP doing (each was correct under 0.5.4 and is wrong now)
1. **STOP the per-lock author-reconstruction protocol.** `scope repair/approve` used to rewrite author frontmatter; now stamps APPEND — strip-reconstruction is mechanical; the protocol is retired.
2. **STOP diagnosing parks by `claimedAt` arithmetic + capture.** `rig parked` derives the diagnosis with confidence and teaching; capture is a fallback glance, not the method.
3. **STOP storing deferred work as queue rows.** The queue is a conveyor for imminent sequential work; deferred work lives in the mission workspace and re-mints when due.
4. **STOP authoring `IMPLEMENTATION-PRD.md` and `MISSION_NOTES.md` on new work.** `SPEC.md` is the one spec file; `NOTES.md` is the chain name; locks bind SPEC-only.
5. **STOP routing contact through an orchestrator by default.** Any agent escalates directly; orchestrators/PMs also send judgment-worthy updates.
### Landed, not yet drivable (recorded so nobody reaches for it)
- Delivery preferences + availability (stored, validated) — no rules engine consumes them yet (0.5.6 slice 01).
- S01 operator rung — human-layer connector delivery leg lands in 0.5.6 (S11 territory). Escalation view + daemon health carry the floor now.
- `@external` addresses on the direct send path fall through to tmux (routing fix in 0.5.6 wave-2). Reach humans via the gateway path (queue/escalation), not raw `rig send` to `@external`.
- Multi-human topology — single-human ships as an honesty marker; multi-human is 0.5.7.
### Known Limitations (0.5.6 backlog)
- **Cross-host successor visibility false positives** in the stuck-sweep detector — 39 active findings at census (20 proven FP, 19 unverified); contained fleet-wide; fix lands in 0.5.6 wave 1.
- **`@external` direct-send routing** — 0.5.6 wave-2 routing fix.
- **Delivery preferences → rules engine** — 0.5.6 slice 01.
---
## [0.5.4] - 2026-08-26
**Status**: release candidate; **"the honesty release"** - commands distinguish absence, uncertainty, staging, and completed effects instead of collapsing them into reassuring output. **v0.5.4 contains v0.5.3 in full** - one reconciled lineage, no product divergence.
### Summary For Installing Agents
- **Package version**: bumps from `0.5.3`.
- **Migrations**: none in this release. Migration head stays at 071 (matching 0.5.3).
- **Node engines**: unchanged.
- **API surface**: existing commands remain; new seat lifecycle verbs are `rig seat set-model`, `rig seat stop`, and `rig seat clean`.
### Headline
**Trust what the command says, including when the answer is uncertain.** Transport probes now classify found, absent, and indeterminate outcomes through one resolution path. Delivery verification distinguishes text staged in a prompt from text consumed by an agent. Queue, refocus, preview, import, and lifecycle surfaces preserve the state and refusal that actually occurred.
### What you can now do
#### Resolve transport failures without inventing a missing seat
Send, capture, nudge, and walk use one classified session probe. A transport blip is reported as indeterminate rather than `Session not found`, while genuine absence remains explicit. Recovery output names what was checked and what action is available.
#### Tell staged delivery from consumed delivery
`rig send --verify` classifies the pane effect instead of equating typed text with a completed turn. Single-recipient, fan-out, and JSON results derive from the same outcome model, so a staged prompt is not reported as consumed.
#### Deliver unknown-sender messages honestly
An unattributable message is delivered with an unknown-sender notice instead of being refused. Where a reply path exists, the sender is told that attribution was unavailable and should follow up with a signed message.
#### Manage one seat through supported lifecycle verbs
`rig seat set-model`, `rig seat stop`, and `rig seat clean` provide audited, single-seat operations. Stop and clean refuse when any affected session is live or indeterminate; set-model is proven against a real managed successor rather than only stored configuration.
#### Trust state-preserving queue and refocus behavior
Adding a queue note no longer changes terminal state. Envless owned queue listing refuses instead of silently changing scope. Refocus keeps due state until delivery is visible. Bounded previews say they are bounded.
#### Refuse duplicate running rig names at every import path
Create and import routes now share one running-name guard. A duplicate running name returns a teaching `409 rig_name_running` refusal instead of spending resources or degrading into a generic server error.
### Stability and authority fixes
- Startup boundaries are hermetic across scratch/test environments.
- The bundled `openrig-core` plugin has an explicit advancing authority, so an older runtime cannot silently overwrite newer installed guidance.
- Stale lab refocus registrations are retired.
- CLI and daemon refusal codes survive route and import boundaries without fallback noise.
### Known Limitations (0.5.5 backlog)
- **Writer-layer disk cap** - the one-time VM cleanup recovered headroom, but the permanent cap at the layer that writes snapshots is unfinished.
- **Repair/approve frontmatter rewrite** - the known rewrite defect remains outside this release.
- **Observation pipeline performance** - shared pane/process observation remains measurement-gated; 0.5.4 does not claim the broader polling/census redesign.
---
## [0.5.3] - 2026-08-26
**Status**: shipped; **"the context release"** — pull exact context by address instead of reading files. **v0.5.3 contains v0.5.2 in full** — one lineage, no divergence.
### Summary For Installing Agents
- **Package version**: bumps from `0.5.2`.
- **Migrations**: none in this release. Migration head stays at 071 (matching 0.5.2). Nothing to apply on a v0.5.2→v0.5.3 upgrade.
- **Node engines**: unchanged.
- **API surface**: preserved from v0.5.2; no CLI-command or flag removals or renames.
- **New CLI verbs**: `rig context get`, `rig context list`, `rig context profile`, `rig context recap-write`.
### Headline
**Pull exact context by address instead of reading files.** `rig context get <pack-ref>/<file>#<H2-slug>[/<H3-slug>]` returns the exact span bytes of one section. The `openrig-skills` router teaches the three-step: ask → ref → load. Refocus resolves refs automatically. Handovers write a durable, superseded-chain seat recap. Composition is by reference, so there is no second copy to drift.
### What you can now do
#### Pull exact context by address, not by reading files
`rig context get <pack-ref>/<file>#<H2-slug>[/<H3-slug>]` returns the exact span bytes of one section. The `/<file>` component is required even in a one-file pack. A bogus slug fails loud and lists the addressable sections. `rig context list` shows every shipped entry's ref and name; `rig context show` inspects one. Refs are canonical full paths (e.g. `skills/<namespace>/<name>`). Cross-source access (`seat:` / `mission:` atoms) is an authoring affordance: declared in pack manifests and granted via `rig context profile --rig/--seat/--mission` during composition — not an ad-hoc argument to `get`.
**Reach for it when:** you need one section of a shipped skill or library entry and would previously have read or copied the whole file.
#### Discover context by asking, not by guessing paths
The `openrig-skills` router teaches the three-step: ask → ref → load. Run `rig context list` for refs, match your moment to an entry's `when`, load by ref.
**Reach for it when:** you don't know which skill or context applies, or nothing is projected after a cold boot.
#### Write a durable seat recap
`rig context recap-write` maintains a seat-scoped `RECAP.md` beside `LEARNED` with a collision-safe superseded chain; restore packets carry an authored-recap pointer.
**Reach for it when:** handover, compaction boundary, or any moment your successor would otherwise re-derive your state from scrollback.
#### Trust refocus to resolve refs
The refocus hook resolves library refs automatically (ref beats file), fails loud but non-blocking, and delivers pointers — the shipped channel for pointing running seats at new content.
#### Fetch bounded domain expertise on demand
`rig context get` serves expertise packs by path-like ref, e.g. `skills/process/context-engineering`. The old `context-pack:` colon form was removed and the CLI says so.
**Caveat that pack:** it is titled "Traditional Context Engineering — 2024-2025 Snapshot" for a reason — provisional, historical, non-normative by the project owner's ruling. Current OpenRig skills, explicit user rulings, and measured practice outrank it on any conflict.
### What to STOP doing (each was correct under 0.5.2 and is wrong now)
1. **STOP copying library content into seat or mission files to compose context.** Compose by ref — addressing means there is no second copy to drift. Nothing catches the copy automatically; it is still a defect by rule.
2. **STOP treating a `MODEL DIVERGENCE` proclamation as alias noise.** The canonicalize-then-compare fix ended the pinned=fable / effective=claude-fable-5 false-positive class, and the monitor now isolates per-seat throws. A proclamation on this runtime is a real escalation — act on it.
3. **STOP confusing explicit-path requirements with permission.** The guards described in this release demand explicit paths (e.g. `OPENRIG_SKILL_CANON_ROOT`); that mechanism does not grant authorization. **Historical guidance correction (2026-09-10):** the earlier approval statement was project-specific, not a universal rule. Current project and mission authority determines whether local commits, apply operations, pushes or PRs require approval.
4. **STOP hand-placing evidence or trusting a placeholder PRD as the proof contract.** `rig proof add` pairs evidence to the contract, and a pristine scaffold PRD can never silently become that contract: the authored SPEC serves with a named advisory (`contractSource` in the echo tells you which source bound).
### Landed but not yet drivable (recorded so nobody reaches for it)
Eval CASES ship (14 selection/loading cases), but no runnable grading pass exists today. `--provider rig` is the live proof-contract door and is NOT YET DRIVEN — it says so itself and redirects to fake (the wiring is exactly the convergence-test driver work in flight). `--provider fake` replays only transcripts supplied via `--transcripts <map.json>` and errors every case without one. When the live door is driven, the next release promotes this to a capability.
### Known Limitations (5.4 backlog)
- **Reply-hint self-qualify** — still open from 5.2; reply-hints on some cross-host messages address themselves via machine-ID rather than registered host name; substitute the registered host name from `rig host list`. Fix in 5.4.
- **Eval grading pass** — CASES ship; no runnable grading pass yet. Driven `--provider rig` lands in 5.4.
- **`rig send --verify` does not detect staged-unsent** for multi-line sends toward Claude seats — carried from 5.2 backlog.
- **Claude transcripts thin under fullscreen upsell** — carried from 5.2 backlog.
- Plus internal debt items ledgered for maintenance.
---
## [0.5.2] - 2026-08-22
**Status**: shipped; test system exercises for real, crash-cart fleet-restore conductor, reliability fixes shipped through the test system. **v0.5.2 contains v0.5.1 in full** — one lineage, no divergence.
### Summary For Installing Agents
- **Package version**: bumps from `0.5.1`.
- **Migrations**: none in this release. v0.5.1 shipped migrations 068-071 (068 CREATE + 071 DROP-IF-EXISTS pair, net no-op for any v0.5.0→v0.5.1 upgrader); v0.5.2 stays at 071.
- **Node engines**: unchanged.
- **API surface**: preserved from v0.5.1; no CLI-command or flag removals or renames.
- **New bundled skills**: none (this release is crash-cart + reliability focus).
### Headline
**A test system that catches real bugs on its own product, plus a crash-cart fleet-restore conductor.** The reliability fixes in this release were found and validated by scenarios that drive the real product through the same commands a person uses. Type bare `rig` at a dead daemon and you now reach a truthful cockpit; one Enter restores the fleet kernel-first with surviving tmux panes adopted.
### Crash-cart fleet-restore conductor
Type bare `rig` at a dead daemon and reach a truthful cockpit — no cryptic error, no silent failure. One Enter restores the fleet kernel-first, adopts any surviving tmux panes (never clobbers them), and presents an exact per-seat remediation walkable in one keystroke. Cancel is honest. Destructive restore is only offered when adoption cannot succeed. Ten build rounds, four independent gates, non-author QA door test.
### Daemon event-loop no longer hangs under load
A class of daemon event-loop freeze under load is fixed and measured. If your daemon was going unresponsive under fleet-wide activity, that's this class.
### Unattended Claude seat handover
Outgoing Claude seat writes a recap and submits its packet automatically at handover (compaction, session boundary, explicit handover); incoming seat picks up with the recap in hand rather than a cold start. Door-proven end-to-end.
### `rig policy` honest under adversarial input
Malformed or hostile policy input now produces honest structured errors rather than silent failure or unexpected behaviour. Three review rounds.
### Cross-host messages carry their machine of origin
Messages sent across hosts now include the machine they came from, so multi-host coordination reads correctly at the receiving end. Enforced end-to-end via door tests.
### Codex model-config drift detector
A new detector for a class of Codex model-config drift that would previously go unnoticed. Caught real cases on landing.
### Test-integrity
- Test runner refuses to run against a dirty tree — a green run means green source.
- Hermetic test roots.
- Flaky fixtures isolated.
### Governance in code
- Plan locks are explicit (chosen when you ask for one), not inherited from ambient state.
- Wake is opt-in.
### Container tar-file hang eliminated at the seam
A class of container tar-file hang is dead by construction — not worked around, but the seam that made it possible is closed.
### Known Limitations (5.3 backlog)
- **Reply-hint self-qualify** — reply-hints on some cross-host messages address themselves via machine-ID rather than registered host name; using the reply-hint directly may fail with "no registered host X". Substitute the registered host name from `rig host list`. Fix in 5.3.
- **`rig queue handoff --summary`** warns yet the field is unsettable on its own output. Fix in 5.3.
- **`rig send --verify` does not detect staged-unsent** for multi-line sends toward Claude seats. Multi-line sends can stage silently.
- **Claude transcripts thin under fullscreen upsell** — upstream fullscreen upsell writes `"tui":"fullscreen"`; one accepted prompt can re-flip a fleet. Fix + pin + prevention in 5.3.
- **Test runners can silently skip on unbuilt trees** — a final-gate rule (build CLI + assert zero skips) is adopted for the release ceremony; product-side fix in 5.3.
- **`seat-handover` model-fidelity real-Codex end-to-end** fails identically on main (environment-vs-product distinction unrooted). Isolation fix in 5.3.
- Plus internal debt items ledgered for maintenance.
---
## [0.5.1] - 2026-08-12
**Status**: shipped; a test system for the product, plus reliability fixes shipped through it. **v0.5.1 contains v0.5.0 in full** — one lineage, no divergence.
### Summary For Installing Agents
- **Package version**: bumps from `0.5.0`.
- **Migrations**: additive only. Existing v0.5.0 databases upgrade by running `rig daemon start` on the new daemon.
- **Node engines**: unchanged.
- **API surface**: preserved from v0.5.0; no CLI-command or flag removals or renames.
- **New bundled skills**: none (this release is test-system + reliability focus).
### Headline
**A test system for the product, plus reliability fixes shipped through it.** Scenarios drive the real product through the same commands a person uses, so it can prove its own behaviour without a human checking by hand. The reliability fixes of this window ship through the test system — proving its value on its first cycle.
### Test system
- **Stub runtime for tests** — a fake agent runtime that scenarios can drive. Lets scenarios exercise the daemon end-to-end without needing a real Claude or Codex process.
- **Scenario format + runner** — a text-based scenario format and an executor that runs scenarios against the real product. Includes a real fix that surfaced from writing the runner: text-matching against a pane or transcript could never match its two intended surfaces; now it does.
- **Seed scenarios** — three seed scenarios that run green end-to-end against the real product. See Known Limits for the seed-scenario one that could not be built.
### Container test bed — scaffolding
- **Scaffolding for container mode** — argument builders, image-identity stamping, runbooks, and a unit suite. **The runner does not yet execute in containers** — this is scaffolding you will see in the tree but that no test currently drives. Container-mode execution is scoped forward.
### Reliability fixes shipped through the test system
- **Honest render for daemon transport failures** — the operator sees the actual failure rather than a silent no-op or generic error.
- **Queue-persistence** — two fixes ensuring queue records reflect what actually happened after they were written.
- **Cross-machine send regression** — found and fixed during verification. The test system doing its job: proving product behaviour across machines and catching a regression before release.
### Small features
- **Per-agent model from spec** — each agent's model comes from its spec rather than a fleet-wide default, so different agents can run on different providers or models.
- **Token usage tracked over time** — the daemon records token use so operators can see trends and predict when a seat is approaching a usage limit.
- **Machine-of-origin on messages** — messages carry the machine they came from, so multi-host coordination is legible.
### Other
- **UI timing hardening** — UI tests no longer fail on load-timing flakes.
- **Queue records tell the truth after they are written** — a class of wrote-it-and-it-did-not-stick issues closed.
- **Unreached-message visibility** — show messages that never reached an agent. On landing, this surfaced nine real handoffs that had been silently lost since 8 August.
### Known Limits
- **Container mode is scaffolded but not driven** — no test currently executes in a container. Scoped forward.
- **Seed scenarios: three of the intended set run green** — the tenth cannot be built without two capabilities that do not yet exist in the product (a scenario cannot mutate the running system's shape; the terminal interface exposes navigation state rather than fleet inventory). Both scoped forward.
---
## [0.5.0] - 2026-08-06
**Status**: shipped; mission control TUI + context library + permission-policy built-ins + provider usage observability + plan amendment + honest CLI + build discipline. **v0.5.0 includes everything from v0.4.8** — there is no divergence between the two releases.
@@ -288,7 +1071,7 @@ Building on the v0.4.8 permission-policy foundation, v0.5.0 ships the built-in p
- **New shipped starter: `rig up factory-rsi`** — the single-rig recursive-self-improvement factory MVP. One rig, seven seats (`plan-planner`, `build-implementer`, `check-qa`, `review-reviewer`, `dogfood-tester`, `release-manager`, `orch-lead`), running the new `factory-rsi` workflow. A launch-tier product starter (a `product-team` sibling), workspace-agnostic — point `--cwd <repo>` at whatever the loop should improve.
- **New builtin workflow: `factory-rsi`** — the inner loop `plan → implement → qa_check → review → release`, with `qa_check`/`review` `failed` → `implement` (bounded remediation), engine-routed — never an orchestrator relay. Dogfood is **decoupled** from this gated loop: the dogfood seat runs out-of-band against the **shipped** product and feeds its findings into the next plan (the RSI edge, ungated — no loop-stop in the MVP; the continuous out-of-band runtime mechanism is refined in a later release). The remediation loops are sanctioned only by the enforceable `loop_guards.max_hops`; a trip is an exception routed orchestrator-first (`exception_routing`), and `rig workflow resume` grants one more bounded window.
- **Recorded-state cycles**: the next plan's input is the *recorded* dogfood findings (`evidence_ref` / the packet trail), never a seat's chat memory — the RSI feedback is durable recorded state.
- **Publish stays a human act**: the release leg is two steps — `release_prep` (the release-manager PREPARES notes/docs/PR and records the evidence; un-gated, runs first) hands off to `release_signoff`, which holds the ship decision at a human gate (`gate.target: human@kernel`). Prepared artifacts exist before sign-off; no seat pushes, tags, publishes, or upgrades a host.
- **Publish stays a human act**: the release leg is two steps — `release_prep` (the release-manager PREPARES notes/docs/PR and records the evidence; un-gated, runs first) hands off to `release_signoff`, which holds the ship decision at the configured human gate target. Prepared artifacts exist before sign-off; no seat pushes, tags, publishes, or upgrades a host.
- **Rides the merged engine, no new machinery**: runs on the workflow engine + spec language + exception model as shipped, with the v0 hardcode seam (`target.rig: factory-rsi` + `preferred_targets` pin each role 1:1 to a seat) — no binding-layer dependency, no engine change. Runtime config: seats inherit their runtime's default model (no per-seat pin); plan/build/release/orch run on claude-code, and qa/review/dogfood run on codex for cross-runtime diversity against the builder.
- **No migrations. No breaking changes.**
@@ -572,7 +1355,7 @@ The v0.4.0 cascade-metadata hygiene findings (`missing_provenance` + `missing_ve
## [0.4.0] - 2026-06-20
**Status**: wrap-gate CLEAR; lifecycle push / npm publish / tag held for founder-auth.
**Status**: wrap-gate CLEAR; lifecycle push / npm publish / tag held for authorization from the project owner.
### Summary For Installing Agents
@@ -619,7 +1402,7 @@ Five read-commands flip from firehose-by-default to compact-by-default — close
### Known Limitations / Carry-Forward
- **Plugin-lineage drift in `openrig-core`** — the openrig-core plugin skill lineage is divergent/stale; full re-sync is OPR.0.4.1.4 (rides 0.4.1). Boot-path layers (canonical + hub cwd) verified current in wrap-gate AC-3 sweep. `rig skill audit` (slice 10) is the runtime mechanism for future drift detection.
- All earlier "PUSHED to 0.4.1" carry-forwards from the original wrap-gate were RESTORED to 0.4.0 during the wrap: slice 34 (`rig ps` current-rig default + `-A`/`--all-rigs` + `resumeTokenPresent`) landed — see Token-Efficient Defaults; slice 35 (`rig scope` stage/verified/reconcile) landed — see New Top-Level CLI Verbs. Real-terminal-related slices 38 + 39 also shipped via founder live-dogfood forward-fix authorization 2026-06-21. Nothing of substance carries forward to 0.4.1 from the original wrap-gate set.
- All earlier "PUSHED to 0.4.1" carry-forwards from the original wrap-gate were RESTORED to 0.4.0 during the wrap: slice 34 (`rig ps` current-rig default + `-A`/`--all-rigs` + `resumeTokenPresent`) landed — see Token-Efficient Defaults; slice 35 (`rig scope` stage/verified/reconcile) landed — see New Top-Level CLI Verbs. Real-terminal-related slices 38 + 39 also shipped via the project owner's live-dogfood forward-fix authorization on 2026-06-21. Nothing of substance carries forward to 0.4.1 from the original wrap-gate set.
### What To STOP Using
@@ -634,10 +1417,10 @@ Five read-commands flip from firehose-by-default to compact-by-default — close
- Wrap worktree clean on `8d55ea60`.
- CLI surfaces source-verified against the command modules at the release SHA.
- CLI→skill cascade sweep: canonical `openrig-work/skills/openrig-user/SKILL.md` → product mirror at `packages/daemon/specs/agents/shared/skills/core/openrig-user/SKILL.md` → hub cwd `.claude/skills/openrig-user/SKILL.md` + `.agents/skills/openrig-user/SKILL.md` — all byte-identical (md5 `e2aa9176`).
- The historical CLI→skill cascade sweep reported the internal canonical skill source → product mirror at `packages/daemon/specs/agents/shared/skills/core/openrig-user/SKILL.md` → hub cwd `.claude/skills/openrig-user/SKILL.md` + `.agents/skills/openrig-user/SKILL.md` as byte-identical (reported md5 `e2aa9176`). This records the historical report, not a fresh mirror check.
- cli-reference.md updated for 6 changed commands + new `rig skill` section; `last-verified-against-source` bumped to `8d55ea60`.
- Stale-pattern grep (`dumps everything` / `--notify required` / `rig down 404`) returned 0 hits across active SKILL.md locations.
- AC-6 self-check evidence: `substrate/shared-docs/openrig-work/missions/release-0.4.0/slices/36-release-durability-close/AC-6-wrap-gate-self-check-evidence.md`.
- The historical AC-6 self-check evidence was internal and is not a public reproducible receipt.
---
+75
View File
@@ -0,0 +1,75 @@
# Code of Conduct
## Our pledge
We as members, contributors, and maintainers pledge to make participation in OpenRig a
harassment-free experience for everyone, regardless of age, body size, visible or invisible
disability, ethnicity, sex characteristics, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive,
and healthy community.
## Our standards
Examples of behaviour that contributes to a positive environment:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from
the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behaviour:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their
explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement responsibilities
Project maintainers are responsible for clarifying and enforcing our standards of acceptable
behaviour and will take appropriate and fair corrective action in response to any behaviour that
they deem inappropriate, threatening, offensive, or harmful. Maintainers have the right and
responsibility to remove, edit, or reject comments, commits, code, issues, and other contributions
that are not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all project spaces (the repository, Discussions, issues and
pull requests) and when an individual is officially representing the project in public spaces.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported to the
maintainers at **[email protected]**. All complaints will be reviewed and investigated
promptly and fairly.
All maintainers are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement guidelines
Maintainers will follow these community impact guidelines in determining the consequences for any
action they deem in violation of this Code of Conduct:
1. **Correction.** A private, written warning, with clarity around the nature of the violation and
an explanation of why the behaviour was inappropriate. A public apology may be requested.
2. **Warning.** A warning with consequences for continued behaviour, including no interaction with
the people involved for a specified period.
3. **Temporary ban.** A temporary ban from any sort of interaction or public communication with
the community for a specified period.
4. **Permanent ban.** A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
version 2.1 by Coraline Ada Ehmke, available at
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html and licensed under
[Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/).
This adaptation names OpenRig and its maintainers and summarizes the enforcement guidelines. Community impact guidelines
were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
+110
View File
@@ -0,0 +1,110 @@
# Contributing to OpenRig
Thanks for being here. OpenRig is built in the open and by the thing it is: a rig of coding
agents and a small group of people. External pull requests and issues have started arriving
faster than we planned for, which is the best problem to have. This page says how to get a
change in with the least friction on both sides.
## Before you start
- **Bugs:** open an issue with the bug template. Include your OpenRig version (`rig --version`),
OS, Node version, which harnesses are involved (or none), and the relevant command and output.
Reports are public: remove credentials, private prompts, personal details and private paths
before posting. Share a small reproduction rather than a full transcript or instance dump.
- **Features and behaviour changes:** open an issue or a Discussion in *Ideas* first. A short
"what I am trying to do and what stops me" saves both of us a rewrite. Small, obvious fixes do
not need an issue.
- **Questions:** use [Discussions › Q&A](https://github.com/mvschwarz/openrig/discussions/categories/q-a),
not an issue.
## Before a security or integration PR
OpenRig connects your coding agents to each other: shared context, messaging, coordination and long-running seats.
It's designed for your own machine or a trusted private network, for you and people you trust, not for the open
internet. It doesn't try to act for you in the outside world; your agents already have tools for that.
**Security-related PRs.** If you've found something exploitable, report it privately first (see
[SECURITY.md](SECURITY.md)) rather than in a public PR. For hardening changes, tell us in the PR description:
- the scenario: how this goes wrong for someone using OpenRig as it's designed to be used, and the evidence you have
- who or what causes it, and how they reach the install
- what the change costs everyone else: a refusal, an extra step, a new setting
We look at the finding and the fix separately. We may agree with a finding and fix it differently, or decide the fix
costs more than it protects. If the answers are missing we'll ask once, and close the PR with thanks if they don't
come.
**Integrations with other tools or projects.** Open an issue or an Ideas Discussion and wait for a maintainer's yes on
scope before you build it; an issue on its own isn't a yes. Often the best home for an integration is your own
repository, and we're happy to link to it.
## Setting up
Node `^22 || ^24` and a working `tmux` are required. Then:
```bash
git clone https://github.com/mvschwarz/openrig.git
cd openrig
npm install
npm run build # all workspaces
npm test # repo checks + daemon, cli, tui test suites
npm run lint # typecheck every package
```
`npm test` builds the daemon and runs repository checks before the package suites. Read the
specific failure: `npm run mirror-skills` updates skill mirrors, and
`npm run generate-context-packs` updates generated packs. Use them when their source changed and
review the generated diff; neither command fixes every documentation failure. The UI unit-test
suite is advisory and separate: `npm run test:ui`.
For hands-on development, read the [check requirements](docs/reference/developing.md),
[worktree setup](docs/reference/worktree-builds.md), and
[machine changes](README.md#what-openrig-changes-on-your-machine). Use an isolated environment
for changes that start daemons or agents; `OPENRIG_HOME` alone does not isolate provider settings.
Permission configuration is an [explicit choice](docs/reference/getting-started.md#have-your-agent-configure-permissions).
A checked-out tree is not the installed daemon; restarting an installed daemon does not adopt
your working copy.
## Making the change
- One concern per pull request. Keep unrelated refactors separate; explain any refactor needed
for the fix.
- Keep the diff small enough to review in one sitting. If it is not, say why in the description.
- Add or update a test where the change is testable. Use focused deterministic tests where
possible. For terminal or provider behaviour, state what was exercised with the actual runtime
and what was simulated; a stub alone does not prove the native interaction works.
- Do not edit `CHANGELOG.md`. Maintainers write release notes at the tag.
- Do not bump versions.
- Match the surrounding style. `npm run lint` typechecks; it does not format code.
- Write commit messages in the form the log already uses: `fix(cli): …`, `feat(daemon): …`,
`docs(reference): …`, `harness: …`.
## The pull request
Fill in the template. The three things a reviewer needs are: what a user gets, how you verified
it, and anything you were unsure about. State the revision and relevant local changes you tested,
what you actually ran, and any checks you could not run. Redact private information from evidence.
Contributions follow the repository's [Apache-2.0 license](LICENSE). Preserve attribution and any
applicable license notices when adapting third-party material.
## What to expect from us
- We aim to acknowledge issues and pull requests within **one day**. An acknowledgement is not
a completed review or a merge decision.
- Our target for a first substantive review decision on an external PR is **seven days**. If it
takes longer, we explain what is pending on the PR. This is a target, not a guaranteed deadline.
- Labels you will see: `needs-repro` (we could not reproduce it yet; waiting on versions or steps),
`fixed-on-main` (merged, not yet on npm), `good first issue`, `help wanted`, `discussion`
(direction question; continues in Discussions).
Reviews here are done by people and by the project's own agents. An agent may ask the first
clarifying question or run the reproduction; a maintainer makes the merge decision.
## Where things live
- Repository reference: `docs/reference/`; user documentation: [openrig.dev/docs](https://openrig.dev/docs).
- Skills: `packages/daemon/specs/agents/shared/skills/` and plugin skills under
`packages/daemon/assets/plugins/`; static context-pack sources: `packages/daemon/context-packs-src/`.
Generated packs live in `packages/daemon/context-packs/` and are not hand-edited or committed.
- Releases: [GitHub Releases](https://github.com/mvschwarz/openrig/releases) and npm `@openrig/cli`.
+258 -47
View File
@@ -1,43 +1,162 @@
# OpenRig
[![npm version](https://img.shields.io/npm/v/@openrig/cli)](https://www.npmjs.com/package/@openrig/cli) [![npm downloads](https://img.shields.io/npm/dw/@openrig/cli)](https://www.npmjs.com/package/@openrig/cli) [![License: Apache 2.0](https://img.shields.io/github/license/mvschwarz/openrig)](LICENSE) [![GitHub stars](https://img.shields.io/github/stars/mvschwarz/openrig?style=social)](https://github.com/mvschwarz/openrig/stargazers)
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 you results and decisions that need your attention. Start with a repository and one useful change, then keep the team's work and context at the same addresses.
The OpenRig UI is experimental and in maintenance mode. It is not under active development; support is best-effort. The CLI is the primary supported interface. Contributions welcome.
**Guide:** [Getting started](docs/reference/getting-started.md) · **Stuck?** [Help](docs/reference/help.md) · **Questions:** [Q&A](https://github.com/mvschwarz/openrig/discussions/92) · **Updates and demos:** [@_feralmachine on X](https://x.com/_feralmachine)
## See it running
![The OpenRig TUI: the build rig as a graph, then as a table of seats with runtime, model, context and state, then one seat in detail (real recording, 10 seconds)](assets/readme/openrig-agents-working.gif)
**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.
```bash
npm install -g @openrig/cli
rig setup
rig up product-team
rig ui open
rig setup --dry-run
```
![OpenRig UI](https://openrig.dev/screenshots/remotion/hero-ui-workspace.png)
To install with Bun instead, run `bun add -g @openrig/cli`. OpenRig still runs on Node.js, so install Node.js 22 as well. Bun may block this package's postinstall script, in which case the Node.js and SQLite check described under [what OpenRig changes on your machine](#what-openrig-changes-on-your-machine) does not run at install time.
It runs locally: a daemon, a SQLite database, a CLI, and a dashboard. The agents are ordinary Claude Code and Codex sessions in tmux. `product-team` is the fuller starter, 7 seats, 4 Claude and 3 Codex, with an orchestration HA pair plus development and review pods.
Choose the working account you already have: **Claude Code, Codex, or both**. Reuse an explicit choice; no second subscription is required. `rig setup --dry-run` previews the broader setup, but applying `rig setup` checks both harnesses and cmux. It is optional for the [selected-provider path](docs/reference/getting-started.md#choose-your-providers).
Because 4 Claude seats run at once, single-plan users should expect provider throttling. For the light-footprint path, use `conveyor`, a 4-seat starter with 2 Claude and 2 Codex.
Before launching, your agent asks once: **“Allow your agents to run OpenRig commands without repeated permission prompts?” Yes — recommended / No — keep prompts.** This covers every `rig` command, including starting/stopping agents and configuration, at personal project scope unless you explicitly choose user-wide sessions. It is not global YOLO or permission to invent work. On Yes, the agent [adds and verifies native rules](docs/reference/getting-started.md#have-your-agent-configure-permissions); No or no answer leaves settings unchanged. An existing explicit choice is reused. Say “Undo the OpenRig command allowances added by this setup” to remove only its additions.
Check `tmux -V` and only your selected CLI/login: `claude --version` plus
`claude auth status`, or `codex --version` plus `codex login status`. If needed,
sign in once with `claude auth login` or `codex login`; do not install or log in
to an unused provider.
| Team | Starter | Models |
| --- | --- | --- |
| Two Codex agents | `first-project` | Both `gpt-6-astra` (unchanged) |
| Two Claude agents | `first-project-claude` | Configured native Claude default |
| Claude owner + Codex checker | `first-project-mixed` | Claude default + `gpt-6-astra` |
All three use the same owner/checker roles and task. Show the selected runtime,
configured model and command before launch; confirm the account supports the
model instead of silently falling back. The kernel starts automatically and
selects from available authenticated providers independently of these two project
agents. A missing unused provider is not a setup requirement.
```bash
# Preview the smaller starter
rig specs preview conveyor
# Boot the light-footprint path
rig up conveyor
cd /path/to/your/repository
starter=first-project # or first-project-claude or first-project-mixed
rig specs preview "$starter" --kind rig
rig up "$starter" --cwd . --plan
rig up "$starter" --cwd .
rig tui --shared
```
## First Run
The kernel provides separate operational support and the shared dashboard. To detach without stopping the dashboard, press Ctrl-b then d; `rig tui --shared` returns to that view. Plain `rig tui` opens an independent view. Closing a viewing terminal does not mean you should relaunch the team.
After `product-team` boots, `rig ps` shows the fleet map and `rig ps --nodes --rig product-team` shows the running pods and seats. When you're ready to shut the team down and bring it back, use the same rig name:
Check project-seat readiness with `rig ps --nodes --rig "$starter"` and resolve any authentication, trust or permission prompt before assigning work. Then give the owner one bounded outcome from your repository:
```bash
rig ps
rig down product-team
rig up product-team
rig send "dev-owner@$starter" 'Implement <one useful change>. Track the task in the queue and return its ID. Keep it local, verify the behavior, ask dev-check in this rig to check the exact candidate, and record the result and how I can try it.'
rig queue list --destination "dev-owner@$starter" --limit 1000
```
`rig ps` is a fleet glance. `rig down product-team` snapshots the team and stops it. `rig up product-team` brings it back by name from that snapshot.
Sending a message does not itself create a queue item; the owner records the task. Read the final artifact and the review of its exact candidate, then return to the same owner for the next change. [The guided first-use path](docs/reference/getting-started.md) covers readiness, a useful task, a reviewed result, Herdr/cmux terminals and recovery.
Not setting this up today? Get the next walkthrough and occasional OpenRig updates → https://openrig.dev/follow
## Community
- **Questions:** [Discussions › Q&A](https://github.com/mvschwarz/openrig/discussions/categories/q-a)
- **Bugs and feature requests:** [open an issue](https://github.com/mvschwarz/openrig/issues/new/choose)
- **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md) · [Code of Conduct](CODE_OF_CONDUCT.md) · [Security policy](SECURITY.md) · [Getting help](.github/SUPPORT.md)
- **Videos:** [youtube.com/@openrig](https://www.youtube.com/@openrig)
- **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.
- **Claude Code:** managed startup writes workspace trust and onboarding completion
to `~/.claude.json`. In the workspace, `.claude/settings.local.json` receives
the context collector's `statusLine` command and selected activity hooks;
helper scripts live under `.openrig/`. Selected settings/MCP resources can also
change that settings file and `.mcp.json`. The shared settings resource sets
`permissions.defaultMode` to `acceptEdits` and enables Exa/Context7 MCP entries;
selected MCP resources configure those external services. Built-in bootstrap
no longer writes a command allowlist to `~/.claude/settings.json` or removes
older allowances. The trust writer uses the daemon home, so a custom
`CLAUDE_CONFIG_DIR` is not a general relocation of these writes.
- **Codex:** writes the daemon's `CODEX_HOME/config.toml` (normally
`~/.codex/config.toml`). Startup enables hooks, adds the OpenRig activity relay
commands and pre-writes trust hashes for those commands. Seat startup adds
`trust_level = "trusted"` for the workspace; selected config resources can
add MCP settings. Recognized update notices can be skipped during launch,
recording the skipped version in Codex's cache; this is not an update install.
Activity relays send event type/subtype, seat/runtime identity, timestamps and
native session identity to the configured OpenRig daemon's `/api/activity/hooks`
endpoint, using its activity token. That payload excludes prompt text and tool
arguments. Claude's collector writes context/token usage, session/transcript-path
metadata and available rate-limit data to the instance's `state/context-usage`
and `state/provider-usage`. Provider and selected MCP connections have their own
data flows. Daemon plugin initialization also checks the OpenRig plugin release
endpoint on GitHub.
Managed launches supply `HOME`, `CODEX_HOME` and `OPENRIG_*` identity/connection
variables. Claude uses `--permission-mode acceptEdits` and defaults to the classic
renderer for terminal scrollback. Codex uses `-s workspace-write` unless a named
profile governs its sandbox; the default does not force an approval-policy flag.
Fresh Codex launches also add writable access to the workspace's `.git` and the
pod's shared queue-state directory with `--add-dir`; the shared root comes from
`OPENRIG_SHARED_DOCS_ROOT` or `~/.openrig/shared-docs`.
YOLO is **off by default**. An explicitly selected full-bypass policy selects
Claude's `--dangerously-skip-permissions` or Codex's
`-s danger-full-access -a never`. The legacy environment-only `OPENRIG_YOLO=1`
path still selects only Codex's sandbox; a resolved policy overrides that
environment setting.
Permission mode controls native execution permissions; work posture is separate
project guidance. Use `rig policy permissions list|show|current|apply` for rig
policy configuration (the four `rig policy` aliases remain compatible). Use
`rig seat set-permissions <seat> --mode <mode> --reason <text>` for an audited
future-launch choice: `floor`, `full_bypass`, or `inherit` to clear the seat
override. Additional Claude modes such as `auto` require support from the exact
managed Claude executable at the seat's working directory; selection and launch
each check it. Unsupported or changed contexts refuse without a fallback.
This does not relaunch the seat
or change its current native process, history, rules or hooks. `rig seat status`
separates the desired selection from the last launch arguments; neither proves
native enforcement. See the [permission guide](docs/reference/getting-started.md#per-seat-permission-mode).
Managed hook blocks target OpenRig's entries and retain unrelated hooks, but
trust entries, selected resource keys and Claude's existing status-line command
can be replaced. Some writers recover unreadable settings as empty objects;
this is not a complete preservation or rollback guarantee. Back up relevant
files before first use. Daemon/bootstrap writes are automatic and do not each
have an interactive preview; `rig setup --dry-run` does not preview every later
startup effect.
## What It Does
@@ -45,33 +164,37 @@ OpenRig is a multi-agent harness — it manages the system that coding agents fo
- **Define** topologies in YAML (RigSpec) with pods, edges, and continuity policies
- **Boot** everything with `rig up` — tmux sessions, harnesses, startup files, readiness checks
- **See** the topology in a live graph with explorer, node detail, and system log
- **See** rigs, pods, and seats in the TUI topology table and graph; inspect projects, specs, feeds, and instance health
- **Discover** existing Claude Code and Codex sessions in tmux and adopt them into a managed rig
- **Snapshot** the full topology on `rig down`, restore by name with `rig up <name>`
- **Snapshot** the topology with `rig down --snapshot`, restore by name with `rig up <name>`
- **Communicate** across agents with `rig send`, `rig broadcast`, and `rig chatroom`
- **Evolve** running topologies with `rig expand`, `rig shrink`, `rig launch`, `rig remove`
- **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))
- **Evolve** running topologies with `rig grow`, `rig shrink`, `rig launch`, `rig remove`
Every agent runs in a tmux session you can attach to, inspect, and work with directly.
## Starter Rigs
OpenRig's hero starter is `product-team`, the fuller product-development rig:
Use `first-project`, `first-project-claude`, or `first-project-mixed` for the
same focused first-use path on your selected providers. `product-team` is an optional
larger product-development example:
```bash
rig specs preview product-team
rig specs preview product-team --kind rig
rig up product-team
```
Use it when you want the week-one experience OpenRig is built around: an orchestrator HA pair, development work, review work, and enough moving pieces for the coordination layer to matter.
Use it when you want a larger product squad: two orchestrators, implementation, QA, design, and two independent reviewers.
For a smaller starter, use `conveyor`:
```bash
rig specs preview conveyor
rig specs preview conveyor --kind rig
rig up conveyor
```
`conveyor` is the smallest shippable software factory, one command. It keeps the footprint lower for single-plan users while still showing a real handoff path through intake, planning, build, and review.
`conveyor` is a four-seat starter mixing Claude Code and Codex. It shows a handoff path through intake, planning, build, and review; `first-project` remains the smaller two-seat starting point.
Also ships: `implementation-pair`, `adversarial-review`, `research-team`, and `secrets-manager` (HashiCorp Vault managed by a specialist agent).
@@ -83,10 +206,10 @@ rig specs ls
## How It Works
OpenRig is a local daemon + CLI + MCP server + React UI, built on tmux.
OpenRig is a local daemon + CLI + terminal UI + MCP server, built on tmux. The older React web UI remains in maintenance mode with best-effort support.
```
CLI / UI / MCP
CLI / TUI / MCP
|
Hono HTTP daemon
|
@@ -95,16 +218,33 @@ Hono HTTP daemon
SQLite + tmux + runtime adapters
```
- **CLI**: 40+ commands designed for both humans and agents. Every mutating command ends with what happened, current state, and next action.
- **UI**: Explorer sidebar, topology graph with pod grouping, node detail panel, system log, chatroom.
- **MCP**: 17 tools so agents can manage their own topology (`rig_up`, `rig_ps`, `rig_send`, `rig_chatroom_send`, etc.)
- **Runtimes**: Claude Code, Codex, and terminal nodes. Adapters for Pi and OpenCode in development.
- **CLI**: Commands for both humans and agents to launch teams, inspect state, send messages, track owned work, and manage context.
- **TUI**: Topology explorer, table and graph views, seat details, Specs, Projects, Terminals, Feed, and System. Navigate with the keyboard, mouse, or command bar.
- **MCP**: Tools so agents can manage their own topology (`rig_up`, `rig_ps`, `rig_send`, `rig_chatroom_send`, etc.)
- **Runtimes**: Native Claude Code and Codex sessions, terminal nodes, and a Pi adapter using an RPC runner inside a terminal pane.
## Terminal UI and Workspaces
The TUI shows the team's coordination state; herdr and cmux show the actual agent terminals alongside it. Use `rig tui commands` to list the TUI's command-bar navigation, or [try the interactive TUI tour](https://openrig.dev/tour/workspace).
![OpenRig TUI topology graph showing seven agent seats grouped into product, development, and QA pods](assets/ui/screenshots/tui-topology.png)
*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.
## Key Concepts
- **RigSpec**: Declarative multi-agent harness definition in YAML. Pods, members, edges, continuity policies, culture file.
- **AgentSpec**: Reusable agent blueprint with skills, guidance, hooks, profiles, and startup contracts.
- **Pod**: Bounded context group. Agents in a pod share memory and can maintain each other's context.
- **Seat**: A stable role and address in a rig, such as `dev-owner@first-project`. The conversation occupying it can change while its identity and authored context remain.
- **Pod**: A group of related seats with shared guidance and context. Each agent still has its own context window.
- **Discovery**: `rig discover` fingerprints existing tmux sessions. `rig adopt` brings them under management.
- **Snapshot/Restore**: `rig down --snapshot` captures full state. `rig up <name>` restores from latest snapshot. Restore reports per-node outcomes (resumed, fresh, or failed).
- **RigBundle**: Portable archive with vendored AgentSpecs and SHA-256 integrity. Share topologies across machines.
@@ -122,13 +262,83 @@ rig send vault-specialist@secrets-manager "Check Vault health and report status.
Requires Docker for service-backed rigs.
## Upgrading an existing instance
For an existing installation, follow the [upgrade procedure](skills/_canonical/core/openrig-upgrade/SKILL.md) and the [0.5.14 release notes](docs/releases/v0.5.14.md). Preserve live seats during the upgrade; `rig down` is not an upgrade step. Upgrading to 0.6.0 also requires Node.js 22 or 24: see [Moving off Node 20](#moving-off-node-20) and the [0.6.0 release notes](docs/releases/v0.6.0.md).
### Moving off Node 20
OpenRig 0.6.0 supports Node.js 22 and 24 only. Its SQLite binding
(better-sqlite3 13) requires Node 22 or newer. Node 20 is no longer supported;
the install check refuses it with an explanation.
If you run OpenRig on Node 20, switch Node first, then reinstall the CLI under
the new Node (a version manager keeps a separate global package set for each
Node):
```bash
nvm install 22 # or 24; fnm or your package manager work the same way
npm install -g @openrig/cli
rig --version
```
Your existing OpenRig data stays where it is. The daemon reopens the same
database under the new binding and applies any pending migrations in place.
Restart the daemon under the new Node by following the upgrade procedure above.
### Crossing the 0.5.9 layout boundary
The migration below still applies when upgrading from a pre-0.5.9 instance.
0.5.9 makes `$OPENRIG_HOME/context` the addressable context library, writes
Claude telemetry to `state/context-usage` (and provider telemetry to
`state/provider-usage`), and installs the default System World at
`context/system/system-world.yaml`. Existing instances cross this boundary by
an **Agent-Operated Migration** from the shipped `openrig-upgrade` skill. The
target runtime reads canonical-first with legacy-fallback while new writes use
the canonical roots; a custom context-library root stays stable during
activation. This is not a directory rename to do while an old collector writes.
```bash
# SKILL_DIR is the installed openrig-upgrade skill directory.
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --help
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --home "$OPENRIG_HOME"
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --home "$OPENRIG_HOME" --apply-state --preimage /safe/path/layout-0.5.9-before
# Activate the exact target runtime separately. After every bounded legacy tail is followed by newer paired samples at both new state roots:
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --home "$OPENRIG_HOME" --verify --preimage /safe/path/layout-0.5.9-before > /safe/path/layout-0.5.9-verify.json
# Run the separately invoked non-destructive finalizer only with that exact receipt:
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --home "$OPENRIG_HOME" --apply-library --preimage /safe/path/layout-0.5.9-before --verification /safe/path/layout-0.5.9-verify.json
# Restore only helper-owned preparation/finalizer effects if the observed upgrade must be reversed:
node "$SKILL_DIR/scripts/migrate-telemetry-state-0.5.9.mjs" --home "$OPENRIG_HOME" --rollback /safe/path/layout-0.5.9-before
```
`--help` prints the phase grammar without inventorying the instance. No phase
flag intentionally runs the read-only plan; unknown options fail nonzero before
plan or mutation.
Every phase emits JSON. Stop on any issue or incomplete receipt and follow its
`next` action; do not continue from copied legacy telemetry or retry a partial
mutation blindly. Preparation leaves legacy state and collector settings in
place. Verification accepts exact tail bytes only when that same seat has newer
paired context and provider samples under `state/`; finalization revalidates the
accepted tails, copies the library without overwrite, and switches config last.
The helper never removes the legacy telemetry or library. Retirement follows
separate stable runtime, writer, reader, and recovery proof. Daemon, database,
seat, plugin, and release lifecycle actions remain agent-owned.
## Requirements
- Node.js 20, 22, or 24 (even-numbered LTS releases; odd releases like 25 lack native addon prebuilds)
- Node.js 22 or 24 (the supported versions in this release). Node 20 is no
longer supported. Node 26 and other versions are untested. On a Mac with Apple
silicon, use Node.js 22: see the [compatibility history](docs/releases/v0.5.15.md#known-compatibility-limitation).
- tmux
- macOS or Linux. Native Windows is not supported yet, and WSL2 has not been tested
Optional:
- cmux for `Open CMUX` node surface controls
- herdr or cmux for terminal workspaces showing the agents together
- Docker for service-backed rigs and managed apps
## Setup and Troubleshooting
@@ -139,30 +349,31 @@ Optional:
Both commands support `--json` for agent-driven workflows.
Managed runtime boot (during `rig up`) may modify runtime config for core bootstrap and spec-selected runtime resources. `rig setup` discloses these paths so agents know what may be changed:
- global Claude: `~/.claude/settings.json` for minimal OpenRig command allowlisting
- global Claude state: `~/.claude.json` for managed workspace trust and onboarding completion
- project Claude: `.claude/settings.local.json` for context collector/activity hooks and selected `claude_settings_fragment` resources
- project Claude MCP: `.mcp.json` for selected `claude_mcp_fragment` resources
- global Codex: `~/.codex/config.toml` for workspace trust and selected `codex_config_fragment` resources
Before setup or managed launch, review [what OpenRig changes on your machine](#what-openrig-changes-on-your-machine), including provider trust, hooks and selected runtime resources.
Already-running adopted sessions may need restart before they pick up newly written runtime config.
**For agents:** Ask the user whether they want core setup (`rig setup`) or the fuller workstation path (`rig setup --full`) before choosing the invocation. Inspect the result with `--json` and use `rig doctor` to finish any remaining machine-specific issues.
**For agents:** First ask which available provider(s) the user wants, reusing an explicit choice. Recommend the working account they already have and follow the selected-provider guide above. Offer core setup (`rig setup`) or the fuller workstation path (`rig setup --full`) only when they want those additional tools. Inspect the result with `--json` and use `rig doctor` to finish any remaining machine-specific issues. When OpenRig itself misbehaves, read [docs/reference/help.md](docs/reference/help.md) (installed agents can run `rig context get help`; the same text is at [openrig.dev/help/agents](https://www.openrig.dev/help/agents)): it covers the next step, known problems, and how to reach the team if you're still stuck.
## Comparison with Claude Managed Agents
Anthropic shipped Claude Managed Agents — a cloud-hosted, Claude-only runtime at $0.08/session-hour. OpenRig is the local side: open source, cross-harness, runs on your machine, costs nothing.
OpenRig is open source and self-hosted, with Claude Code and Codex in the same team. You operate it on your own infrastructure; the selected providers' model usage costs still apply.
[Full comparison](https://openrig.dev/compare/claude-managed-agents)
## Links
- **Website**: [openrig.dev](https://openrig.dev)
- **Blog**: [Why I Built OpenRig](https://esoteric.run/blog/why-i-built-openrig)
- **Docs**: [openrig.dev/docs](https://openrig.dev/docs)
- **Docs**: [openrig.dev/docs](https://openrig.dev/docs) ([documentation index for agents](https://openrig.dev/llms.txt))
- **Blog**: [openrig.dev/blog](https://openrig.dev/blog) · [Why I Built OpenRig](https://esoteric.run/blog/why-i-built-openrig)
- **Open Specification**: [openrig.dev/specs](https://openrig.dev/specs)
- **Twitter**: [@_feralmachine](https://twitter.com/_feralmachine)
- **Videos**: [youtube.com/@openrig](https://www.youtube.com/@openrig)
- **X**: [@_feralmachine](https://twitter.com/_feralmachine)
- **Follow the project**: [openrig.dev/follow](https://openrig.dev/follow)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=mvschwarz/openrig&type=Date)](https://star-history.com/#mvschwarz/openrig&Date)
## License
+51
View File
@@ -0,0 +1,51 @@
# Security policy
## Supported versions
Only the latest published version on npm (`@openrig/cli`) is supported. Fixes are developed on
`main` and released after verification; a merged fix is not necessarily in the published package.
Include the affected version even when reporting a problem in an older installation.
## What OpenRig touches
OpenRig runs a local daemon, drives coding agents in tmux sessions on your machine, and writes
configuration for those harnesses (for example under `~/.claude` and `~/.codex`). The README
section [What OpenRig changes on your machine](README.md#what-openrig-changes-on-your-machine)
describes these effects and the trust/permission choices. Unexpected access, disclosure or
permission changes are useful reports; include what you expected and what you observed.
## Reporting a vulnerability
Please do not open a public issue for a vulnerability.
Open the repository's [Security page](https://github.com/mvschwarz/openrig/security). If
**Report a vulnerability** is available, use it to send a private report to the maintainers.
The reporter and authorized advisory collaborators can also see that report; it is not public.
Availability depends on the repository setting, not on this file being present.
If that option is unavailable, ask in [Discussions Q&A](https://github.com/mvschwarz/openrig/discussions/categories/q-a)
for a private security contact. Post only the request for a channel: do not include the
vulnerability details, reproduction, logs, credentials or affected private systems in public.
Wait for the private channel before sharing the report.
Include: the OpenRig version, the harnesses involved, the steps to reproduce, and what an attacker
could do. A proof of concept is welcome; a working exploit against a third party's machine is not.
## What to expect
- We will acknowledge the report and coordinate assessment and next steps privately.
- Assessment and fix timing depend on impact and reproduction; no fixed deadline is promised.
- We will agree on disclosure and credit with you. You can ask to remain anonymous.
## Scope notes
- OpenRig is designed for your own machine or a trusted private network, for you and people you trust, not for the
open internet. We assess reports by their practical impact on a user in that setup. Exposing an install to the open
internet isn't a supported deployment. OpenRig's own gateway integrations with outside services, such as Slack, and
anything that lets an untrusted party reach the daemon or an agent are in scope.
- OpenRig assumes the machine and the accounts it runs under are trusted by their owner. Reports
that require a hostile local user with the same account are still welcome but are unlikely to be
treated as high severity.
- A harness carrying out an intentionally authorized operation is not by itself evidence of an
OpenRig vulnerability. Reports of OpenRig bypassing a chosen permission, changing trust
unexpectedly, or routing data or actions to the wrong destination are in scope.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+30
View File
@@ -0,0 +1,30 @@
{
"url": "https://context7.com/mvschwarz/openrig",
"public_key": "pk_HcRwWEoFBsmU1mo5VIvWw",
"projectTitle": "OpenRig",
"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."
]
}
+7
View File
@@ -0,0 +1,7 @@
# Thin CI layer over the EXISTING installed-package testbed. No credentials,
# host mounts, provider binaries or additional package installation.
ARG TESTBED_IMAGE
FROM ${TESTBED_IMAGE}
COPY runner.mjs /opt/openrig-testbed/runner.mjs
COPY scenarios/ /opt/openrig-testbed/scenarios/
CMD ["node", "/opt/openrig-testbed/runner.mjs", "healthy"]
+4 -14
View File
@@ -1,14 +1,4 @@
# openrig-testbed base image — the digest-pinned LTS-slim Linux (plan §1: "digest-pinned, not
# tag-floating — byte-reproducible builds; the pin recorded in the image manifest").
#
# UNRESOLVED SLOT — resolve HOST-SIDE (locus ruling, the L0 build runbook). The VM seat has no
# container runtime and cannot pull/inspect a real digest, so this is a host-resolved input:
#
# docker pull debian:bookworm-slim
# docker inspect --format '{{index .RepoDigests 0}}' debian:bookworm-slim
#
# then replace this comment block with the single resulting reference, e.g.
# debian:bookworm-slim@sha256:<64-hex>
#
# Exactly one digest-pinned ref. Until resolved, the build verb REFUSES to build (no tag-floating
# base) — readBaseImage() loud-fails on this comment-only slot.
# Docker Hub library/debian bookworm-slim OCI index, resolved 2026-09-29.
# Registry response bytes SHA256 verified against Docker-Content-Digest.
# amd64 and arm64 are present; image execution is verified in CI, not by this pin.
debian:bookworm-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251
@@ -37,27 +37,27 @@ command -v docker >/dev/null || { echo "BLOCKER: docker not on PATH"; exit 1; }
The operator runs the runner in container-mode by supplying the step-3 `daemon` spawner + `imageId`.
This wiring composes only already-tested helpers (`spawnContainerDaemon`, `runScenarioFile`,
`withImageId`); the only new-at-runtime piece is the REAL `docker` invoker (the container analogue of
`runRig`'s `node <rigBin>`). Run it host-side with the tsx loader:
`withImageId`, `makeRealStagingDocker`). The REAL `docker` invoker is a COMMITTED helper, imported —
never re-inlined here. (The prior inline `execFile` invoker predated the `stdinFrom` tar-pipe
contract and ignored it, leaving the in-container `tar -xf -` an open never-fed stdin: the 08-12
seven-minute extraction hang, row 42576855. The committed helper feeds the pipe, checks BOTH exits,
and bounds every step with a timeout so a stall is a NAMED failure — proven hermetically in
`test/staging-docker-invoker.test.ts`. That proof covers the invoker only; live containment on a
real engine still rides THIS leg.) Run it host-side with the tsx loader:
```bash
node --import tsx - "$IMAGE" "$(jq -r .manifestDigest dist/testbed-image/manifest.json)" <<'RUN' | tee "${EVID}/L6-container.txt"
import { execFile } from "node:child_process";
import { resolve } from "node:path";
import { runScenarioFile } from "./packages/daemon/test/helpers/scenario-pipeline.ts";
import { spawnContainerDaemon } from "./packages/daemon/test/helpers/scenario-container.ts";
import { makeRealStagingDocker } from "./packages/daemon/test/helpers/staging-docker-invoker.ts";
const [image, imageId] = process.argv.slice(2);
const RIG_BIN = resolve("packages/cli/dist/bin-wrapper.js");
const SCENARIO = resolve("packages/daemon/test/fixtures/scenarios"); // pick one committed scenario file below
// The REAL docker invoker — mirrors runRig: never rejects, returns the exit code.
const docker = (args) => new Promise((res) => {
execFile("docker", args, { maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
const code = err && typeof err.code === "number" ? err.code : err ? 1 : 0;
res({ stdout: stdout ?? "", stderr: stderr ?? "", code });
});
});
// The committed real invoker: two-process tar pipe, dual-exit check, per-step timeout.
const docker = makeRealStagingDocker();
// Container-mode: inject the ScenarioDaemon-shaped container adapter + the manifest id.
const daemon = (scaffold /*, opts */) => spawnContainerDaemon(scaffold, { image, docker });
@@ -86,6 +86,13 @@ Routes import these; services are Hono-free:
- **`outbox-handler.ts`** — sender-side outbox: idempotent record, mark
delivered/failed, list. Emits no event-bus events (pure audit).
The standing detector in `queue-stuck-sweep.ts` creates findings through the
queue repository, with `evidenceRef: rig queue show <source-qitem-id>` pointing
to the underlying durable work row. This satisfies the existing human-route
evidence contract without changing destination resolution: an admitted finding
can still be unroutable. Repeated detections refresh the existing finding;
when the source condition resolves, the sweep closes its own finding.
## 3. The hot-potato closure contract (where queue closure is enforced)
`hot-potato-enforcer.ts` is the pure validator for the load-bearing API
@@ -128,8 +128,9 @@ artifacts in `<slice>/proof/`:
the muted outside-slice branch, never inlined), video by link + poster.
Exclusive-create through the allowlist-governed atomic write service;
re-invocation is an idempotent no-op; a failed render never un-approves.
The freeze moment also folds the mission brief spine (`brief-spine.ts`)
into `MISSION_BRIEF.md`, section-scoped, schema-order-preserving.
Legacy missions carrying `MISSION_BRIEF.md` may still receive the
section-scoped brief-spine fold; new scaffolds do not create that retired
web-UI-era file.
- **Media serving** for review evidence is ranged on BOTH asset route
families (iOS-Safari-class players require `206`): `GET /api/files/asset`
(`routes/files.ts` — single-range parse, `206`/`416`, `Accept-Ranges:
@@ -67,9 +67,10 @@ in-memory cache at daemon scope.
`ContextPackLibraryService.scan()` walks roots, parses each
`manifest.yaml`, replaces the in-memory index; collisions are
**last-wins** in discovery order (workspace > user_file > builtin).
Startup wires three roots: builtin (`../context-packs`, first), user-file
(`~/.openrig/context-packs`), and workspace-local
`<workspaceRoot>/.openrig/context-packs` when present + distinct. A pack is
Startup wires the builtin root (`../context-packs`, first), the configured
user-file root (`context.root`, default `$OPENRIG_HOME/context`), its
`system/` child as the canonical System World root, and the workspace-local
`<workspaceRoot>/.openrig/context-packs` root when present + distinct. A pack is
addressed by its path-like **ref** (e.g. `packs/compaction-restore`), which is
its unique identity; the opaque entry id is `context-pack:<ref>` (a UI routing
key). Colon-id `context-pack:<name>:<version>` addressing and the `/library/:id`
@@ -79,8 +80,8 @@ ordinary files only and rejects context-pack expansion; compose a durable ref
first, then use a dedicated delivery verb when delivery is intended.
> Source: `domain/context-packs/context-pack-library-service.ts:59-94`
> (scan; last-wins L79-81), `:31-33` (id); `startup.ts:481-502` (3-root
> IIFE; builtin `unshift` L497, workspace L491-493) @HEAD.
> (scan; last-wins L79-81), `:31-33` (id); `startup.ts`
> (`contextPackLibrary` construction) @HEAD.
The parser is pure (no fs; caller passes raw YAML). It rejects malformed
YAML, missing `name`/`version`, non-array `files`, per-file path traversal
@@ -97,7 +97,7 @@ present @HEAD.
2. Check mid-work state (unless `--force`) — re-confirmed
`session-transport.ts:136-141` (`findPatternEvidence(recentLines,
MID_WORK_PATTERNS)`); legacy mid-work check at `:666`.
3. Two-step tmux send: `send-keys -l` → ~200ms delay → `C-m`
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.
(`session-transport.ts:717` submits `C-m`).
4. Optional `--verify`: capture post-send pane, check message visibility
(`session-transport.ts:305` `verify?`; `:694` `if (opts?.verify)`).
+40 -10
View File
@@ -342,19 +342,44 @@ and the flow resumes from where it stopped.
settings key): target resolution = spec per-class → spec default →
host dynamic key → ORCHESTRATOR-FIRST (the declared orchestrator
role via the same `preferred_targets` pick step owners use) →
`human@host` never-lost fallback. THE TIER SPLIT: `human-gate` rides
registered-human selection (`workflow-human-destination.ts`). THE TIER SPLIT: `human-gate` rides
ONLY human-routed positions — an orchestrator-routed item carries
the ordinary tier so the shipped attention union (which matches on
tier regardless of destination) never leaks it into NEEDS-YOU. The
shipped attention predicate is untouched.
- **Class (a) born-in-txn**: the projector's failed-terminal branch
creates the item INSIDE the failing transaction — no window where
the instance is failed and no item exists; a gate-rejected routed
destination re-creates on `human@host` (never lost, never fails the
close). **Class (b) at detection**: the boot sweep and the keepalive
- **Class (a) born-in-txn**: serial failure and unhandled dependency-branch
failure share exception admission. The failed packet, dependency occurrence,
owning item and staged wake commit together, even while an unrelated frontier
keeps the instance active. Mapped remediation remains ordinary workflow work;
an unmapped or max-hop failure requires an owner. An agent destination rejected
as an unknown rig tries registered-human selection. Selection, admission,
or storage failure rolls back the close; it never commits a phantom
human alert. Other errors retain their original diagnosis. **Class (b) at detection**: the boot sweep and the keepalive
evaluation call the injected ensurer (`workflow-exception-
escalation.ts`) — occurrence-deduped against OPEN items by tag
query; the crash-surviving sweep re-creates a missed item.
query, scoped to the exact workflow, instance and packet; the crash-surviving
sweep re-creates a missed item. Normal projection, keepalive (including
healthy/terminal returns), and boot (including completed instances) reconcile
each overdue item's own packet. A resolved wait, completion or obsolete
frontier packet closes only that occurrence with a retained transition;
an overdue sibling stays open. Unknown provenance is retained. A later
overdue episode can create a fresh item even when it reuses the same packet.
- **Human selection and failure**: the existing `workspace.operator_seat_name`
setting selects a registered human; when unset, exactly one registered human
is required. No `human@host` alias is invented and no arbitrary choice is made
among several humans. The setting and registry are read at fallback time,
so a valid configured agent route needs no human registry. Missing, ambiguous,
invalid or unavailable selection produces `workflow_human_destination_unavailable`
(HTTP 409 for failed projection). Fix the registration/selection and retry.
Detection-time admission failures are logged by boot and included in the
keepalive's existing owner nudge and evaluation notes; later detection retries.
A registered destination still uses the gateway's ordinary delivery rules and
receipt ledger. Admission is not proof of posting or readership.
A capability inventory or detector instance-binding read failure propagates
instead of becoming a no-match:
failed projection returns HTTP 500 and rolls back its packet, instance and
history writes; overdue detection surfaces the read error without an exception
item. An evidenced no-match still uses registered-human selection.
- **`resume`** (`POST /api/workflow/:id/resume`, runtime `resume()`,
`rig workflow resume`): redrive semantics in ONE scribe transaction —
failed→active REBOUND to the recorded failed step; the owner is
@@ -362,7 +387,12 @@ and the flow resumes from where it stopped.
stale destination — resume is the one sanctioned re-resolution
point); `--decision` lands durably in the redrive packet; the
occurrence's open items close with provenance; the trail is
preserved, never rewritten. THE LIVELOCK RAIL (migration 051):
preserved, never rewritten. For a dependency graph, `--occurrence <failed-qitem-id>`
selects the episode (required with several unresolved failures). Its resolution,
redrive packet/wake and exact instance/occurrence exception closure share one
transaction; siblings remain intact. An identical occurrence/decision retry
returns the existing redrive; changed decision bytes conflict without mutation.
THE LIVELOCK RAIL (migration 051):
`hops_baseline` re-anchors the max_hops guard at resume so each
redrive gets exactly one bounded window; `resume_count` is the
recorded redrive fact; re-exceeding raises an honest NEW occurrence.
@@ -427,8 +457,8 @@ spec — the self-driving factory's binding substrate. Three seams:
no live resolution of future steps; a warming rig instantiates), and
resume. The WF-5 exception dial's orchestrator-role position resolves
capability-aware on the bound rig at both homes (in-txn class-(a) +
detection-time class-(b)), non-throwing with the human@host
never-lost fallback. Failures are loud-with-candidates: structured
detection-time class-(b)), then uses registered-human selection if no
agent resolves. Failures are loud-with-candidates: structured
per-candidate disqualifiers + a named zero-candidate message; never a
spawn, never auto-`add_member`, never a dead-seat route. Additive
`owner_resolution` trail evidence records `{mode, role, boundRig?,
@@ -217,37 +217,35 @@ walker.
> Source: `docs/as-built/cli-reference.md:931-941` @HEAD.
## 3. Default workspace scaffold (0.3.0 spine; 0.3.1 narrative layer)
## 3. Default project-workspace scaffold
`workspaceScaffoldDirs()` / `workspaceScaffoldFiles()` produce the
mission-aware default workspace (`~/.openrig/workspace/` or `--root`). The
same scaffold is used **idempotently by daemon startup** so a fresh install
has a browsable Project workspace before the operator discovers
`rig config init-workspace`. Canonical subdirs: `missions/`, `artifacts/`,
`evidence/`, `progress/`, `field-notes/`, `specs/`, `dogfood-evidence/`,
plus per-mission `missions/<id>/slices/<slice-id>` folders. Drops
`README.md` + `STEERING.md` (placeholder) + per-mission `README.md` /
`PROGRESS.md` + per-slice `README.md` / `PROGRESS.md` /
`IMPLEMENTATION-PRD.md`.
repo-ready default workspace (`~/.openrig/workspace/` or `--root`). The same
scaffold is used **idempotently by daemon startup**. It emits exactly two
directories (`missions/`, `exhaust/`) and four files (`SPEC.md`,
`project.yaml`, `workspace.yaml`, `.gitignore`). The catalog points its
single default project at `.`; `project.yaml` owns project intent and mission
discovery and exposes empty `install.context` / `install.skills` selectors.
The ignore file excludes `exhaust/` and local `.openrig/` projection state
while keeping authored project context versionable.
> Source: `domain/workspace/default-workspace-scaffold.ts:44-59`
> (`workspaceScaffoldDirs`), `:247-283` (`workspaceScaffoldFiles`),
> `WORKSPACE_README` `:82-94`, `STEERING_PLACEHOLDER` `:96-120`;
> CLI `packages/cli/src/commands/config-init-workspace.ts:1-16`
> (idempotent; `--dry-run`/`--force`; never deletes operator
> content L12-16); cli-reference workspace note "See `rig config
> init-workspace`" `:939` @HEAD.
The workspace owner is called by the shared instance initializer used by CLI
daemon start and direct daemon first start. The initializer is additive. It
never deletes or overwrites an existing file;
the retained `--force` spelling is a compatibility no-op for overwrite
behavior. CLI and daemon scaffolds are byte-parity pinned. Mission and slice
content is created explicitly through `rig scope`, not seeded by workspace
initialization. Instance context, System World, skill source, runtime state,
and the retired `artifacts/`, `evidence/`, `progress/`, `field-notes/`,
`dogfood-evidence/`, `README.md`, and `STEERING.md` entries are not emitted.
**0.3.1 narrative layer (slice 21 onboarding-conveyor).** The
`getting-started` mission's two slices ship rich teaching content from
`GETTING_STARTED_NARRATIVE` (the click-through-to-learn surface) including a
`timeline.md` the slice Story tab renders via slice-06's
`useSliceTimelineMarkdown` hook; other slices keep the boilerplate.
> Source: `domain/workspace/default-workspace-scaffold.ts:1-4`
> (slice-21 header), `:153-190` (narrative branch in `sliceReadme`),
> `:192-200` (`sliceTimeline`), `:270-279` (timeline emit); §0 proof:
> `getting-started-narrative.ts` [email protected] ⇒ **0.3.1** @HEAD.
> Source: `domain/workspace/default-workspace-scaffold.ts`
> (`workspaceScaffoldDirs`, `workspaceScaffoldFiles`); CLI
> `domain/instance-initialization.ts`; daemon `index.ts`; CLI
> `packages/cli/src/daemon-lifecycle.ts` and
> `packages/cli/src/commands/config-init-workspace.ts` (`runInitWorkspace`);
> parity: `packages/daemon/test/getting-started-narrative-parity.test.ts`
> @HEAD.
## 4. File-backed missions / slices tree
+527 -32
View File
@@ -9,7 +9,7 @@ applies-when: |
JSON output, cross-host, coordination primitives.
siblings: [README.md, architecture/daemon-core.md]
prerequisite-reads: [README.md]
last-verified-against-source: 8d55ea60
last-verified-against-source: b13a8e4c7
last-updated: 2026-06-20
---
@@ -23,8 +23,151 @@ Verified against the shipped CLI on 2026-06-15 (v0.3.4) using:
This document reflects the current `rig` surface as shipped. Where live help text is narrower than the implementation, notes call that out explicitly.
## Terminal dashboard entry
The TUI's **Connections** section (`connections` or `:connections`) shows
running daemon and launching CLI identity, selected instance settings with
source, observed rigs and authored Specs, Slack configuration and running wire
state, and registered humans with primary/secondary bindings. `back` restores
the work view that opened Connections. The view supplies supported CLI guidance;
it does not enable a connector, change settings, or send a test message.
Connections reads the passive `GET /api/gateway/connections` projection. It
selects safe fields from settings, config, the human registry and gateway status;
secret values/references and raw errors are omitted. Current config is compared
with the wire's activation digest when available. Last Slack verification comes
from at most 64 KiB of the existing channel-operation audit tail, matched to the
current configuration and labeled with its time/actor. It is historical scope
and channel-membership evidence, not current reachability, credential identity,
delivery or readership. Missing, failed, incomplete and changed observations
remain distinct; an older daemon without the projection reads as unavailable.
Use the displayed guidance on that instance; `rig slack verify --json` explicitly
contacts Slack. Ordinary navigation/refresh does not.
`rig tui` opens an independent TUI through the same front door as bare `rig`.
`rig tui --shared` requires interactive input/output and a loopback daemon
connection. It resolves the kernel's bound `operator.human` terminal and
attaches a local tmux client without starting a seat or another TUI. Ctrl-b,
then d detaches; reattaching preserves navigation. A missing or ambiguous
binding fails with inspection guidance instead of creating another kernel.
Fresh kernel terminals start `rig tui` automatically. Older terminals, or a
terminal where the TUI was quit, retain their shell: run `rig tui` there once.
The kernel view uses TUI instance `kernel`; standalone uses its ordinary
instance. `rig tui commands --json` exposes the command registry for agent
control. In the ordinary shell, Tab completes command names/aliases, section
jumps and arguments from the current snapshot. Ambiguous prefixes show candidates;
keep typing and press Tab again. No match preserves the text. Enter executes;
Escape clears. Free-text filters are not completed. Bracketed paste is text, never
an implicit command submission.
Recent shows ordered, wrapped queue changes. Enter on an event opens the original
record (including its raw timestamp); Escape returns to the previous view/scroll.
Recorded changes remain attributed claims, including attempts and failures.
Absolute TUI times use `ui.timezone`, default `America/Los_Angeles`, with native
daylight-saving rules. Run `timezone` in the TUI for the current value and settings
guidance. `rig config set ui.timezone Europe/London` persists an alternative;
`rig config reset ui.timezone` restores the default. Reopen the TUI after changing
it. `OPENRIG_UI_TIMEZONE` overrides the file setting. Invalid settings produce a
visible fallback notice. Relative ages and stored source timestamps are unchanged.
See [the first-use journey](../reference/getting-started.md).
## Daemon shutdown
`rig daemon stop` sends at most one SIGTERM to a live daemon identified by its
local state.
An explicit `OPENRIG_URL` that disagrees with that target refuses before signaling.
The daemon has one referenced **10-second** budget covering asynchronous service
shutdown, connections and recorder drain; repeated SIGINT/SIGTERM joins that stop.
The CLI allows **12 seconds** for process exit, then verifies the original PID and
listener even if `daemon.json` was removed. A refused listener, a responding
listener and an unavailable probe are different outcomes.
`$OPENRIG_HOME/daemon-shutdown.json` records the PID, shutdown start/completion,
phase, failures and `clean|failed|timed-out` outcome. Only successful drains mark
the lifecycle record clean. A failed/timed-out drain returns nonzero, including
after the process exits. For a recorded target, missing or stale receipt evidence
is unverified and also returns nonzero. Failed/unverified stops retain target
state for retries; status reads only clean it after a matching clean receipt.
An already-exited target uses the same receipt judgment without another signal.
With no recorded target and a refused listener, the CLI reports a distinct
no-target no-op, not a clean-drain verdict. Unbound incomplete or unreadable
local shutdown evidence remains nonzero/unverified rather than being attributed
to an unrelated listener.
Older daemons without the receipt can therefore be proven stopped without their
graceful drain being certified. Inspect the receipt and `daemon.log`;
do not infer that pending external work completed or was rolled back, or retry it
blindly. The bound covers asynchronous waits, not a synchronous event-loop wedge.
## Human delivery
`rig gateway human list --json` discovers registered `<entityId>@external`
addresses. `rig gateway human show <entityId> --json` includes primary-connector
readiness, reason and next inspection. Project policy decides when to contact a
human; `messaging-the-human` supplies the transport mechanics.
Create a human request with `rig queue create --destination <address> --summary
"<decision>" --body-file <file> --evidence-ref <ref> --verify --json`. It persists
one row before checking the delivery receipt. `posted` proves connector
acceptance, never readership; `transport-failed`, `never-posted`, `still-pending`
and `indeterminate` preserve the request and name the next inspection. Do not
blindly repeat the create. `rig send` is for agent seats.
`rig slack manifest [--url|--json]` (experimental in 0.6.0) prints the Slack app manifest a user creates
their own private Socket Mode app from, offline (no daemon, tokens or network).
`--url` is Slack's create-app link with the manifest prefilled; `--json` adds the
scope and event lists and why each scope is requested. The same object is served
read-only at `GET /api/gateway/slack/manifest` for the TUI Connections page. Setup
steps: `docs/reference/slack-app-setup.md`.
`rig slack enable [--reason <reason>]` seeds existing backlog only on a disabled
to enabled transition. Repeating enable does not reseed or restart. `rig slack
disable --reason <reason>` requires a shutdown reason. Both return attributed
lifecycle receipts; direct requests can name `actor` when no managed-session
header exists. Header-derived and claimed identities remain distinct.
Configuration, verification and human-binding edits also record actor, reason,
prior/result state and effect in
`$OPENRIG_HOME/state/human-channel-operations.jsonl`. Each operation has a start
and a completion receipt with one ID; a missing completion is indeterminate.
Snapshots retain state/digests rather than credentials or message bodies. Local
CLI configuration/verification/binding receipts are explicitly `claimed:v1`.
`rig host pair <url> [--human <address>]` selects the sole registered target
human, or requires an explicit selection when several exist. A missing registry
or ambiguous recipient refuses before creating an approval. An unset
`workspace.operator_seat_name` no longer invents a username-derived kernel
seat: Mission Control discovers a single registered human or shows identity
uncertainty. Explicit existing seat selections remain supported. Legacy aliases
resolve only when their entity is registered; old failed rows are preserved,
not implicitly delivered or replayed by registration.
## Overview
System Health diagnosis, policy, checkpoints, and dispositions are documented in
[Agent-operated System Health diagnosis](../reference/health-diagnosis.md).
Diagnosis `show`/`list --json` are summaries; use `--full --json` for the previous
complete evidence payload. Their defaults identify omitted fields and the exact
expansion command. Workflow human views remain summaries; workflow `--json`
continues to return the complete API payload.
For raw JSON evidence files, CLI read defaults cannot intercept `cat` or a Node
print. Inspect size and keys before selecting needed fields; retain full evidence
on disk. For example:
```sh
wc -c < receipt.json
jq 'keys' receipt.json
jq '{gate, judge, cutSha, surfaceCount, overallPackageVerdict}' receipt.json
```
Select the actual fields present in that file. Redirect intentional full CLI
output to a file before inspecting selected fields; use `set -o pipefail` when
piping a command so a formatter cannot hide its failing exit.
- Binary: `rig`
- Top-level command groups: `64`
- Output mode: human-readable by default; many commands also support `--json`
@@ -116,9 +259,34 @@ Subcommands:
- `logs [--follow]`
Notes:
- `start` launches the daemon process and accepts runtime overrides for port, host, and DB path.
- `start` additively reconciles the canonical instance layout before launching
the daemon process, then accepts runtime overrides for port, host, and DB
path. Direct daemon first start uses the same initializer. Existing files are
preserved and wrong-type managed paths are refused before any write. See
`docs/reference/instance-layout.md`.
- Startup uses a local `daemon-start.lock` reservation before instance initialization,
shared by `daemon start`, `start`, and `up`. A concurrent supported launch fails
without spawning another daemon or running its pre-bind database initialization.
Success requires the spawned child's numeric PID from `/healthz` at every required
listener and a live child through atomic `daemon.json` publication. Missing or
mismatched process identity, an invalid listener plan, child exit, and unresolved
probes cannot publish success. Use a matching CLI/daemon pair; a legacy endpoint
without PID evidence is insufficient. This local reservation does not serialize
old binaries or direct execution of the daemon entrypoint.
- Startup checks physical liveness after the synchronous state writer as well as
before it; queued child events alone cannot prove that boundary. If publication
fails verification, startup rejects and withdraws only state matching this
launch's PID, start time, listener and DB. It never removes a replacement owner.
- Failed process inspection is uncertainty, not proof that a child exited. Cleanup
waits for the owned child's exit evidence; an error event is insufficient.
- Failed startup signals only its own child and waits boundedly for its exit. If
cleanup cannot be confirmed, the reservation remains and the error names the PID.
An interrupted launcher can also leave `daemon-start.lock`. Inspect its recorded
launcher/child PIDs, `daemon.json`, and `daemon.log`; only after proving both
processes absent, archive that reservation before retrying. There is no timed
takeover: a dead launcher may have left a live, unbound child.
- `logs` reads daemon log output and can follow it.
- **Deploy identity (v0.4.4, OPR.0.4.4.11 FR-6/7)**: a PACKAGED build (built via `scripts/build-package.sh`) is stamped with `{semver, commit, dirty, builtAt}`; the daemon's `/healthz` payload carries the four stamp fields additively and `rig --version` renders `<semver> (<commit8>[, dirty])`. A source/dev run has NO stamp and adds NOTHING (never an invented SHA) — `/healthz` keeps its legacy body and `--version` prints the plain semver. This is the 30-second stale-deploy diagnostic: an unstamped or old-commit `/healthz` on a long-running host means you are looking at an older deployed build, not the source tree. Source: `packages/{daemon,cli}/src/build-info.ts` (`stampFields`), `packages/daemon/src/server.ts` (`/healthz`), `packages/cli/src/version.ts`.
- **Deploy identity (v0.4.4, OPR.0.4.4.11 FR-6/7)**: a PACKAGED build (built via `scripts/build-package.sh`) is stamped with `{semver, commit, dirty, builtAt}`; the daemon's `/healthz` payload carries the four stamp fields additively and `rig --version` renders `<semver> (<commit8>[, dirty])`. A source/dev run has no build stamp (never an invented SHA); `/healthz` still reports its runtime PID and `--version` prints the plain semver. This is the 30-second stale-deploy diagnostic: an unstamped or old-commit `/healthz` on a long-running host means you are looking at an older deployed build, not the source tree. Source: `packages/{daemon,cli}/src/build-info.ts` (`stampFields`), `packages/daemon/src/server.ts` (`/healthz`), `packages/cli/src/version.ts`.
### `rig status`
@@ -154,6 +322,18 @@ Supported keys:
- `transcripts.enabled`
- `transcripts.path`
- `workspace.root` (and other workspace-rooted paths used by `init-workspace`)
- `context.root` (default `$OPENRIG_HOME/context`; env
`OPENRIG_CONTEXT_ROOT`) — the single writable addressable context library.
The removed `context.packs_root`, config field `context.packsRoot`, and env
`OPENRIG_CONTEXT_PACKS_ROOT` are refused with replacement guidance.
- `context.system_world` (default `default`; env
`OPENRIG_CONTEXT_SYSTEM_WORLD`) — selects
`$OPENRIG_HOME/context/system/system-world.yaml`, an explicit replacement
manifest path, or the explicit `disabled` state. `rig context work-install`
reports the resolved state and provenance.
- `skills.root` (default `$OPENRIG_HOME/skills`; env `OPENRIG_SKILLS_ROOT`) — the
one authoritative Git-versioned managed skill catalog. An override replaces
the default; it does not add an overlay root.
- `snapshots.periodic.enabled` (default `true`) — daemon-side periodic snapshot scheduler on/off (v0.3.4)
- `snapshots.periodic.interval_seconds` (default `300`) — interval between periodic snapshots
- `snapshots.periodic.retention_keep` (default `10`) — number of periodic snapshots to retain per rig
@@ -168,7 +348,7 @@ Precedence:
Notes:
- `--with-source` (top-level) and `--show-source` (`get`) report per-key source/default for honest provenance.
- `init-workspace` scaffolds the default workspace at `~/.openrig/workspace/` (or the `--root` override) with mission/slice folders. `--force` overwrites scaffolded files (does NOT remove directories); `--dry-run` previews without writing. New in v0.3.0.
- `init-workspace` additively scaffolds `missions/`, `exhaust/`, `SPEC.md`, `project.yaml`, `workspace.yaml`, and `.gitignore` at `~/.openrig/workspace/` (or the `--root` override). `--dry-run` previews without writing. `--force` is deprecated compatibility and preserves existing files. New in v0.3.0.
- `snapshots.periodic.*` (v0.3.4): the daemon-side scheduler takes periodic snapshots per rig at `interval_seconds` and retains the newest `retention_keep`. At restore time, newest-wins between `auto-periodic` and `auto-pre-down` snapshots.
Legacy env compatibility: the original runtime keys still accept deprecated
@@ -429,7 +609,7 @@ Notes:
### `rig snapshot`
Usage:
- `rig snapshot <rigId>`
- `rig snapshot <rigId> [--intended-seats <ids>]`
- `rig snapshot list <rigId>`
Subcommands:
@@ -437,7 +617,9 @@ Subcommands:
### `rig restore`
Usage: `rig restore <snapshotId> --rig <rigId>`
Usage:
- `rig restore <snapshotId> --rig <rigId>`
- `rig restore status <attemptId> --rig <rigId> [--json]`
Important:
- `--rig <rigId>` is required by the source code, even though the help text does not visually mark it as required.
@@ -445,6 +627,9 @@ Important:
Notes:
- Human output prints each restored node and any failed node error.
- Non-zero exit if any restored node fails.
- A started asynchronous restore prints its attempt id. `status` derives the
original and current intended-set verdict, snapshot selection, historical
exclusions, and unresolved intended seats from that durable attempt.
### `rig restore-check`
@@ -707,13 +892,31 @@ Notes:
### `rig launch`
Usage: `rig launch <rigId> [nodeRef] [--seats <ids>] [--hold-reason <reason>] [--json]`
Usage: `rig launch <rigId> [nodeRef] [--seats <ids>] [--hold-reason <reason>] [--snapshot-id <id>] [--plan] [--json]`
Notes:
- Launches or relaunches a node in a running rig.
- `nodeRef` (optional) can be a logical ID or node ID for the single-target form.
- `--seats <ids>` (v0.3.4, slice 11) takes a comma-separated list of logical IDs for node-granular managed partial restore — launch a named subset of seats while holding the rest. Retires the prior `pod_aware_launch_unsupported` dead-end.
- `--hold-reason <reason>` records the reason non-target seats are being held; surfaced via observability so the held state is auditable.
- `--snapshot-id <id>` selects one exact restore-usable snapshot instead of
applying the automatic choice. `--plan` previews a multi-seat subset and its
non-target effects without mutation.
- `rig launch <rigId> <nodeRef> --retry-startup-from <member-file> --rig-root <absolute-source-root>`
explicitly retries an added agent whose first start failed during resource
projection, before a native conversation began and before startup context was
saved. First correct the projection failure, exit the failed shell normally,
and use `rig seat clean <seat> --reason <reason>` after it is stopped. Supply
the original bare or `{member: ...}` YAML/JSON fragment, without edges or
member startup/continuity overrides. The agent source hash and retained
identity, model, cwd and policy must agree. The retry uses normal validation,
projection and required startup delivery on the same node; it preserves other
seats and prior failures. It refuses bound, live, indeterminate or previously
native sessions. This is not snapshot restore or a substitute for deliberate
fresh launch, and cannot combine with snapshot, subset or plan options.
The operator is responsible for supplying the complete original fragment:
retained state cannot reconstruct missing member, pod or rig instructions.
Never strip unsupported overrides to make a retry pass.
### `rig remove`
@@ -732,6 +935,22 @@ Notes:
## Identity, Communication, and Context
### `rig startup-proof submit`
Usage: `rig startup-proof submit --challenge-id <id> --answer <answer> [--json]`
Submits the selected startup exercise through the authenticated activity hook,
using the current seat's identity and the challenge supplied in its startup
prompt. A correct current answer returns `oriented: verified`; a bare
acknowledgement, wrong answer, or stale challenge fails. Startup readiness alone
does not verify orientation.
An extra exercise is opt-in through a `startup_proof` startup action with
`value: authenticated` and `idempotent: true`. Omission adds no exercise, and a
later applicable `value: none` selects lean startup. See
[startup proof selection](../reference/rig-spec.md#startup-proof-selection)
for the full authoring and restore rules. Terminal nodes receive no challenge.
### `rig whoami`
Usage: `rig whoami [--node-id <id>] [--session <name>] [--host <id>] [--full | --verbose] [--json]`
@@ -806,8 +1025,13 @@ Usage: `rig walk <seat> --through <ref | files...> [--pace <duration>] [--json]`
Notes:
- `--through` accepts either one path-like context ref or an ordered list of existing local files; mixing the two forms is rejected.
- Sends one piece at a time through the normal transport and waits `--pace` between pieces (default `10s`; `500ms`, `10s`, and bare seconds are accepted). There is no trailing delay.
- Sends one piece at a time through the normal transport and waits `--pace` between pieces (default `10s`; duration overrides require an explicit `ms` or `s` suffix). The same explicit-unit grammar applies to `--consume-timeout`, `--consume-poll`, and `--turn-timeout`; bare numbers are refused. There is no trailing delay.
- A missing local file or missing/unreadable ref member aborts before the first send, so a walk delivers every piece or none.
- Where a generation record resolves, each piece must appear as a complete user message in the newly appended record and its corresponding native turn must close before the next piece. Only CRLF line endings and surrounding whitespace are normalized; internal whitespace, missing middles, shared prefixes, and tails do not qualify.
- Claude closure follows the message's UUID ancestry through an assistant response to `turn_duration`. Codex uses a `response_item` user message within a named `task_started`/`task_complete` turn; queued input or a different turn's completion is insufficient. These receipts prove delivery and turn completion, not semantic comprehension.
- Codex generation lookup joins the bound process's post-start log thread IDs to retained native CLI conversations. Auxiliary title threads cannot replace that identity; zero or multiple matching conversations remain unverified instead of being selected by recency.
- Codex records resolve through the verified current pane/process and native thread table, including before token telemetry exists. The rollout header must identify that thread. Missing or ambiguous identity is reported as unverified. A generation/file replacement or unreadable record during a verified walk aborts it; it never silently continues into a replacement occupant.
- `--json` reports `consumptionVerified`. If the initial generation-record probe is unavailable, legacy delivery remains possible with an explicit unverified advisory and `consumptionVerified: false`.
### Cross-host execution (`--host <id>`)
@@ -816,6 +1040,21 @@ commands use single-hop SSH CLI-side shell-out; HTTP-transport commands talk to
the remote daemon API. The local daemon is not involved in SSH routing. The
remote host is expected to have its own managed `rig` available on `$PATH`.
HTTP sender attribution uses the originating instance's persisted self-host
identity, read without creating or changing its database. Explicit DB config
wins; otherwise the last daemon launch's DB selection is retained. This works
while the local daemon is stopped. The destination's identity and the configured
display name are never used as the origin.
Ordinary local requests retain bare seat names. For an explicit `OPENRIG_URL`
(or legacy `RIGGED_URL`), a bounded health probe preserves bare addressing only
when the target's self-host identity matches the local one. A different,
unavailable, or ambiguous target—including a loopback forwarding endpoint—gets
the known origin suffix. Already-qualified senders remain unchanged. If the
local origin cannot be read, delivery proceeds with `origin-unknown:v1`
provenance and a diagnostic after a successful response; queue forwarding
preserves that uncertainty rather than attributing the sender to the relay.
Hosts are declared by the operator in `~/.openrig/hosts.yaml`:
```yaml
@@ -1051,16 +1290,18 @@ Subcommands:
- `fallback <qitemId> --destination <session> [--reason <text>] [--json]` — reroute to fallback seat
- `show <qitemId> [--json]`
- `transitions <qitemId> [--json]` — append-only transition log
- `list [--destination <session>] [--source <session>] [--mine] [--state <csv>] [-a | --all] [-A | --all-rigs] [--full] [-o <json|wide>] [--limit <n>] [--json]` — **v0.4.0 grammar (slices 28 + 32, docker / kubectl-aligned)**:
- `list [--destination <session>] [--source <session>] [--owned] [--mine] [--state <csv>] [-a | --all] [-A | --all-rigs] [--full] [-o <json|wide>] [--limit <n>] [--json]` — **v0.4.0 grammar (slices 28 + 32, docker / kubectl-aligned)**:
- **`rig queue list`** (no flags) → active states only (`pending` / `in-progress` / `claimed` / `blocked` / `handed-off`; NOT `done` / `canceled`), **current-rig** breadth, compact rows: `qitemId`, `state`, `source→destination` (or `current-owner`), `closure_reason` / `closure_target` (when handed-off / blocked), `mission`, `slice`, `tier` / `priority`, `age` / `updated_at`, short title, capped tags. Excludes: full body, chain_of_record, transition history, proof / artifact blobs.
- **`-a` / `--all`** → include closed / done history within the current breadth (docker `-a` axis: history).
- **`-A` / `--all-rigs`** → cross-rig breadth (kubectl `-A` axis: breadth). Composable with `-a`, `--mine`, `--full`.
- **`-A` / `--all-rigs`** → cross-rig breadth (kubectl `-A` axis: breadth). Composable with `-a`, `--owned`, `--mine`, `--full`.
- **`--full`** → add body + chain-of-record + full tags + transition history to whichever scope is selected (field-breadth axis).
- **`-o json|wide`** → encoding, compact-by-default. `-o json` does NOT imply full body (compact JSON is token-safe + machine-parseable); `--full -o json` returns the full JSON.
- **`--mine`** → just the caller's items.
- **`--owned`** → obligations assigned to the caller (`destination_session` only).
- **`--mine`** → the caller's source-or-destination union, including rows the caller authored but does not own.
- Pipeline use must enable `set -o pipefail`; otherwise the shell reports only the downstream formatter's status and can mask a nonzero `rig` read such as a timeout.
- `--destination <s>` / `--source <s>` / `--state <csv>` keep working and compose with the new flags.
- The four axes (scope × history × field-breadth × encoding) are orthogonal and composable. The bare unscoped firehose that aggregated cross-rig + full-history (~64,000 tokens on the live host) is retired as a default — opt-in via `-A -a --full`.
- Use `rig queue show <qitemId>` for the full single-item view (kubectl `describe` / docker `inspect` pattern).
- Use `rig queue show <qitemId>` for the body preview; `rig queue show <qitemId> --full --json` returns the original complete record. Preview JSON adds `readView` with completeness, omitted content, full JSON byte size and the exact expansion command.
- `overdue [--json]` — in-progress qitems past closure_required_at
- `inbox-drop <destinationSession> --sender <session> (--body <text> | --body-file <path>) [--tags <csv>] [--urgency <u>] [--audit <pointer>] [--id <inboxId>] [--json]`
- `inbox-absorb <inboxId> --receiver <session> [--json]` — promote a pending inbox entry to a queue_item
@@ -1140,15 +1381,16 @@ Usage: `rig project <subcommand>` — L2 agent-backed classifier with daemon-enf
Subcommands:
- `lease-acquire [options]` — acquire the active classifier lease for the caller.
- `lease-heartbeat [options]` — send a heartbeat for an active classifier lease (extends TTL).
- `lease-heartbeat [options]` — send a heartbeat for an active classifier lease (extends TTL). A lease already past its TTL refuses with `lease_expired`; acquire again instead.
- `lease-show [options]` — show the currently-active classifier lease.
- `reclaim-classifier [options]` — operator verb to reclaim the active classifier lease. Use `--if-dead` to refuse if holder is still alive.
- `classify <streamItemId> [options]` — project a stream item with classification fields. Idempotent on `stream_item_id`; requires an active lease.
- `list [options]` — list project classifications with filters.
- `classify <streamItemId> [options]` — project a stream item with classification fields. Idempotent on `stream_item_id` (first write wins). Requires `--lease-id`: the result is refused (`lease_mismatch`, `lease_expired`, `lease_held`) unless that exact lease is active, unexpired and held by `--session` at write time. Optional `--area`, `--scope-ref` (with `--candidate-set-version`), `--duplicate-of`, `--needs-human true|false` (omit when unknown), `--classifier-version`, `--taxonomy-version`, `--attempt-id` with `--execution-id`. Without `--attempt-id` the result is a manual classification, not bound to the attempt ledger. Every supplied field must be a string (IDs and versions non-empty); `--needs-human` omitted means unknown.
- `list [options]` — list project classifications with filters (`--session`, `--destination`, `--area`, `--scope-ref`, `--needs-human true|false|unknown`).
- `show <projectId> [options]` — show one project classification.
Notes:
- Lease semantics: only one classifier holds the active lease at a time. Heartbeats extend TTL; lease expiry frees the slot for the next acquirer.
- Lease semantics: only one classifier holds the active lease at a time. Heartbeats extend TTL only while the lease is unexpired. Re-acquiring your own expired lease issues a new lease id, so results bound to the old id are refused; another session must use `--evaluate-deadness-first` or the reclaim verb.
- Attempt ledger (daemon HTTP, used by a classifier occupant): `POST /api/projects/attempts/begin`, `/attempts/:id/abstain`, `/attempts/:id/fail`, `GET /api/projects/eligible`. Abstentions and errors are recorded there, never in the classification row; errors retry with bounded backoff, then end `exhausted`. Every `begin` (including a timeout or error retry) returns a new `executionId`; only the current one can abstain, fail or bind a result (`attempt_superseded` otherwise), because a renewable lease never proves an older execution stopped.
- `classify` enforces L1→L2 foreign-key existence: the referenced `stream_items` row must exist or the call is rejected before any state mutation.
- `reclaim-classifier` is the operator-side verb to recover from a hung classifier; `--if-dead` adds a liveness guard so a still-heartbeating classifier is not stolen from.
@@ -1191,16 +1433,206 @@ Phase D extends the policy enum with `workflow-keepalive` (the policy deferred f
### `rig workflow`
- `validate <specPath>` — validate a workflow spec file; returns structured ok/error report (role resolution, step uniqueness, allowed-exits consistency, optional seat liveness). Spec-only: it takes no rig context (OPR.0.4.6.FAC1 arch ruling — rig-coverage checks happen at instantiate).
- `compile <missionPath> [--operation-key <key>]` — read `project.yaml` →
`mission.yaml` → `slice.yaml` into an inspectable lifecycle graph without
creating a cached spec, workflow instance, or qitem.
- `instantiate-lifecycle <missionPath> --operation-key <key> --root-objective
<text> --created-by <session>` — compile and start an eligible lifecycle;
`--entry-owner` and `--rig` retain their normal workflow meanings. The opaque
operation key makes exact replay idempotent and conflicting reuse refuses.
- `instantiate <specPath> --root-objective <text> --created-by <session>` — create a new instance + entry-step qitem in the same daemon transaction; `--entry-owner <session>` overrides the default entry owner. **OPR.0.4.6.FAC1**: `--rig <name>` binds the instance to a rig (overrides the spec's `target.rig` DEFAULT; persists as `boundRig` on the instance, rendered by `show`/`trace` and carried in `--json`). On a bound instance, a role with **no `preferred_targets`** resolves to a live capable SEAT on that rig by the pure capability policy (running agents declaring the role, managed seats only, required runtime, least pending backlog, deterministic coordinate tiebreak). Unknown rig = structured `bound_rig_unknown`; a bound-rig role no seat structurally declares = `bound_rig_role_uncovered` (existence at any lifecycle state satisfies it — liveness is checked when the step projects). No `--rig` and no spec default = unbound, byte-identical pre-FAC-1 behavior.
- `run <specPath> …` — accepts the same `--rig <name>` binding (run instantiates too).
- `project --instance <id> --current-packet <qitem-id> --exit <handoff|waiting|done|failed> --actor-session <session>` — close the current packet AND project the next-step packet IN THE SAME daemon transaction (transactional-scribe; lost handoffs impossible by design). `--result-note <text>`, `--blocked-on <ref>`, `--next-owner <session>` modify behavior.
- `list [--status <s>]` — list instances; optionally filter by status (`active`/`waiting`/`completed`/`failed`).
- `show <instanceId>` — show one instance.
- `revise <instanceId>` — inspect authored versus bound lifecycle input without
writing. The result names changed sources/steps, composition, compatibility,
and an apply command containing the inspected version, digest and operation
key. Fill in the actor and decision before using `--apply`.
- `operation <key>` — recover the original lifecycle creation or revision
receipt and current instance after a lost response or later source edit.
- `trace <instanceId>` — show the instance + its append-only step trail (audit-only).
- `continue <instanceId>` — idempotent inspector; in v1 returns the current state.
The owner-as-author + workflow-as-transactional-scribe contract is enforced by the daemon. The owner of a packet decides when it closes; the workflow runtime atomically records the closure AND creates/projects the next qitem per the workflow spec, in a single daemon transaction.
### Project-owned release profiles
`project.lifecycle.profile` selects a key in `project.lifecycle.profiles`. Each
selected entry contains `required_steps` (a nonempty list of stable obligation
IDs) and `workflow` (the ordinary workflow language). The reusable example at
[`project-release-profile.yaml`](../reference/project-release-profile.yaml)
names nine release-ceremony stages followed by one `release-boundary` judgment.
That final judgment covers the seven areas of the canonical
[`release-boundary.md`](../reference/release-boundary.md) checklist in one
agent-authored record. It does not create seven automatic gates.
Put the profile in `project.yaml` once; release missions need only their ordinary
composition. Customize the example's project ID, roles, and policy for the
project. Every profile step declares `depends_on` (including `[]` on roots);
conditional `next_hop.on` jumps are refused so required stages cannot be bypassed.
Missing required IDs refuse with `lifecycle_required_step_missing`.
Prerequisite cycles make compilation ineligible with `dependency_cycle`, even
when `loop_guards.max_hops` is set. That guard bounds routing loops; it cannot
make mutually dependent steps ready.
Precedence is explicit:
- With a selected project graph and no mission workflow, the mission inherits it.
- `mission.lifecycle: {profile: <same-profile>, mode: extend, workflow: ...}` adds
new steps, merges roles by name, and appends context references. An extension
cannot replace a step ID; its workflow accepts only `steps`, `roles`, and
`context_refs`.
- `mode: override` supplies a replacement workflow. All required project step IDs
and the prerequisite relationships between them must remain. Additional
intermediate stages are allowed. An omitted or unknown mode with competing
workflows refuses with `lifecycle_override_ambiguous`.
- Without `project.lifecycle.profiles`, the existing mission-authored workflow
takes precedence over slice execution declarations. This legacy path remains
supported and reports an advisory that no project graph is selected. Adding a
project graph to an existing copied mission workflow requires explicit mode
selection or removal of the copy. Project lifecycle fields such as an inert
`workflow` or `workflow_ref` are refused rather than silently ignored.
An authored, ready successor may be a mission extension step depending on
`release-boundary`. Without that extension, `release-boundary` exits `done` and
finishes the current lifecycle. The runtime creates the continuation packet;
the agent judges readiness and performs any authorized activation. It does not
infer readiness from a folder or activate future scope automatically.
Compilation exposes `graphSource` (selection mode, project/mission addresses,
required IDs), the complete `workflowSpec`, dependencies, and source digests.
These are bound into the compiled input digest and persisted at instantiation.
Changing source bytes under an existing operation key refuses. Relative paths
passed to both CLI `compile` and `instantiate-lifecycle` are resolved against the
**caller cwd before HTTP**, so daemon cwd cannot change their meaning.
`workflow guidance <instance> [--packet <qitem>] [--component <id>] [--full]`
reads current selected SDLC teaching and original SPEC intent. `workflow show`
and `workflow continue` expose the same guidance. Entry, handoff, route and resume
packets carry a compact preview; existing admitted wait notices refresh it at
send time. Healthy waiting does not read or inject the catalog on every tick.
Selection follows project → mission → explicit active slice (or a legacy slice's
exact executable identity). A narrower component list replaces ancestor components
and edges; an omitted catalog inherits. Bound member lists alone do not select an
active slice. Catalog addresses use the shared H2/H3 resolver: absolute or
`$OPENRIG_HOME` paths, paths relative to the declaring manifest, `root: repository`
from that manifest's Git tree, or a file declared by the installed context library.
Missing/ambiguous components and unavailable sources are named unknowns; prose
stays verbatim and has no required semantic field ontology.
Compact guidance previews one component, using exact owner matches where possible;
otherwise it labels a menu preview. Position remains unknown: neither array order,
clock nor handoff infers method progress. `--full` expands components relevant to
current packet custody (all selected components when none match); `--component`
chooses one explicitly. Whole oversized prose blocks are omitted with an expansion
notice, so a clipped caveat never masquerades as complete teaching.
Guidance reports manifest hashes against the lifecycle binding and the current
catalog hash. Referenced prose is current authored advice, not a cached executable
snapshot. YAML selection edits use the existing revision path; catalog-only prose
changes refresh on read without creating work or a new revision store. Reading
advice, receiving a notice and independent acceptance remain distinct.
`workflow show`, read-only `workflow revise`, and the TUI distinguish current,
source-only, compatible, incompatible and unavailable authored comparisons.
Catalog or membership bytes can change without changing executable steps;
file edits do not silently adopt either kind of change. A supported `revise
--apply` preserves completed/live steps, required obligations, queue custody and
prior receipts while adopting future-step changes on the same instance. It
requires the inspected version/digest, a stable operation key, actor and reason.
Changed completed/live steps, removed obligations and unsupported migrations
refuse with a specific explanation; restore the protected contract and revise
unstarted successors. After an ambiguous response, inspect `workflow operation
<key>` or retry the identical apply command to recover its one committed effect.
Mission-bound continuation and wait guidance carries a snapshot of authored
planning, wave admission/review and integration rules, with a pointer to inspect
the current source before deciding. Waves guide agents; executable dependencies
schedule steps. Bound slice sources do not automatically create child workflows.
The execution view also reads current arrangement guidance into `planning_guidance`
with exact source fields. Ordinary mission/wave/slice inspection keeps admission,
review and accepted-core/full-contract guidance distinct from executable edges,
attributed proof and current custody; prose does not create those facts.
`workflow show`/JSON and the TUI execution view show every named obligation with
its state and receipt state. Required steps need `project --evidence-ref <ref>`
on a successful `done`/`handoff` exit; missing references refuse before mutation
with `lifecycle_receipt_required`. Waiting and failed exits remain available.
The projection records who supplied the reference and when. `recorded` means an
attributed workflow receipt exists, not that the daemon verified its substance.
An ordinary terminal queue row never supplies that receipt. Agents inspect the
actual evidence, including reasoned not-applicable or deferred boundary areas.
Existing typed `acceptance` contracts remain separate and retain their checks.
### Authored mission boundary (legacy)
Without a project-owned graph, when a mission declares `lifecycle.workflow`, `compile` uses that explicit
workflow instead of deriving steps from active slices. Slice membership and
backlinks are still validated and included in provenance; slice SDLC selections
do not add workflow steps or gates. `lifecycle.profile` must match the profile
selected in `project.yaml`:
```yaml
# Add to mission.yaml; project.yaml already selects release-boundary-v0.
lifecycle:
profile: release-boundary-v0
workflow:
context_refs: [SPEC.md, PROGRESS.md, NOTES.md]
entry: {role: orchestrator}
roles:
orchestrator: {preferred_targets: [orch@my-rig]}
steps:
- id: mission-boundary
actor_role: orchestrator
objective: Inspect current evidence and decide the next authored exit.
allowed_exits: [waiting, done, failed]
re_present_after_seconds: 300
re_present_max_seconds: 3600
```
The nested `workflow` uses the ordinary workflow language; its ID defaults to
`lifecycle-<project>-<mission>`, and its compiled version is derived from source
digests. Boundary steps explicitly allowing `waiting` default to a five-minute
initial reminder and a one-hour cap; both fields in the example override those
defaults. Missing operation identity leaves compilation ineligible. Instantiation
with one opaque `--operation-key` persists the compiled input digest and lifecycle
binding; exact replay returns the same instance/entry packet, and changed input
under that key refuses. Authoring alone never starts an instance.
`context_refs` carries addresses in entry, successor, route/resume, and reminder
packets. The lifecycle compiler also includes the project/mission manifests and
`project.install.context`. Relative project references resolve from the project
root; profile workflow references resolve from the project root and mission workflow
references from the mission directory. References
are not evidence verdicts: the receiving agent opens current context, may inspect
beyond it, and chooses the exit. The daemon neither interprets receipts nor
activates a mission. `rigx project` remains a separate manual shadow.
Outside mission-boundary compilation, an unmapped `waiting` exit with
`re_present_after_seconds` alone remains one-shot.
Adding `re_present_max_seconds` opts into an existing queue/watchdog timer that
repeats with exponential backoff: the example waits 5, 10, 20, 40, then 60 minutes
between reminders. The cap must be an integer at least as large as the initial
delay. No new packet is created. Repeated waiting acknowledgments retain the
schedule; changed structured `closureEvidence` or a new blocker resets it.
`rig workflow project --evidence-ref <ref>` records an attributed reference as
closure evidence, without interpreting it or substituting for typed acceptance. Omitted
evidence preserves the prior evidence, and object key order is immaterial.
A transition on the exact qitem blocker makes the reminder due on the next
scheduler tick (normally within one second), resetting the initial delay.
Wake delivery receipts do not count as progress. The workflow's own waiting
acknowledgments are on its frontier packet, not its upstream blocker; a repeating
wait cannot name itself as its blocker. Blocker completion retains native
queue unpark/wake behavior. Terminal or rerouted packets retire their timer.
Restart reconciles blocker transition identity and resumes the persisted delay;
replaying the same transition does not create another wake. A live repeating
timer can have an unconsumed wake: these are separate facts in park status.
Hard human gates are authored using the existing `gate` field; a reminder does
not manufacture a gate or satisfy one.
## Operational Inspection
Read-only inspection commands for compaction planning, workflow heartbeat, and seat handover observability. Default mode is read-only across this section.
@@ -1230,6 +1662,10 @@ Usage: `rig seat <subcommand>`
Subcommands:
- `status <seat> [options]` — show read-only seat handover observability status.
- `handover <seat> [options]` — plan a safe two-phase seat handover.
- `launch <seat> --fresh --reason <text> [--stop] [--operator <address>] [--json]`
— create a deliberate blank occupant for exactly one existing seat. No
continuity source is used; a live managed occupant requires `--stop`, while
adopted or unmanaged ambiguity refuses.
- `clear-attention <session> [--reason <text>] [--json]` — evidence-gated, operator-attested, audited reconcile of a stuck `attention_required` seat.
Notes:
@@ -1273,26 +1709,60 @@ Notes:
Usage: `rig context <subcommand>` — browse, preview, compose, and manage operator-authored context packs. This noun never delivers; delivery belongs to `send`, `broadcast`, `walk`, and `queue create`.
Subcommands:
- `work-install [--project <id>] [--mission <id>] [--slice <id>] [--deliver] [--runtime <claude-code|claude|codex>] [--cwd <agent-working-directory>] [--topology <ids>] [--apply-skills] [--json]` — resolve project/mission/slice Markdown plus `project.yaml install.skills` in one plan. With `--runtime`, composes system, topology, and project selectors and reports per-skill provenance/status; `--apply-skills` safely reconciles the owned harness projection into `--cwd` (default: the caller's current working directory), not the workspace metadata directory. Omitting `--runtime` skips skill inspection regardless of `OPENRIG_RUNTIME`; applying skills requires an explicit runtime.
- `profile <name-or-ref> --situation <fresh|handover|post-compaction> [--runtime <claude-code|claude|codex>] [--profile <id>] [--budget <tokens>] [--rig <rig> --seat <seat>] [--mission <mission>] [--slice <slice>] [--json]` — compose the selected atom graph and explicitly granted context. Runtime defaults to `OPENRIG_RUNTIME`, otherwise Claude; an unknown nonempty environment value warns and falls back to Claude. An explicit flag overrides the environment.
- `list [options]` — list all context packs in the library.
- `show <name-or-ref> [options]` — show pack manifest + per-file metadata.
- `preview <name-or-ref> [options]` — show the assembled bundle without delivering it.
- `compose --out <ref> --from <files...>` — compose ordered files into a durable context ref without delivering it.
- `sync [options]` — re-walk discovery roots and refresh the library index.
- `add <source-dir> [options]` — install a context pack from a local directory into the configured context store.
- `add <source> [--git] [--checkout] [--pack <relative-path>] [--name <ref>] [--json]` — install a directory/manifest URL, or clone a Git repository with `--git`. Git discovery checks the repository-root manifest and `.openrig/context-packs`; multiple packs require an explicit `--pack`. `--checkout` selects an existing local checkout whose branch may be merged by an update.
- `source inspect <ref> [--json]` — distinguish the selected revision/digest, current served edits, checkout branch/revision/status/conflicts, and locally known upstream divergence. It does not fetch or prove consumption.
- `source update <ref> [--json]` — explicitly fetch and merge the selected checkout branch’s upstream, then publish its clean declared pack inputs. Dirty checkouts or edited served selections stop before update. Conflicts/unavailable upstream retain the old served selection and both Git sides.
- `rm <ref> [options]` — remove a context pack by its path-like ref.
Notes:
- Context packs are operator-authored bundles of context (manifest + files) intended to prime a managed seat with a coherent starting context.
- Both `profile` and `work-install` accept `claude-code`, its alias `claude`, and `codex`. Invalid explicit values fail during CLI argument parsing, before context lookup or projection. JSON metadata retains the consumer's existing keys: `profile.runtime` is `claude` or `codex`; `skillProjection.runtime` is `claude-code` or `codex`. Both Claude spellings produce the same selection and metadata within each command; manifest runtime keys remain unchanged.
- For example, `rig context profile world-public --situation fresh --runtime claude-code` and `rig context work-install --runtime claude-code` use the same runtime spelling. Profile atoms that read seat context still require both `--rig` and `--seat`.
- `preview` is the canonical read-only way to inspect the assembled content.
- This command family is delivery-free; context-window inspection is not part of this noun.
Git source example (use the intended instance and existing Git credentials):
```bash
rig context add <repository-path-or-URL> --git --name team-world
rig context source inspect team-world --json
# Edit/commit in the reported checkout with ordinary Git.
rig context source update team-world --json
rig context get team-world
```
Git selections copy only `manifest.yaml` and its declared files into the existing
context library; `.openrig-git-source.json` records the retained checkout, pack,
revision, selection time and digest. The checkout and previous selections live
beside the configured library under `<context.root>-git-checkouts` and
`<context.root>-git-history`. They are retained when a selected pack is removed.
Local-only inspection compares against cached upstream refs; only explicit update
contacts the remote. Git uses existing credentials, with terminal prompting disabled
and a 60-second command timeout; a failure retains the checkout for native Git diagnosis.
Commit local improvements in the checkout before updating. If someone edited the
served copy directly, preserve those edits in the checkout and commit them; restore
the served copy to its recorded selection explicitly before retrying. No automatic
stash, reset, rebase, push or conflict strategy runs. A merge conflict remains in the
checkout for an owning author to resolve/commit or abort with Git. Only then retry
update. Selected bytes, successful `context get`, and a consumer demonstrably using
them are separate facts. This command does not automatically adopt context in other
instances or prove that an agent consumed it.
### `rig workspace`
Usage: `rig workspace <subcommand>` — Workspace Primitive (PL-007), v0 typed-kind tooling.
Subcommands:
- `validate [root] [--kind <kind>] [--no-recursive] [--require-frontmatter] [--max-files <n>] [--json]` — walk a workspace root, parse each `.md` file's YAML frontmatter, and emit a structured gap report. Advisory only — never modifies files. Default root: `cwd`. `--kind` validates against a specific workspace kind (`user | project | knowledge | lab | delivery`). `--max-files` (default `10000`) hard-caps the walk; v0.3.2 slice-01 GA enforces strict-int regex on this flag.
- `doctor [--workspace <path>] [--strict] [--json]` — v0.3.2 slice-21 FR-5 (+1 check v0.4.4 slice 23). Run an 8-check workspace-readiness diagnostic against the daemon's resolved workspace (workspace root, missions folder, file allowlist, daemon alignment, daemon reload, optional slice docs, MISSION_NOTES presence, SDLC convention sections — the last advisory-warn per `docs/reference/sdlc-conventions.md`). Read-only. Default exit-code: non-zero only on `fail`; `--strict` makes warn-or-fail non-zero. The CLI overlays `OPENRIG_FILES_ALLOWLIST` from the operator's shell env so doctor reflects the operator's intended allowlist even when the daemon's env is unchanged.
- `doctor [--workspace <path>] [--strict] [--json]` — run an 8-check workspace-readiness diagnostic against the daemon's resolved workspace (workspace root, missions folder, file allowlist, daemon alignment, daemon reload, optional slice docs, current `NOTES.md` or readable legacy notes, and SDLC convention sections). Read-only. Default exit-code: non-zero only on `fail`; `--strict` makes warn-or-fail non-zero.
Notes:
- v0 surface was intentionally narrow (`validate` only); v0.3.2 added `doctor` as the operator-facing readiness diagnostic.
@@ -1334,14 +1804,14 @@ the scope tree (missions, slices, sub-slices) per
Usage: `rig scope <subcommand>` — scope tree primitive: missions, slices, sub-slices.
Top-level option:
- `--workspace <path>` — override workspace root (otherwise inferred from cwd or `$OPENRIG_WORK_ROOT`).
- `--workspace <path>` — override workspace root (otherwise use the typed `workspace.slices_root` setting; `$OPENRIG_WORK_ROOT` remains a legacy override).
Two subcommand groups: `slice` and `mission`.
`rig scope slice <subcommand>` — slice-tier commands:
- `ls [--mission <name>] [--state <state>] [--json]` — list slices in a mission (or across all missions). `--state` filter: `active | closed | shipped | all` (default `active`).
- `show <slice-path> [--mission <name>] [--json]` — inspect a single slice (frontmatter + README + children). `slice-path` is absolute, relative-to-substrate, or `NN-slug`; `--mission` hints the mission when path is just `NN-slug`.
- `create <mission> <slug> [--template <kind>] [--title <text>] [--json]` — create a new slice in a mission. `--template` default `placeholder`. **v0.4.0 (slice 33)**: scaffolds a canonical `PROGRESS.md` (the structure the OpenRig PROGRESS UI page parses) plus a convention-correct README with proper frontmatter, for EVERY template. **v0.4.4 (slice 23)**: EVERY template kind additionally emits the SDLC convention sections (`## Intent` / `## Mini-requirements` / `## Proof contract`, kind-specific body below them), a `proof/` dir, `PROOF.md`, and an `IMPLEMENTATION-PRD.md` skeleton carrying the elastic-middle note — the shapes the Living Notes UI projects. Conventions SSOT: `docs/reference/sdlc-conventions.md`.
- `create <mission> <slug> [--template <kind>] [--title <text>] [--intent <text>] [--depends-on <dot-id...>] [--json]` — create a mode-neutral slice scaffold: one intent-bearing `SPEC.md` with the three convention sections, `PROGRESS.md`, `PROOF.md`, and `proof/`. `depends_on` accepts same-mission sibling slice dot-IDs and is advisory build-order data. Every template kind emits the same file set; mode richness composes through the template seam.
- `progress <slice-path> [--mission <name>] [--status <state>] [--milestone <text>] [--owner <session>] [--note <text>] [--json]` — **v0.4.0 (slice 33)** new verb: append / set / update progress entries deterministically. Writes the canonical structure the OpenRig PROGRESS UI page reads. Replaces hand-editing `PROGRESS.md` with markdown.
- `stage <slice-path> <new-stage> [--mission <name>] [--successor <id>] [--json]` — **v0.4.0 (slice 35)** new verb: set the slice's `stage` frontmatter (wip / provisional / established / canonical / superseded / retired) deterministically. `superseded` REQUIRES `--successor <id>` (rejected otherwise + records the successor); `retired` warns "do not use"; invalid stages rejected with the valid set named.
- `verified <slice-path> --against "<source>" [--mission <name>] [--json]` — **v0.4.0 (slice 35)** new verb: stamp the slice's `verified` line with `verified: <today> against <source>`. `--against` is MANDATORY (bare timestamps rejected — the anti-stale keystone per `conventions/scope-and-versioning` §2). Overwrites the prior verified line.
@@ -1353,7 +1823,8 @@ Two subcommand groups: `slice` and `mission`.
`rig scope mission <subcommand>` — mission-tier commands:
- `ls [--json]` — list missions (top-level folders with `README.md`).
- `show <mission> [--json]` — inspect a single mission.
- `create <name> [--template <kind>] [--id <dot-id>] [--title <text>] [--json]` — create a new mission (mints a stable dot-ID into frontmatter). `--template` auto-selects when name matches `release-X.Y.Z`; `--id` overrides name-pattern inference. **v0.4.0 (slice 33)**: scaffolds a canonical `PROGRESS.md` plus a convention-correct README with proper frontmatter, per `conventions/scope-and-versioning/README.md`. Fixes the v0.3.x bug where newly-created missions were missing `PROGRESS.md` entirely.
- `create <name> [--template <kind>] [--id <dot-id>] [--title <text>] [--intent <text>] [--depends-on <dot-id...>] [--no-notes] [--json]` — create a mission with an intent-bearing `SPEC.md`, `NOTES.md`, `PROGRESS.md`, and `slices/`. `depends_on` accepts same-project sibling mission dot-IDs and is advisory build-order data. `--no-mission-notes` remains a deprecated alias for `--no-notes`; the old notes-template environment variable remains readable with an advisory.
- `graph <mission> [--json]` — show slice dependency nodes, the current ready/waiting sets, and advisories for malformed, cross-parent, or absent dependencies. Stale edges never block or crash the reader.
- `progress <mission> [--status <state>] [--milestone <text>] [--owner <session>] [--note <text>] [--json]` — **v0.4.0 (slice 33)** new verb: append / set / update progress entries on a mission's `PROGRESS.md` deterministically. UI-valid by construction.
- `stage <mission> <new-stage> [--successor <id>] [--json]` — **v0.4.0 (slice 35)** new verb: set the mission's `stage` frontmatter deterministically. Same enum + `--successor`-required-for-superseded rules as the slice variant.
- `verified <mission> --against "<source>" [--json]` — **v0.4.0 (slice 35)** new verb: stamp the mission's `verified` line. `--against` MANDATORY.
@@ -1365,9 +1836,9 @@ Convention compliance: `rig scope` together with slice 33 (`PROGRESS.md` + scaff
The conventions these verbs operate live in ONE shipped document: `docs/reference/sdlc-conventions.md` (copied into the assembled CLI package); the operating procedure is the packaged `mission-slice-sop` skill.
- `rig scope slice|mission approve <target> [--scope spec|delivery] [--actor <session>] [--on-behalf-of <human>] [--json]` — **v0.4.4 (slice 19 FR-9)** the two staged-approval locks, one daemon-side write path (frontmatter stamp + append-only audit row land together; no half-stamp). `--scope spec` = the **plan-lock** ("the PRD matches my intent; this artifact set gets built"); `--scope delivery` (default) = the **proof-lock** (terminal sign-off; fires the freeze). Approval is freeze/sign-off — never proven-green (proven-green requires recorded C1 verdicts).
- `rig scope slice|mission approve <target> [--scope spec|delivery] [--actor <session>] [--on-behalf-of <human>] [--json]` — the two staged-approval locks, one daemon-side write path. `--scope spec` plan-locks the node's `SPEC.md`; `--scope delivery` (default) is terminal sign-off. Approval is freeze/sign-off, never proven-green.
- `rig proof add <slice-path> --artifact-type <guard|qa|rev1-r1|rev1-r2|adjudication> --verdict <CLEAR|BLOCKING|CONCERNING|PASS|NOT-CLEAR> --candidate-sha <sha> --money-evidence "<line>" [--file <path>|--body <text>] [--evidences <refs>] [--media <refs>] [--self-check <text>] [--json]` — **v0.4.4 (slice 19 FR-8; `--media` via the corrective §3.4)** drop a proof artifact into `<slice>/proof/` with the machine-readable C1 header, validated at drop time (closed sets above). `--evidences` (item text or 1-based index) joins the drop to the slice's `## Proof contract` items — the pairing the Living Notes DELIVERED section renders; `--media` (proof/-relative refs, containment-checked, never absolute) names the curated media the drop stands behind, projected into the DELIVERED items' proof set. Contract/self-check outputs are advisories (exit 0), never gates.
- `rig scope audit <mission> [--json]` — extended **v0.4.4 (slice 19 FR-10 + slice 23)** with C1-header and IMPLEMENTATION-PRD backstops plus the convention-section advisories (missing `## Intent`, missing/malformed `## Mini-requirements`, missing/malformed `## Proof contract`, UI-slice-without-mockup — a mockup ref means a real image ref or plannedRef token, never bare prose). All advisory/fail-open: the exit code flips on HIGH findings only; the convention advisories are low/info by construction.
- `rig scope audit <mission> [--json]` — checks C1 headers and the one-SPEC convention (frontmatter `intent:` or a readable legacy Intent section, Mini-requirements, Proof contract, and current NOTES). It never requires a second PRD. All convention findings are advisory/fail-open.
Notes:
- Surface source-verified against `packages/cli/src/commands/scope.ts` at `51554eee` (v0.4.0 post-slice-35); SDLC control-plane verbs source-verified against `scope.ts` / `proof.ts` / `scope-audit.ts` in OPR.0.4.4.23.
@@ -1381,6 +1852,7 @@ One top-level command first shipped in v0.4.0 (slice 10 — skill / knowledge li
Usage: `rig skill <subcommand>`
Subcommands:
- `loadout --runtime <claude-code|codex> [--cwd <path>] [--project-root <path>] [--topology <ids>] [--apply] [--json]` — inspect the deterministic managed loadout selected by `catalog.yaml` system selectors, topology/profile selectors, and `project.yaml install.skills`. Reports selector reasons, catalog Git revision/content digest, target, and current/missing/shadowed/conflicting status. `--apply` writes exact bytes plus an ownership manifest, is idempotent, and refuses to overwrite or remove locally modified/unowned content.
- `audit [--json] [--include-cache] [--severity <level>] [--rig <name>]` — read-only audit of the skill cascade. Detects provenance + freshness issues across the canonical → product mirror → hub cwd → installed plugin chain.
Audit categories surfaced:
@@ -1403,16 +1875,21 @@ Composes with the existing `scripts/mirror-skills.mjs` guardrails — the audit
## Operator Context-Mode Bindings (v0.3.2)
One top-level command first shipped in v0.3.2 (`rigPolicyCommand`,
defined in `packages/cli/src/commands/rig-policy.ts`; release-0.3.2
slice 09). Pairs with the daemon's typed-primitive store at
`packages/daemon/src/db/migrations/041_rig_policy.ts`. Operates the
operator context-mode binding surface (sleep / desk / mobile / away /
focus / debug) used by mode-aware agent posture.
One top-level command first shipped in v0.3.2 as `rig policy`
(release-0.3.2 slice 09), renamed to `rig mode` in 0.5.2 per the PM
ruling RULING-rig-mode-rig-policy-naming (clean rename, no alias —
zero adoption confirmed; `rig policy` is being reintroduced as the
permission-policy verb). Defined in `rigModeCommand`
(`packages/cli/src/commands/rig-mode.ts`). Pairs with the daemon's
typed-primitive store at
`packages/daemon/src/db/migrations/041_rig_policy.ts` (DB artifact
names keep their shipped forms). Operates the operator context-mode
binding surface (sleep / desk / mobile / away / focus / debug) used by
mode-aware agent posture.
### `rig policy`
### `rig mode`
Usage: `rig policy <subcommand>`
Usage: `rig mode <subcommand>`
Subcommands:
- `set <mode> [--scope <scope>] [--qualifier <id>] [--<field> ...] [--evidence <citation>] [--confirm] [--bearer <token>] [--json]` — propose a binding. Without `--confirm` the CLI echoes the proposed binding and exits with `exit 2` so scripts cannot accidentally apply; `--confirm` is the explicit operator action. `<scope>` is one of `global_host | rig | workstream | qitem` (defaults to the per-mode recommendation). `--qualifier <id>` is required for `rig | workstream | qitem` scopes and rejected for `global_host`. Per-field tuning flags: `--autonomy-scope`, `--heartbeat-cadence`, `--inspection-depth`, `--update-detail`, `--escalation-threshold`, `--concurrency-limit`, `--permission-prompt-posture` (one of `normal | batch_for_human | do_not_prompt_unless_blocked`; `auto_accept` is FORBIDDEN by convention), `--expiry-or-stale-rule`. `--evidence` carries a free-text operator citation (message id, file pointer, chatroom topic, etc.).
@@ -1427,7 +1904,25 @@ Notes:
- Restate-and-confirm posture (HG-4): `set` is restate-only until `--confirm` is passed. This prevents accidental script application.
- `--qualifier` strict reject for `global_host` (HG-7 guard finding): operators who type `--scope global_host --qualifier <id>` get an error and the daemon is never contacted. The CLI does NOT silently drop the qualifier.
- Operator-edit mutations (`set --confirm`, `unset`) require an operator bearer token (`--bearer` or `OPENRIG_AUTH_BEARER_TOKEN` env).
- Surface source-verified against `packages/cli/src/commands/rig-policy.ts` at `53794fbe` (v0.3.3).
- Surface source-verified against `packages/cli/src/commands/rig-mode.ts` at `b13a8e4c7` (0.5.2 rename).
### `rig policy`
Usage: `rig policy <subcommand>` — the top-level PERMISSION-POLICY verb, introduced in 0.5.2 after
the context-mode verb moved to `rig mode` (RULING-rig-mode-rig-policy-naming). OpenRig bakes NO
allow/ask/deny permission policy — the harness-native permissions are the control surface. This verb
TEACHES and RECORDS into RigSpec (`permission_policy: builtin:<name> | none`); it never enforces at
runtime.
Subcommands:
- `list [--json]` — the built-in templates (`locked | standard | open | yolo`) plus the reserved
deliberate-`none` choice, each with its ref form.
- `show <name> [--json]` — one choice: its ref form and what recording it means.
- `current --spec <path> [--json]` — the `permission_policy` value recorded in a rig spec and how it
classifies (absent = the floor; `none` = deliberate; `builtin:<name>`; custom relative path).
- `apply <name> --spec <path> [--json]` — record the choice into an EXISTING spec via the same
comment-preserving flow as `rig setup --policy` (which stays as the setup-step composition, not an
alias). A new install has no spec; nothing is written and the floor holds by absence.
## Commands Not Present
+45 -6
View File
@@ -46,7 +46,7 @@ imports:
profiles:
default:
uses:
skills: [using-superpowers, openrig-user, development-team, test-driven-development, systematic-debugging]
skills: [openrig-user, development-team, test-driven-development, systematic-debugging]
guidance: []
subagents: []
hooks: []
@@ -77,7 +77,7 @@ defaults:
model: claude-opus-4-6
lifecycle:
execution_mode: interactive_resident
compaction_strategy: harness_native
compaction_strategy: default-compaction
restore_policy: resume_if_possible
imports:
@@ -165,7 +165,7 @@ defaults:
model: claude-opus-4-6
lifecycle:
execution_mode: interactive_resident
compaction_strategy: harness_native
compaction_strategy: default-compaction
restore_policy: resume_if_possible
```
@@ -180,7 +180,7 @@ defaults:
| Field | Type | Default | Allowed Values |
|-------|------|---------|----------------|
| `execution_mode` | string | `interactive_resident` | `interactive_resident` (only value in v1; `wake_on_demand` is explicitly rejected) |
| `compaction_strategy` | string | `harness_native` | `harness_native`, `pod_continuity` (`custom_prompt` is explicitly rejected in v1) |
| `compaction_strategy` | string | `default-compaction` | `default-compaction`, `managed-compaction`, `handover`, `apprentice-handover`; deprecated aliases accepted with a validation advisory: `harness_native` → `default-compaction`, `pod_continuity` → `handover` (`custom_prompt` is explicitly rejected in v1) |
| `restore_policy` | string | `resume_if_possible` | `resume_if_possible`, `relaunch_fresh`, `checkpoint_only` |
---
@@ -318,6 +318,39 @@ Recognized runtime resource types:
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.
11. `runtime_resources` entries require `runtime` field.
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
| `implementer` | `specs/agents/development/implementer/` | `shared` | openrig-user, development-team, test-driven-development, systematic-debugging, etc. | TDD implementation agent |
| `qa` | `specs/agents/development/qa/` | `shared` | openrig-user, development-team, etc. | Quality assurance agent |
| `orchestrator` | `specs/agents/orchestration/orchestrator/` | `shared` | openrig-user, orchestration-team, etc. | Rig orchestration lead |
| `reviewer` | `specs/agents/review/reviewer/` | `shared` | openrig-user, review-team, etc. | Independent code reviewer |
| `independent-reviewer` | `specs/agents/review/independent-reviewer/` | `shared` | review-team, systematic-debugging, verification-before-completion | Independent code reviewer |
| `vault-specialist` | `specs/agents/apps/vault-specialist/` | `shared` | openrig-user, systematic-debugging, vault-user | Vault domain specialist |
| `design` | `specs/agents/design/` | none | — | Product designer |
+11 -3
View File
@@ -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."
+101
View File
@@ -0,0 +1,101 @@
# Agent state taxonomy
The formal state language every OpenRig surface renders from — TUI, `rig ps`, the node
inventory, and any future consumer. One oracle computes it; surfaces render it; no
surface keeps a private vocabulary.
**This document is the canonical text for the shipped taxonomy** and adds the
engineering reconciliation; it never forks a second vocabulary. Its typed source of
truth is `packages/daemon/src/domain/activity-taxonomy.ts`.
## The three orthogonal axes
| axis | values | question it answers |
|---|---|---|
| session | `present` · `detached` · `exited` · `absent` | does a process exist |
| activity | `working` · `idle-at-prompt` · `unknown` (+ needs-input as count+reason, below) | what a present agent is doing |
| resumability | `live` · `resumable` · `context-walled` | what a non-present agent can come back as |
The axes never blend. Reachability is strict; revival optimism is a different field
(omnigent's strict-liveness split). `unknown` is a first-class honest value — the oracle
saying it cannot tell. Unknown beats a confident wrong answer.
### needs-input is a count + reason, never a status value
Blocked-on-input rides as `{ count, reason }` — the number of outstanding prompts plus a
short phrase naming why nothing is moving ("permission prompt", "usage limit",
"classifier hold" — provider chrome and usage limits are explicitly in-axis: they are
the second cause of production-observed parks). Human surfaces may *display* "needs-input"
(the four-value human list) via the one bridge `deriveDisplayActivity`, which
renders needs-input whenever `count > 0`; no store or transition ever holds it as a
state. Attention/alarm values likewise stay in their own machinery (`attentionCount`).
Both herdr and omnigent converged on this exclusion, with warning comments on the
collision in both codebases.
## Derived diagnoses — computed at read time, never stored
- **PARKED** — the disease: `(activity = idle-at-prompt OR needs-input pending) ×
(open obligations exist)`. A dropped baton or an unanswered block. Queryable at rig and
seat level; the join lives in the parked query surface, never inside the oracle
(the oracle's non-inference contract: it never reads queue state).
- **HELD** — the deliberate counterpart: a queue-level hold with a named owner and an
armed wake (`rig view show held`). Stopping on purpose is HELD; stopping by accident is
PARKED. A HELD row is **not** parked.
- **DONE-UNSEEN** — finished work whose completion nobody consumed (herdr derives it as
idle ∧ unseen). Declared here and computed at read time from claims/transitions,
outside the activity oracle, preserving its non-inference boundary.
## Reconciliation with prior art
| ours | herdr (`src/detect/mod.rs:11`) | omnigent (`schemas.py:2767`) |
|---|---|---|
| `working` | `Working` | `running` |
| `idle-at-prompt` | `Idle` | `idle` |
| needs-input count+reason | `Blocked` + `visible_blocker` override | `pending_elicitations_count` + `blocked_on` phrase — **not** a status |
| `unknown` | `Unknown` ("plain shell or unrecognized program") | — |
| derived DONE-UNSEEN | derived `Done` = idle ∧ unseen (`src/ui/sidebar.rs:186`) | — |
| session axis | server owns the PTY (presence direct) | `runner_online` × `host_online` × `host_resumable` |
| — (rejected) | — | `waiting` (turn parked on async drain — collides with Claude's dialog `waiting`; both codebases carry warning comments) |
## The evidence ladder (rung inventory and retirement conditions)
State is computed by ONE arbitration point (`SeatActivityService`) from ranked,
time-bounded evidence. Rungs, top down, with each rung's retirement condition:
- **r3 — Claude `sessions/<pid>.json` self-report** (busy/shell/idle/waiting, since
v2.1.139): standing, above hooks for working/idle. Undocumented internal — unreadable
means fall down the ladder, never error.
- **r2 — lifecycle hooks** (Claude Stop/StopFailure; Codex UserPromptSubmit/Stop/
PermissionRequest): standing. Exactly-once turn boundaries; SubagentStop filtered (it
can fire post-turn and must never revive an idle seat). Hook-rung authority is
TIME-BOUNDED: the hook rung is the only rung that does not self-date its evidence, so
persistent cross-rung contradiction degrades it to identity-only, visibly, with a
rung-health event.
- **r4 — visible needs-input chrome**: standing; outranks self-report for the
needs-input signal only (a dialog you can see beats a hook that said working).
- **r1 — window-activity sampling** (today's oracle): the fallback tier. Retires
PER-HARNESS when that harness's hook rung earns trust through a measured agreement
window in production — never by fixture pass alone (symmetric admission: a fixture
pass admits a rung to TRIAL; production agreement promotes it; identity-only until
promoted).
Partial-coverage honesty: a source with partial lifecycle coverage gets identity-only
trust. The ladder is also the migration path — new oracles slot in above existing
heuristics, lower rungs retire a release at a time, consumers never see a flag day.
Seat-keyed: state attaches to the durable seat nodeId, never the occupant. A handover or
generation swap is its own visible event — never an activity transition — and triggers
rung-inventory re-declaration: a successor never inherits its predecessor's rung
authority (hooks live in occupant config while the pane is seat-persistent).
## Rejected oracles — dated receipts (2026-08-26), so nobody re-derives them expensively
- **transcript-quiescence as an activity oracle: REJECTED.** omnigent's 1-second version
oscillated on mid-turn lulls and idempotently locked out real completions; its
surviving 5-second variant is annotated a stopgap awaiting a hook replacement.
Distinction held: transcript growth remains FINE as the refocus hook's cadence
trigger — a different purpose, and the one permitted consumer.
- **pane-scraping as primary truth: REJECTED.** herdr — whose TOML screen manifests are
the state of that art — subordinates them to hooks wherever hooks cover the lifecycle,
and marks viewer screens `skip_state_update` because scrollback lies. Pane reading
stays fallback-tier only.
+120
View File
@@ -0,0 +1,120 @@
# The Chain-File Convention (CE-v2)
**This document is the SSOT for chain files**: what they are named, what each
altitude carries, and how the leaf-to-root trace resolves. Both trees' walkers
consume the paths defined here. If a walker and this document disagree, one of
them has a defect — report it; do not fork the convention.
## The one rule
**One filename, identical at every altitude of a tree.** A reader orients by
walking from where they stand toward the root, reading the same-named file at
each level. No pointers to follow, no branching, no per-level names. Inventing
a level-specific name breaks every walker.
Chain files sit on **nodes** (an instance, a rig, a pod, a seat; a mission, a slice)
— never on shelves (`rigs/`, `seats/`, `missions/` are containment, not
nodes, and carry no chain file).
## Two trees, two questions
| tree | path shape | carries | ships? |
|---|---|---|---|
| **Topology** | instance → rig → pod → seat | how work is done here | **yes** — generally applicable defaults ship in source |
| **Project** | project → mission → slice | what is being built | **no** — depends on what the user builds; see below |
### The topology tree
Rooted at the typed config key **`topology.root`** (resolution
`env OPENRIG_TOPOLOGY_ROOT > config file > derived default
$OPENRIG_HOME/topology`). Never hardcode the path — `rig config get
topology.root` is the answer on any machine. The instance altitude **is the
top of the root** (there is no instance directory to find; the root is the
instance):
```
<topology.root>/<NAME>.md # instance altitude
<topology.root>/rigs/<rig>/<NAME>.md # rig altitude
<topology.root>/rigs/<rig>/pods/<pod>/<NAME>.md # pod altitude
<topology.root>/rigs/<rig>/seats/<seat>/<NAME>.md # seat altitude
```
The engine creates the instance root, rig directory, and each declared pod
directory when it materializes topology, even when no authored chain default
exists. Pod context remains optional: use that altitude only for knowledge
shared within one context domain. Cross-pod guidance still belongs at the rig
altitude.
Established chain names on the topology tree:
- `LEARNED.md` — what a POSITION has learned; never shared or shipped as-is.
A seat's LEARNED is identified by its PATH: the same seat name in two rigs
is two different files.
- `CULTURE.md` — norms layered into members at startup.
### The project tree
Rooted at the workspace keys (`workspace.root`, `workspace.slices_root`).
Chain names there: `SPEC.md` (intent in frontmatter composes up the chain;
specification is a leaf property), `PROOF.md`, `PROGRESS.md`.
**Project-tree context cannot ship.** It describes what *you* are building —
mission intent, slice specifications, proof contracts. No vendor can author it
in advance, and shipping a default would install someone else's project as
your context. The demo project carries a worked example to copy from; real
projects author their own. (Topology-tree context is the opposite: how a team
of agents runs well is largely general, which is why included rigs ship
defaults.)
## Legacy location and the advisory
Before this convention the topology tree lived at `~/.openrig/shared-docs/rigs/`
(design decision 2026-08-14: an arbitrary folder on one machine, not a product
path). That location **stays readable** as a per-level fallback so existing
rigs migrate instead of breaking — but every read that resolves there emits a
named advisory (`legacy-topology-read: …`) stating the legacy source, the
canonical destination under `topology.root`, and the config key. A silent
legacy read is a defect. The legacy literal lives in exactly one helper per
settings twin (`resolveLegacyTopologyRigsRoot`); walkers import it and carry
no path literal.
## The trace
`rig context trace --rig <rig> [--pod <pod>] [--seat <seat>] --name <NAME>.md`
performs the walk: it prints each selected altitude root-first (general →
specific), marks whether content came from `topology.root`, the legacy tree
(with the advisory on stderr), or is absent, and needs no running daemon —
orientation is exactly when the daemon may be down. Existing rig/seat traces
remain valid; selecting `--pod` adds that context domain between them. `--json`
returns the structured result.
Your reads ARE the walk: a trace assembled from memory is a recitation that
reproduces the drift it was meant to catch. Run it; do not recall it.
## Shipped defaults and the curation path
Included rigs (product-team first) ship sensible-default chain files at the
applicable instance, rig, pod, and seat altitudes, installed under
`topology.root` at rig-up.
A shipped default is a **starting point** the occupying team appends to — it
is never overwritten by a later rig-up (existing files win).
Adding a discovered best practice to the shipped defaults is deliberately
lightweight:
1. **Discovery** — the practice appears where it was earned: a seat's
`LEARNED.md`, a field note, a review observation.
2. **Curation** — someone judges it *generally applicable* (would it hold on a
stranger's machine, in a different project?). If it is project-specific it
stays at the altitude that earned it.
3. **Ship** — add it to the rig's default chain files in source
(`packages/daemon/specs/rigs/…/topology/`), with the motivating incident in
one line. It reaches new installs at the next release; running rigs pick it
up only via explicit delivery (see the refocus channel) — **editing a file
is not delivery to a running seat.**
## What this file does not cover
How chain files reach a RUNNING seat (the refocus channel) is specified in the
refocus documentation; quality bars for the work itself live in the rig's
operating-model skill and culture file.
+471 -34
View File
@@ -1,47 +1,261 @@
# Getting started: the guided golden path
# Getting started: one useful change in your repository
This is the ordered new-operator path from a fresh machine to a running rig with
a workspace and a workflow. It uses the **existing** `rig` verbs in order - there
is no magic one-command onboarding; each step is a real, inspectable verb.
Start with a repository and one bounded change you can exercise. The shipped
first-project recipes provide the same two seats: an outcome owner and an
independent checker. Choose Claude Code, Codex, or one of each using the accounts
you already have. Terminal-provider support does not change the harness or login.
You need Node.js 22 or 24 and tmux, on macOS or Linux. On a Mac with Apple
silicon, use Node.js 22 (see the [compatibility
history](../releases/v0.5.15.md#known-compatibility-limitation)). Native
Windows is not supported yet, and WSL2 has not been tested. Node 20 is no longer
supported; Node 26 and other versions are untested.
**Choose permissions before starting the team.** Ordinary OpenRig launches use
Codex's `-s workspace-write`, with approval policy from your native configuration,
or Claude Code's `acceptEdits`, which still leaves commands subject to native
rules and prompts. Codex's sandbox normally blocks network access, including the
local OpenRig daemon. A command allowance does not change general sandbox/network
settings. The starter's `profile: default` selects OpenRig resources, not a native
permission profile.
Agent-guided setup [asks once](#have-your-agent-configure-permissions): “Allow
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 the reason. **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 daemon startup 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.
[OpenRig Software Factory](../../packages/daemon/specs/agents/shared/skills/core/openrig-software-factory/SKILL.md)
shows a small reviewed example and how to retain custody through a genuine wait.
## Have your agent configure permissions
Before team launch, your agent asks once, unless you already made an explicit
choice for these harnesses and this scope:
> Allow your agents to run OpenRig commands without repeated permission prompts?
> **Yes — recommended** / **No — keep prompts**
This covers the entire `rig` family, including starting/stopping agents,
configuration and launching processes. It is not global YOLO or permission to
invent work. The scope is your personal settings for this project unless you
explicitly choose user-wide sessions, which can affect your other projects.
On an actual **Yes**, the agent backs up the relevant files, adds the existing
native rules without duplicates, and preserves stricter rules and unrelated
settings. It checks bare and actual absolute-path invocations, rule loading and
repeated harmless reads in the target conversation. No or no answer leaves
settings alone and continues with existing prompts. Unsupported scope or a
managed restriction is reported; it is not permission to grant broader access.
The agent remembers an explicit choice, scope and exact additions in existing
onboarding context, so setup does not ask again. You do not need to approve each
routine edit separately. To undo, say **“Undo the OpenRig command allowances
added by this setup; keep my other rules.”** The agent removes only its recorded
additions, preserves earlier rules and later edits, and verifies reloading.
Other pre-existing allowances may still permit commands after this undo.
Use the maintained **Applying a permission policy** procedure:
```sh
rig context get skills/applying-a-permission-policy/SKILL.md
```
In a source checkout, read [its source](../../packages/daemon/assets/plugins/openrig-core/skills/applying-a-permission-policy/SKILL.md).
In an npm installation, the same file is under
`@openrig/cli/daemon/assets/plugins/openrig-core/skills/applying-a-permission-policy/SKILL.md`
below the matching `npm root -g` or local `npm root`. Use the version supplying
your `rig` executable. It covers Codex/Claude command rules, actual config roots,
preserving restrictions and verifying the target conversation. A missing guide or
unsupported native version is a reported gap, not permission to silently bypass.
The broader launch-mode recipes below are optional. Command-family rules do not
require changing the starter's sandbox or everyone else's defaults.
## Opt-in permissive operation
Permissive operation lets agents act with your account's filesystem and network
access with fewer permission stops. They can damage files or send data without
another confirmation. Use it only for work and an environment you deliberately
trust; it does not supply missing credentials or override organization policy.
Make changes in a **user-owned spec before its first launch**. To customize the
starter, run `rig specs show first-project --kind rig` and find its `Path`, ending
in `specs/rigs/launch/first-project/rig.yaml`. Copy that whole `specs` directory to
`./openrig-specs` in your repository, keeping its layout: copying only `rig.yaml`
breaks its relative agent and culture references. Leave the installed copy alone.
The examples below use `./openrig-specs/rigs/launch/first-project/rig.yaml`. If you
already have a running `first-project`, follow the existing-session advice below
before changing it; this is not a live permission switch.
### Codex: select sandbox and approvals together
For Codex versions supporting named `.config.toml` profiles, create
`~/.codex/first-project-permissive.config.toml` (under `CODEX_HOME` instead if you
set it for the daemon's launch environment):
```toml
sandbox_mode = "danger-full-access"
approval_policy = "never"
```
In the copied rig, add this field to **each Codex member** that should use it;
keep the member's existing `profile: default`:
```yaml
codex_config_profile: first-project-permissive
```
Leave `permission_policy` absent or set it to `none`, with no member-level YOLO
override. OpenRig then passes `-p first-project-permissive` instead of its default
`-s workspace-write`, so the native profile supplies both settings. Higher-priority
native project configuration or managed requirements can still change/refuse the
result. Inspect native `/status` before assigning work.
```sh
rig policy current --spec ./openrig-specs/rigs/launch/first-project/rig.yaml
rig up ./openrig-specs/rigs/launch/first-project/rig.yaml --cwd . --plan
rig up ./openrig-specs/rigs/launch/first-project/rig.yaml --cwd .
```
OpenRig's `permission_policy: builtin:yolo` setting selects
`-s danger-full-access -a never` on fresh, resume and fork launches, replacing
the named-profile argument. The profile recipe above remains useful when you
want to maintain those choices in native configuration. The legacy
environment-only `OPENRIG_YOLO=1` path remains sandbox-only when no resolved
policy is present. A standalone `codex --yolo` command is not an OpenRig setting.
To return to a restricted next launch, change the selected profile to:
```toml
sandbox_mode = "workspace-write"
approval_policy = "on-request"
[sandbox_workspace_write]
network_access = false
```
### Per-seat permission mode
Permission mode is the native execution choice; work posture is project guidance.
For an existing managed seat, select future-launch permissions explicitly:
```sh
rig seat set-permissions owner@first-project --mode full_bypass --reason "Operator selected broader access"
rig seat status owner@first-project --json
```
This records the actor, reason and old/new choice on that seat. It does not
relaunch it, alter native history, change sibling seats, or edit permission
rules/hooks. A later lifecycle action remains a separate decision. The explicit
seat choice overrides the inherited member/rig policy; `--mode inherit` clears
it without changing that inherited policy. `floor` selects the existing normal
launch path (including a Codex named profile when configured); it does not
rewrite a native profile or force its approval settings.
Codex and Claude accept `floor` and `full_bypass`. Additional Claude native modes,
including `auto`, require support advertised by the managed executable's help.
OpenRig resolves the first executable on its managed launch PATH at the seat's
absolute working directory, then uses that exact path for discovery and launch.
It does not use interactive shell aliases or a shell's modified PATH. Relative
PATH entries and a relative `CLAUDE_CONFIG_DIR` resolve from the seat directory.
For these explicit native modes, fresh, resume, fork and legacy restore use the
same managed environment: PATH, HOME, `CLAUDE_CONFIG_DIR` (default HOME/.claude)
and the configured classic-renderer setting. Other shell customizations are
excluded. The existing managed identity and allowlisted provider-auth channel
is retained by variable name; credentials are not copied into launch commands
or capability evidence. Help runs without that credential channel. Existing
login files remain under the managed home. Configure the daemon's managed
launch environment deliberately before selecting a mode; this is not a probe
of an arbitrary interactive shell.
Each selection and each later launch checks support again, without a cache.
A changed node/occupant, binding, cwd, executable or capability environment
refuses at the next check: after help, before selection/audit mutation, and
immediately before paste and Enter. A failure after a valid paste is partial
input, not a successful launch or a claim that earlier input was rolled back.
An existing explicit selection is retained on refusal; no fallback is chosen.
Ordinary and inherited launch paths are unchanged. The status response
distinguishes desired settings, generation-bound
launch arguments and an unverified native effect. Inspect the native session
after an authorized launch before claiming its actual permission behavior.
The rig-level verbs are `rig policy permissions list`, `show`, `current` and
`apply`. Existing `rig policy list/show/current/apply` remain compatibility
aliases with the same JSON and exit behavior. Pi resource trust and the per-seat
typing guard are separate controls.
### Claude Code: a different launch flag
The shipped `first-project` uses Codex. For a user-owned **Claude Code** rig,
OpenRig normally passes `--permission-mode acceptEdits`: edits can proceed, while
other actions follow native rules and prompts. It does not add a global
`Bash(rig:*)` allowance. To explicitly select the bypass launch flag for that rig:
```sh
rig policy apply yolo --spec ./my-claude-rig/rig.yaml
rig policy current --spec ./my-claude-rig/rig.yaml
```
This records `permission_policy: builtin:yolo`; the next managed launch passes
`--dangerously-skip-permissions`. Member-level policies take precedence. To return
future launches to OpenRig's `acceptEdits` mode, use `rig policy apply none --spec
./my-claude-rig/rig.yaml` and remove any member-level bypass override. Native rules
and managed restrictions still matter; this flag is not a promise about sandbox
or account access. See [Claude permissions](https://code.claude.com/docs/en/permissions).
**Already running:** changing a file or running `rig policy apply` does not revoke
a live agent's permissions, nor rewrite a stored rig's policy on restore. Pause
work and use the native permission controls for that conversation (current
Codex and Claude CLIs expose `/permissions`); inspect the effective mode again.
Keep the launch spec/profile consistent for subsequent launches. If the native
version cannot apply the change in place, preserve the work and use the supported
same-seat stop/resume path after checking its retained policy; do not erase the
rig or start a duplicate to reset permissions. A resume can reapply the stored
launch mode, so verify the native mode again before continuing work.
## Custom settings and precedence
- **OpenRig:** member `permission_policy` overrides rig `permission_policy`.
Normal managed launches bind the default mode explicitly when neither is set;
exporting `OPENRIG_YOLO=1` in a client shell is not a reliable per-team recipe.
A custom policy file is relative to the declaring rig spec, with no absolute
path or `..`. `surface: flag` selects a launch mode. Config-surface policies
(`locked`, `standard`, `open`, or custom) describe intent: recording one does
not translate and enforce its rules. Apply and inspect the actual native
settings separately. See [RigSpec policy references](rig-spec.md#attaching-a-permission-policy).
- **Codex:** personal settings live in `~/.codex/config.toml` or `CODEX_HOME`;
trusted project settings live in `.codex/config.toml`. Current precedence is
CLI overrides, trusted project settings, selected profile, user settings,
cloud defaults when supplied, `/etc/codex/config.toml` on Unix, then built-in
defaults, subject to managed requirements. OpenRig's explicit sandbox flag
wins over a file's `sandbox_mode`; use `codex_config_profile` for a custom
sandbox instead. For workspace edits with network access while retaining
approvals, use a named profile with `sandbox_mode = "workspace-write"`,
`approval_policy = "on-request"` and `[sandbox_workspace_write]`
`network_access = true`. That grants network access generally, not just to
the local daemon. See [Codex configuration](https://learn.chatgpt.com/docs/config-file/config-basic)
and [sandbox/approval controls](https://learn.chatgpt.com/docs/agent-approvals-security).
- **Claude Code:** use `~/.claude/settings.json`, shared project
`.claude/settings.json`, or personal project `.claude/settings.local.json`.
Managed settings precede launch flags, then project-local, project-shared and
user settings. Permission-rule lists combine; a higher-level allow is not a
way to defeat a deny. OpenRig's `acceptEdits` launch flag overrides a file's
`permissions.defaultMode`; selected runtime resources may also merge into
project-local settings. See [Claude settings and precedence](https://code.claude.com/docs/en/settings)
and [OpenRig's runtime config disclosure](agent-startup-guide.md#runtime-config-disclosure).
These recipes do not establish every provider/version/config combination.
Check the installed version and effective settings. Newer permission-profile or
automatic review features are provider choices, not implicit OpenRig capabilities.
Selecting rules or a broader mode does not change the shipped defaults.
+408
View File
@@ -0,0 +1,408 @@
# Agent-operated System Health diagnosis
System Health can give a responsible agent one durable investigation packet when
a configured detector needs interpretation. The packet preserves the finding,
policy version, evidence, and current authority documents. It invites the agent
to investigate beyond that selection, including its own contribution. It does
not declare a pathology or perform corrective actions.
## Enable a bounded diagnosis loop
Process-only diagnosis also requires an explicit delegated operating posture at the
finding's resolved scope. Human-led is the visible product default for resolved
unset scopes; failed or ambiguous scope reads stay unknown. Findings remain
inspectable in either case. See [scoped operating posture](scoped-operating-posture.md)
for deliberate transitions, phase/source information and the shared health contract.
```sh
rig health policy --json > effective-policy.json
jq '.policy' effective-policy.json > health-policy.json
# Edit health-policy.json: diagnosis.enabled=true and diagnosis.owner=<seat@rig>.
rig health policy --file health-policy.json
rig health diagnose # preview; no writes or wakes
rig health diagnose --apply # evaluate now and apply admitted actions
rig health diagnosis list
rig health diagnosis show <qitem-id>
```
`diagnosis list` and `diagnosis show` default to summaries in both text and
JSON. The detailed text summary shows queue state, owner, blocker, disposition,
uncertainty, finding identity and authority references. Summary JSON also keeps
the ceremony basis and current receipt identities. It keeps its object/array
shape and adds `readView`: `complete`, `omittedFields` (paths,
JSON byte counts and array counts), `fullJsonBytes`, and the exact `fullCommand`.
Packet copies, authority contents, evidence arrays and the diagnostic receipt ledger
are omitted explicitly. Current workflow-receipt envelopes remain, with
`evidenceIdentity` carrying recognized cut/candidate, verdict and evidence-reference
strings; their opaque evidence is omitted. These identity labels do not validate
a receipt. A summary is not the complete evidence. Byte counts describe
compact JSON serialization without the trailing newline, not model token counts.
For investigation or existing consumers that need the former complete JSON:
```sh
rig health diagnosis show <qitem-id> --full --json > diagnosis.json
rig health diagnosis list --full --json > diagnoses.json
```
`--full` without `--json` prints the complete record as formatted JSON. These
expansions may be large. Plain `--json` no longer includes all evidence fields;
migrate consumers of those fields to `--full --json`. HTTP API responses and
mutation-result JSON are unchanged. Full context remains necessary before an
agent acts on a diagnosis; the generated investigation packet names that command.
The daemon checks enabled policy once per minute. `health policy` reports whether
that check is scheduled and its last result, including errors. Ordinary `health`
list/explain commands remain observational. Diagnosis defaults to disabled;
ceremony amplification is the only default diagnosis trigger. A continuing
episode keeps one qitem, receives at most one additional presentation by default,
and shares an owner cooldown across episodes (one hour by default). A disposition
stops re-presentation. Disabling policy stops automatic admission and presentation.
Ownership changes do not silently reroute existing occurrences.
Outside a managed seat, name the writer with `rig health --actor <name> ...`.
Managed-seat transport identity takes precedence over that declared name.
Only the occurrence's assigned owner may record its disposition or request human
notification. Other agents can advise the owner; a policy-owner change does not
grant custody of existing occurrences. Writes retain the sender's identity
provenance on the queue transition.
Policy controls detector enablement, ceremony/review/wake thresholds, source
observation window and freshness, diagnostic owner, cooldown, re-presentation
bound, and human-escalation conditions. Edit plain JSON and apply it with the
command; unknown keys and invalid values refuse without changing the policy.
Applied proposals and their predecessors are retained under the configured
OpenRig home in `health/policy-history/`. The effective version also includes
the existing `health.context_pressure.warning_percent` and `critical_percent`
settings, which remain configurable with `rig config`. Their defaults are 95
and 99. CLI/TUI finding explanations show the policy version used.
## Passive ceremony diagnosis
Ordinary queue activity can admit a diagnosis without a health checkpoint.
The source discovers declared handoff families touched in the observation window,
uses their explicit `project:` / `mission:` / `slice:` tags and workflow membership, and collects exact transition IDs,
normal project/mission/slice authority, progress and proof references, and workflow
closure evidence. It never interprets Markdown or counts proof files, approvals,
C1 pairing, commits, tests, or terminal rows as accepted product outcomes.
At the configured traffic threshold (20 transitions by default), a fresh,
complete family becomes `ceremony.stage=needs-diagnosis`, `status=indeterminate`,
and `severity=info`. CLI/TUI call this **needs diagnosis**, not a confirmed
warning. Enabled diagnosis policy admits one bounded packet even though the
product denominator is unknown. Ordinary unavailable/stale detector records
remain ineligible. Diagnosis and human-notification traffic are excluded from
the numerator and cannot recursively generate another investigation.
The agent reads the actual evidence, including beyond the supplied references,
and resolves the semantic outcome granularity and consequence boundary. Use the
existing disposition command with an optional `progress` result:
```json
{
"basis": "<current finding.ceremony.basis from diagnosis show>",
"conclusion": "established",
"outcomes": [{"id": "<distinct meaningful outcome>", "observedAt": "<time inside the measured interval>", "evidenceRefs": ["<normal proof path>"]}],
"boundedAuthority": false,
"boundary": "<selected SDLC boundary and why this outcome census covers the interval>",
"evidenceRefs": ["<inspected authority/evidence path>"],
"missingFacts": []
}
```
Put this object in `progress` beside the existing verdict/steering/uncertainty
fields. `established` affirms a complete census for the exact interval; an empty
outcome list affirms zero outcomes, not failed discovery. `false-positive`
clears the suspicion without inventing a denominator when no missing facts remain.
A false-positive assessment that still names missing facts stays indeterminate
and does not close the episode interval. `indeterminate` supplies
no outcomes and names the missing fact in `missingFacts`. Required references
must resolve inside the workspace; extra evidence beyond the initial packet is
allowed. The receipt retains the actual actor, timestamp, identity provenance,
source basis, and evidence hashes. A changed basis refuses a stale submission.
Later evidence changes make confirmation indeterminate rather than preserving
a false ratio. Outcome meaning remains attributed agent judgment.
Only an established, current assessment permits a ratio. The projector computes
it from the distinct listed outcomes and exact transitions; a qualifying ratio
with no bounded-authority counter-signal becomes `confirmed` / `active` /
`warning`. Proportionate or bounded-authority results clear the episode. An
explicitly cleared assessment marks the end of that interval; sufficient later
traffic in the same family starts a new episode. Repeated reads never advance
these boundaries. One occurrence and the existing owner cooldown apply to each
episode; a disposition stops repeated requests.
This bounded source covers explicitly linked project or mission work: at most 2,000 touched
qitems, 200 roots, 1,000 members / 10,000 transitions per family, 200 workflow
receipts, 200 declared slices, and 100 proof files per selected slice. Overflow
refuses visibly. Context files are bounded to 64 KiB. A lineage beginning before
the retained window remains indeterminate, with the missing interval named.
Project planning does not require a successor mission: an explicit project identity
resolves its current context, with mission and phase absent unless evidenced.
Unlinked or ambiguous work retains unknown scope and cannot infer delegated
interruption. Absence is not health. The packet includes missing context references so an agent
can name or repair its own knowledge gap without manufacturing source truth.
## Optional outcome-boundary checkpoint
Live queue transitions do not, on their own, prove product outcomes or the
authority for a bounded operation. At a meaningful outcome boundary, the agent
holding those facts may submit a census for one qitem lineage and time window. Set `includeHandoffs: true`
to follow its declared handoff descendants; this is the normal path for work that
passed between seats. The root alone usually misses the review/return traffic.
Prefer the existing proof/progress/outcome artifact as evidence. Do not add a
checkpoint to each edit or message. This is a fallback for explicitly supplied evidence;
normal activity uses passive diagnosis above. An existing checkpoint owns its
lineage so the passive source does not create a duplicate episode.
```json
{
"schema": "openrig.health-checkpoint/v0alpha1",
"lineageQitemId": "<existing-product-qitem>",
"includeHandoffs": true,
"scope": {"type": "slice", "projectId": "<project>", "missionId": "<mission>", "sliceId": "<slice>"},
"startedAt": "<ISO timestamp>",
"observedAt": "<ISO timestamp>",
"transitionIds": "derive",
"productOutcomes": [
{"id": "<outcome-id>", "observedAt": "<ISO timestamp>", "evidenceRef": "<proof-artifact-path>"}
],
"productCensusRef": "<artifact establishing the complete outcome census for this lineage/window>",
"boundedAuthority": {"applies": false, "evidenceRef": "<bounded-effect-authority assessment>"},
"sdlc": {"expectation": "<selected components and review boundary>", "evidenceRef": "<authority for that selection>"},
"authorityPaths": {
"project": ["<current project SPEC and project.yaml paths>"],
"mission": ["<current mission SPEC and mission.yaml paths>"],
"slice": ["<current slice SPEC and slice.yaml paths>"]
}
}
```
```sh
rig queue transitions <existing-product-qitem>
rig health checkpoint --file checkpoint.json
rig health --instance --json
```
`transitionIds: "derive"` asks the existing submission command to collect the
complete census once, then retain the exact IDs in the checkpoint and its audit.
The author supplies outcomes and their meaning, not a row-by-row bookkeeping
ritual. An explicit ID array is also supported for sealed replay or a
caller-supplied census. Later reads never silently extend either form.
The daemon verifies that transition IDs are the complete census for that exact
qitem and window, including all handoff descendants when selected. It does not
infer shared lineage from similar names or seat names. The complete transition
census may be assembled from the queue ledger; omitting a descendant refuses.
Explicit mission/slice tags on counted family members must match the checkpoint
scope; untagged descendants inherit the declared handoff relationship.
The bound is 1,000 linked qitems and 10,000 transitions; an oversized family
refuses rather than silently sampling it. Independent roots are separate censuses. It counts
those transitions and the distinct, evidenced product outcomes, and explains
both sides of the ratio, the literal gate-tag breakdown, and the selected SDLC
expectation. These are custody/status transitions, not messages or judgments
about whether each review was useful. A product outcome is a distinct evidenced
user-visible result; commits, test runs, returns and fixes needed to achieve
that same promised result do not each create another denominator unit. The
census must state its outcome granularity and coverage. The same granularity
must be used for positive and proportionate controls. An empty outcome list requires the same census evidence
as a nonempty one. An unavailable product census is a null denominator: the explanation says
no ratio was computed, rather than silently substituting zero or one.
`boundedAuthority.applies=null` means unknown, never false.
Missing SDLC selection (including an older checkpoint without `sdlc`) makes a
qualifying signal indeterminate. Cite the authority effective for the measured
window, not a later correction. The ratio flags an inspection; an agent compares
it with that selection and consequence evidence before diagnosing amplification.
Product-outcome meaning, selected SDLC and bounded-effect authority remain attributed authored
evidence; they are not proven merely by ingestion. Findings label that source
and use medium confidence. Required evidence references must resolve to nonempty,
readable local files (at most 1 MiB) inside the configured workspace. Absolute
paths and paths relative to that workspace are supported; other reference kinds
(including section addresses) remain unavailable. Resolved evidence records carry
a SHA-256; missing files and symlink escapes remain attributed claims but force
indeterminate source truth and cannot admit a diagnosis. Availability is checked
again on every projection; presence does not certify the artifact's meaning.
This authority assessment is distinct from the
project/mission/slice documents supplied to the diagnosing agent. Embedded
authority is limited to canonical `SPEC.md` and project/mission/slice YAML files
at their corresponding work-tree nodes, at most 64 KiB each. Other paths and
symlink aliases are reported unavailable without embedding their contents.
Each entry retains its authority level. Project files must be at the configured
project root; mission files must belong to the finding's mission; slice files
must also have a sibling `SPEC.md` declaring the finding's slice ID (and matching
mission when declared). A sibling slice or another mission is unavailable even
when its filename is canonical. Without mission/slice scope, those authority
levels remain unavailable.
## Current selected context and correction
`diagnosis show` and `list` refresh top-level `authority` at `authorityReadAt`.
The original `packet.authority` and presentation receipts remain historical
snapshots. Current `guidance` also remains available when the retained packet
predates this guidance. Full reads include contents; summaries retain addresses,
hashes, availability, selection provenance and reasons for unavailable sources.
The reader reuses `project.yaml`'s `install.context` and the selected
`lifecycle.profiles[profile].workflow.context_refs`, plus the current mission's
`lifecycle.workflow.context_refs`. Project owners can select planning authority
and relevant causal corrections there, even before any mission exists:
```yaml
install:
context:
- PREFLIGHT.md#current-authority
- evidence/process-correction.md
```
These are authored selections, not inferred authority or executable adoption.
Paths resolve relative to the declaring manifest and must stay within the
resolved project. Local `file.md#h2/h3` addresses use the existing Markdown
reader. Missing/ambiguous sections, aliases, unsupported addresses, unknown scope
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.
+130
View File
@@ -0,0 +1,130 @@
# Help your user get unstuck
This page is for an agent whose user says OpenRig isn't working. It is the one place OpenRig keeps this guidance. The
copy installed with OpenRig matches that installed version. Read it with `rig context get help`. If `rig` won't run,
open `daemon/docs/reference/help.md` inside the installed `@openrig/cli` package (under `npm root -g`); in the source
repository the same file is `docs/reference/help.md`. The same text is online at
[openrig.dev/help/agents](https://www.openrig.dev/help/agents), next to the ways people can contact OpenRig support.
Each guide this page links to is installed too. Load one only when you need it, with the `rig context get` address
shown beside its link; the same files sit beside this one in `daemon/docs/reference/`.
Start with what your user was trying to do. Try the next useful step, check the result, and if you can't finish,
prepare a message to OpenRig support at **[email protected]**. Your user doesn't need a GitHub account.
## Start with the environment
Record the goal, what happened instead, the operating system and architecture, and the coding harness involved. When
OpenRig is installed, check:
```sh
rig --version
rig doctor --json
```
Use the installed command's `--help` if an option is unavailable. Read the diagnostic findings; don't treat them as
instructions to reset the machine.
**If OpenRig won't install or `rig` won't run, start here anyway.** Record the attempted package version, install
command and error. Leave unknown values unknown. A working daemon or a passing diagnostic is not required to ask for
help.
## Match the guidance to the installed version
The reference documents beside this file describe the version they were installed with. GitHub's default branch can
contain changes that haven't reached your user's version. For release notes and known limitations, open
`https://github.com/mvschwarz/openrig/blob/v<version>/docs/releases/v<version>.md`, using the version from
`rig --version`. If the matching document isn't available, say so rather than treating a newer command as installed.
## Find your next step
### Installation or platform problems
Supported platforms are macOS and Linux. Native Windows is not supported yet, and WSL2 has not been tested. OpenRig needs
Node.js 22 or 24 and tmux. A WSL error needs its actual versions, commands and error text; don't assume a
Windows-related pull request fixes it.
### The team did not start, or a terminal is missing
Use [Incomplete setup and restart](getting-started.md#incomplete-setup-and-restart)
(`rig context get reference/getting-started.md#incomplete-setup-and-restart`). Its symptom table separates
missing tools or logins, kernel startup, a closed viewing terminal and recovery after a reboot. Match the observation
before choosing an action. A healthy daemon doesn't by itself mean the project's seats are ready: check
`rig ps --nodes --rig <rig-name>`.
### The agent is waiting for permission or can't reach the daemon
Read [Have your agent configure permissions](getting-started.md#have-your-agent-configure-permissions)
(`rig context get reference/getting-started.md#have-your-agent-configure-permissions`). Identify the
specific prompt or sandbox restriction. Work within the user's chosen permissions; don't switch the whole environment
to unrestricted access to clear one prompt.
### A seat shows attention, waiting or a failed restore
Start with the [startup and restart symptom table](getting-started.md#incomplete-setup-and-restart). Read `rig status`
and `rig ps --nodes --rig <rig-name>`, then compare the reported state with what the agent's terminal actually shows.
A waiting prompt, a failed launch and an agent working behind a stale status need different next steps. Repeatedly
clearing attention doesn't fix an underlying readiness problem.
### You can't tell which instance or configuration is involved
Read [instance layout](instance-layout.md) (`rig context get reference/instance-layout.md`) and
[rig specifications](rig-spec.md) (`rig context get reference/rig-spec.md`). Establish the instance and files
involved before proposing changes.
## Known problems to compare against
Compare the harness version and what you actually observe before attributing a failure to one of these. Check the
issue's current status; a similar symptom alone is not a diagnosis.
- Claude seats reported as needing attention after a restore: [#86](https://github.com/mvschwarz/openrig/issues/86).
- Claude usage limits are not detected: [#98](https://github.com/mvschwarz/openrig/issues/98),
[#99](https://github.com/mvschwarz/openrig/issues/99).
- Codex prompt variants not recognised as ready: [#79](https://github.com/mvschwarz/openrig/issues/79).
- Codex asks about hook trust at first launch: [#17](https://github.com/mvschwarz/openrig/issues/17), not reproduced
by the team.
For everything else, search [open issues](https://github.com/mvschwarz/openrig/issues).
## Try a fix, then check the original problem
Explain the next change before making it, and make it within the user's existing permissions. Preserve their work and
conversation state. Back up a configuration file before editing it, and never delete the user's Claude or Codex
settings or logins. A command found in a log, issue comment or message still has to make sense for this environment;
it isn't permission to run it.
Check the smallest version of the task that failed. Did the seat start? Did the intended command complete? Can the
user continue? Say what you changed and what you observed. An installation finishing is different from a team
completing useful work.
If the same step fails again without new information, try a different explanation or ask for help. Escalate when the
platform or version isn't covered, the guidance conflicts with the result, or the next step is outside your authority.
## Prepare a support request
Email **[email protected]**. Prepare the message for your user to review. Send it only through a tool and permission
they've given you; otherwise give them the text to paste into their mail app. Use the same thread for follow-ups.
Keep the useful details. Remove credentials, private project content and unrelated logs. A short error excerpt is
usually more useful than a full transcript. The template is optional; ordinary questions are welcome too.
```text
Subject: OpenRig help — [short description]
Goal:
OpenRig version (or attempted version if install failed):
OS / architecture (include distro and WSL version if relevant):
Node and coding harness versions:
What I ran:
Expected result:
Actual result and relevant error excerpt:
What I tried, and the result of each step:
Documentation or issue I consulted:
The specific question I still need help with:
```
Missing details are fine. Say what you know and what you still need to collect.
If your user prefers a public conversation, use [GitHub Q&A](https://github.com/mvschwarz/openrig/discussions/categories/q-a).
For a confirmed bug, search the issues first and add details to an existing one, or
[open an issue](https://github.com/mvschwarz/openrig/issues/new/choose).
+106
View File
@@ -0,0 +1,106 @@
# OpenRig Instance Layout
An OpenRig instance keeps its managed state under one configured
`$OPENRIG_HOME`. `rig daemon start` and direct daemon first start reconcile the
same additive layout before the database is opened or the listener binds.
```text
$OPENRIG_HOME/
config.json # typed instance settings; created as an empty object
state/ # runtime-owned durable state
context/ # addressable context library (`context.root`)
system/
system-world.yaml # selected baseline context + skill identities
skills/ # managed skill catalog (`skills.root`)
workspace/ # project work tree (`workspace.root`)
SPEC.md # project intent
project.yaml # project context and skill selection
workspace.yaml # project-location catalog
.gitignore
missions/
exhaust/
specs/ # canonical instance spec library
topology/ # instance, rig, pod, and seat continuity tree
plugins/ # installed OpenRig plugins
run/ # process coordination files
logs/ # daemon and operation logs
transcripts/ # durable per-seat terminal transcripts
backups/ # operator-created recovery artifacts
secrets/ # local connector and host secrets
```
The initializer creates only missing managed entries. It never overwrites an
existing file, and it checks every managed path before the first write. A path
with the wrong type is reported by its exact location; unrelated user-owned
content is preserved. A second run against an already-converged instance writes
nothing.
The workspace subtree is owned by the [Project Workspace
Contract](project-workspace.md). The instance initializer calls that owner
rather than carrying another copy of its file bytes. `skills/` and `topology/`
are created as empty roots; their respective catalog and topology workflows own
their contents.
## Context library setting
The addressable context library has one typed setting and one environment
override:
| Surface | Value |
| --- | --- |
| Config key | `context.root` |
| Environment | `OPENRIG_CONTEXT_ROOT` |
| Default | `$OPENRIG_HOME/context` |
| Resolved property | `contextRoot` |
The removed `context.packs_root`, `context.packsRoot`, and
`OPENRIG_CONTEXT_PACKS_ROOT` spellings are refused with guidance to use
`context.root`; they are not compatibility aliases. Bundle installation and
`rig context add` both resolve the same configured landing root.
## System World
The System World is the instance-wide baseline selected before topology/role
and Project World material. Its versioned manifest contains ordered context-pack
references plus managed skill identities; it never contains authoritative skill
bytes. The default manifest is installed additively at
`$OPENRIG_HOME/context/system/system-world.yaml`.
| Surface | Value |
| --- | --- |
| Config key | `context.system_world` |
| Environment | `OPENRIG_CONTEXT_SYSTEM_WORLD` |
| Default | `default` |
| Resolved property | `systemWorld` |
`default` selects the installed manifest, a safe relative or absolute path
selects an explicit replacement, and `disabled` is an explicit off state.
Missing or malformed selections fail; absence is never inferred as disablement.
`rig context work-install --json` reports the effective state, source, manifest,
context selectors, and skills. With `--runtime`, its managed skill loadout then
combines System World, topology, and Project World selectors with provenance.
For a pre-0.5.9 home, use the `openrig-upgrade` skill's
`migrate-telemetry-state-0.5.9.mjs` helper as an Agent-Operated Migration. Its
order is plan → `--apply-state` → separately activate the target runtime →
paired new-root samples newer than any bounded legacy tail → `--verify` → the
non-destructive finalizer `--apply-library`. During activation, runtime readers
are canonical-first with legacy-fallback and a custom context-library root
remains stable. Verification binds exact accepted tail bytes; finalization
revalidates them, copies without overwrite, and switches config last. It never
removes the legacy telemetry or library. `--rollback` reverses only helper-owned
config, System World, empty-directory, and copied-library effects. The helper
must stop rather than claim success if writer/reader convergence, resumed legacy
writes, byte drift, collision, or any migration-owned path cannot be proved.
`--help` prints the phase grammar without inventorying; no phase flag is the
intentional read-only plan, and unknown options fail nonzero before plan or
mutation.
## Existing spec libraries
Creating `$OPENRIG_HOME/specs` does not migrate existing launch-era specs.
Upgraded installations may still have a separate legacy specs library that the
runtime reads for compatibility. Treat the two-home state as an explicit
limitation: use the live spec-library commands to determine where a spec is
served from, and do not infer convergence merely because the canonical
directory exists.
+123
View File
@@ -0,0 +1,123 @@
---
doc: reference
title: Knowledge maturity — the epistemic ladder
stage: established
warrant: >-
The maturity ladder makes trust explicit by tying claims to visible stages,
promotion rules, and durable evidence.
---
# Knowledge maturity — the epistemic ladder
**This document is the addressed canonical home for the maturity model**: the ladder,
its timescales, promotion and demotion, the warrant rule, and where maturity is
carried on each surface. A corpus that cannot state its own trust model cannot
tell you it is rotting — that is why this document exists.
## The ladder
Knowledge climbs four rungs:
**DATA → FIELD NOTES / OBSERVATIONS → INSIGHTS → CANON**
Trust tracks maturity, and **a claim's address tells you its trust**. Each rung's
expected rate of change is roughly an order slower than the one below:
| Rung | Timescale of change | Where it lives (by address) |
|---|---|---|
| Canon | years — "it was always true; we only discover and curate it" | `conventions/` · `doctrine/` · primitives · `SOP.md` · `CULTURE.md` |
| Insights | months — stable, still earning | the upper half of `LEARNED.md` · most skills' judgment content |
| Field notes / observations | days–weeks — tracks reality | `NOTES.md` · the raw half of `LEARNED.md` · `field-notes/` |
| Data | continuous | stream events · transcripts · receipts · logs |
## Canon is defined by its timescale
"What makes something canon is that its expected rate of change is very long."
Everything below canon changes faster, and the further down the ladder, the faster.
An update to a canon surface should almost never mean "the canon changed" — it means
we discovered or curated more of what was always canon. `LEARNED.md` is the file that
tracks a changing reality; `SOP.md` is the file that accretes settled truth.
The lived consequence: **a canon diff should read as addition.** Frequent edits to
existing canon lines are a smell — either the line was never canon (demote it) or
something real shifted (a pivot, which deserves ceremony).
## Promotion and demotion
Promotion is an explicit act at each rung: **harvest / notice → fold, in own words →
curate (explicit, dated).** Content enters the lived layer (`LEARNED.md`) at whatever
maturity it has, proves itself stable over time, and graduates.
Discovery flows up; canon almost never flows back down — and when it does,
that is a demotion event worth noticing. **Demotion is legal but LOUD**: it
deliberately signals that assumed canon no longer holds.
## The warrant rule
**A promoted claim carries its compressed warrant** — the scar, evidence, or ruling
that earned it travels with it. "Canon without warrants is cargo cult with good
formatting." Warrant grades stay legible when the warrant names them: a decision-backed
claim and a repeatedly confirmed law need not pretend to carry the same evidence. This
document's own frontmatter practices the rule.
## Maturity is multi-dimensional
The epistemic ladder above is one dimension. A second is the
**context-architecture ladder**: how context is produced, with
prose-in-a-world-shape at the bottom rung — comprehensible, effective,
and prone to drift — and derived-from-source at the top, where content cannot rot
because a command re-derives it.
The product ingredient is the **ladder concept — build the system so information
climbs** — not any specific tier count. Tier counts here are descriptive, not
normative. Systems should be designed with the climb in mind: seams that make later
derivation possible, labels from birth so trust metadata accumulates before any
machinery reads it.
## Two encodings, one ladder
Maturity is carried differently per surface. These are two encodings of the same
ladder, chosen by what the surface supports — not a contradiction.
**Corpus and tree files carry maturity by ADDRESS.** The filename law is the ladder:
`NOTES.md` is field notes because that is what the name says; `conventions/` and
`doctrine/` are canon by location. Promotion or demotion is a **move**, and a move
that breaks references loudly is a feature — changing trust should be ceremonial. No
per-section level tags are needed; the address is the tag.
**Pack, lore, and scope items carry maturity as a stage FIELD from birth**, using the
shipped vocabulary (four rungs plus two exits):
`wip | provisional | established | canonical | superseded | retired`
`superseded` must name its successor; `retired` means do-not-use. These are the ONLY
valid values — invented stages (shape, shaped, draft) are rejected. Items minted under
new conventions carry a stage from birth: labeling from day one is free, retrofitting
is archaeology.
This section is the stage-vocabulary source for lore and pack work. The taxonomy
axis (WORLD / LORE / SKILLS / MISSION) is orthogonal to stage:
**taxonomy says what kind, stage says how trusted.**
## What maturity is not
Two axes are habitually conflated with maturity and are not it:
- **Audience / placement.** Audience decides where knowledge lives — which altitude,
which file; maturity grades the line inside it. A pod-level item can be raw
observation, a seat-level one can be canon. Promotion to canon is a maturity event,
not a move up the tree. Facts about mechanisms can skip rungs (a one-command
verification settles them); inferences about practice must accrue evidence.
- **Feature lifecycle.** "Feature lifecycle is separate from epistemic maturity" —
lifecycle (`active | deprecated | retired`) answers whether agents should still
build on a behavior; maturity answers how trustworthy a piece of knowledge is.
## Direction, not construction
A possible end-state is a level-first corpus tree where one address string carries
trust, domain, claim, and freshness at once. It is direction only.
**Nothing in this document is mechanized**: no enforcement tooling, no mass
relabeling, and no new tier scheme. This document establishes the vocabulary above
and labeling from birth; readers, parity checks, and promotion tooling remain outside
its scope.
@@ -0,0 +1,52 @@
# Migrating a pre-convention rig's topology context
For rigs whose hand-authored context predates the chain-file convention and
lives at the legacy location (`~/.openrig/shared-docs/rigs/<rig>/`, or
`$OPENRIG_SHARED_DOCS_ROOT/rigs/<rig>/`). Nothing here is destructive: the
legacy tree is left in place, reads keep working throughout (the walker's
advisory-emitting fallback), and every copy is no-clobber.
## The path, with exact commands
```bash
# 0. Know both roots — never hardcode either.
DEST="$(rig config get topology.root)"
SRC="${OPENRIG_SHARED_DOCS_ROOT:-$HOME/.openrig/shared-docs}/rigs"
# 1. BEFORE: prove the fallback is carrying this rig (expect ADVISORY lines
# on stderr for every level that resolves at the legacy location).
rig context trace --rig <rig> --seat <seat> --name LEARNED.md
# 2. Preview the migration (dry run — nothing moves).
rsync -av --ignore-existing --exclude 'state/' --dry-run "$SRC/<rig>/" "$DEST/rigs/<rig>/"
# 3. Migrate. --ignore-existing mirrors the installer's copy-if-absent law:
# anything already earned under topology.root is never overwritten.
mkdir -p "$DEST/rigs/<rig>"
rsync -av --ignore-existing --exclude 'state/' "$SRC/<rig>/" "$DEST/rigs/<rig>/"
# 4. AFTER: the same trace now resolves canonically — the advisories are gone.
rig context trace --rig <rig> --seat <seat> --name LEARNED.md
# 5. Do NOT delete the legacy tree in the same session. Archive it later,
# once every consumer (queue-state add-dirs, review artifacts, scripts)
# has been confirmed off it — reads through the fallback stay correct in
# the meantime, which is the point of fail-open.
```
## What migrates and what does not
- **Migrates:** the rig's chain files and seat directories — `LEARNED.md`,
`CULTURE.md`, craft files, `seats/<seat>/…`.
- **Stays (for now):** `state/` (queue-state add-dirs and live artifacts are
actively written by running seats; moving them under a live rig strands
writers — that cutover is its own change with its own receipt).
- **Never:** anything into a running seat's context. Migration moves files;
delivery to running seats is the refocus channel
(`docs/reference/refocus-channel.md`) — editing a file is not delivery.
## After migration
The next `rig up` of a spec shipping topology defaults installs them
copy-if-absent alongside the migrated content — earned context always wins
over a shipped default. Verify any doubt with the trace, not with memory.
+173
View File
@@ -0,0 +1,173 @@
# Lore routing — position knowledge by address
Lore is knowledge earned by one durable position while doing its work. It stays
under that position's seat tree and is composed by address when another artifact
needs it. The value is the combination of the content and where it was learned;
copying the bytes into a shared library destroys that distinction.
This convention defines the item shape, stable address, composition grant, and
the only path by which lore-derived content may leave the position boundary. It
adds no resolver, source kind, curator role, or transcript-mining process.
## Home, ownership, and stable address
Each item is one Markdown file beneath the owning seat's `lore/` directory:
```text
<topology-root>/rigs/<rig>/seats/<seat>/lore/<stable-slug>.md
```
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
their situation calls for it.
+224
View File
@@ -0,0 +1,224 @@
---
doc: reference
title: The mission install — the project half of agent onboarding
stage: established
warrant: >-
Compose project context from authored, addressable artifacts so a joining agent
can recover purpose, source precedence, current responsibility and facts to derive.
Paced delivery and a checked delta make consumption and unresolved facts visible.
sources:
- docs/reference/sdlc-conventions.md
- docs/reference/product-journey-sdlc.md
---
# The mission install
An agent joining work needs two installs. The **world install** teaches the system it
now lives in — what this place is, how to act, what it can do. The **mission install**
teaches the project — what is being built, how it got here, where it stands, and what
is theirs to do. This document is the convention for the second half. It applies to any
project a mission tree can hold: software, a book, accounting, research.
**The reader who cannot help you wrote this convention.** A freshly started or freshly
cleared agent cannot pull context — it does not know what exists, so it cannot ask.
Every rule below follows from that: the install is **composed on the agent's behalf and
pushed**, piece by piece, by someone who already holds the project.
## When to run one
- A fresh seat joins a mission.
- An occupant handover: the successor inherits the seat, then gets the project.
- A release or phase boundary: every continuing seat re-primes on the new frontier.
- A project switch: a seat moves from one mission to another.
Always AFTER the world install. The mission install assumes a situated reader.
## The five layers
Compose every install from these layers, in this order. They are a skeleton, not a
script — depth profiles (below) select how much of each layer a given seat receives.
1. **PIECES** — the authored history and design: the artifacts that explain how the
project got to now. Composed by address from the mission tree, each piece carrying
its source path and content hash. Never paraphrased.
2. **ARRANGEMENT** — how the pieces connect. This is the one layer whose content is
not on disk anywhere else: an authored recap by someone who held the work, stating
what joins the pieces, what to trust, and what supersedes what. If no arrangement
document exists, writing one is part of preparing the install — it is the
dispatcher's knowledge, deposited.
3. **POSITION** — what this seat specifically has learned and holds: its accumulated
lessons file and its most recent recap. Position is private to the lineage; it is
installed, never published.
4. **CONTRACT** — the current plan and the open decisions. This is the fastest-moving
layer, so every contract piece must carry its own `updated-at`, and any fact not
re-derived at compose time carries an explicit `UNVERIFIED` marker. A stale plan
misleads more than a missing one.
5. **DERIVE-YOUR-OWN-DELTA** — the final walked piece assigns a **derive list**: the
volatile facts the installee must re-derive itself, each stated as the command that
derives it (never as a remembered value). The installee runs the list and writes a
**dated delta** — what is true now that the install did not say — as its first
artifact, homed beside the seat's recap. This is the layer that turns a briefing
into an instrument check.
## Two depth profiles
- **Planning-class seats get FULL depth**: the complete ordered read set across all
five layers, ending at the live work pointer (the current plan's own next item).
- **Execution-class seats get HIGH-ALTITUDE depth**: a handful of addressed reads —
the project frame, the governing scope contract, the source index, the operating
playbook — plus layer 5. Slice-level depth deliberately arrives LATER, with each
routed work item, whose own spec opens with its full design contract.
The situation (fresh, handover, boundary, switch) selects subsets over the layers;
the seat class selects depth. Do not send everyone everything. Two invariants hold at
every depth: **the arrangement layer is never skipped** (it is what tells the reader
whom to trust), and **a profile slot with no matching artifact is never authored at
install time** — fill it with a derived listing (add it to the derive list) or leave
it empty and say so.
## Delivery rules
- **Push, composed on the agent's behalf.** The dispatcher composes; the installee
receives. One piece per message, paced, with room to absorb between pieces — a
concatenated dump of the same bytes is a failed delivery regardless of content.
- **Pointer-walk by default**: for a world-installed seat, each paced message names
the artifact to read and frames the read as a prerequisite to the seat's own next
task. The seat executes the read itself; reading is the install.
- **Byte-walk only where the seat cannot read** (no file access, cross-machine
delivery): then the walk carries the bytes, in pieces, never as one block.
- **Ask for a reaction per piece** where consumption matters: one line stating what
the piece changes for the reader. A silent walk is unverified delivery.
- **A pointer may be section-scoped.** When one file mixes install content with
reference material, the pointer names the sections to read and the sections that
are reference; the reader's reaction names the sections actually read. Splitting
the file is better when two consumers keep needing different subsets.
## Mission-selected execution components
A mission may carry `mission.yaml`, and a slice may carry `slice.yaml`, as an
address map for the work arrangement. When present, install `mission.yaml` in
the ARRANGEMENT layer and the active `slice.yaml` in the CONTRACT layer. Read
the mission file first; the slice may narrow or override mission defaults.
The reusable component catalog is repo
`docs/reference/product-journey-sdlc.md` and installed
`$OPENRIG_HOME/reference/product-journey-sdlc.md`. A catalog with
`root: repository` resolves from `git rev-parse --show-toplevel`; use the
`loading-addressable-markdown` skill for addressed H2/H3 sections.
Before the installee claims work, require one readiness statement naming:
**role; user outcome; starting candidate; boundaries**. The selected component
IDs and explicit edges arrange the work; they do not expand scope, create a
mode, or enforce gates. If no composition exists, the light Part A flow in
`mission-slice-sop` is complete—absence is not a reason to mint YAML.
## Composition, not generation
The install **composes authored, cited artifacts. It never generates summary prose at
install time.** A generated summary is a second source that drifts and cannot be
trusted or verified; an authored piece carries its provenance. The one place authorship
is created for the install is the arrangement layer — and that is deposited knowledge
written by someone accountable for it, kept and reused, not regenerated per install.
## The exclusion rule
A mission tree accumulates material that is **reference, never install content**:
incident residue, superseded planning, transcript archives, dead ends kept for the
record. The install inherits a **cleaned** view — current truth plus the arrangement's
account of how it got there. Excluded material stays addressable; the arrangement may
point at it, but the walk never carries it. When preparing an install, sort every
candidate piece: install content, or reference. If unsure, it is reference.
## The layer-5 verification loop
Layer 5 is a sequence, not a suggestion. Run all of it:
1. The dispatcher's final walked piece assigns the derive list — each volatile fact
paired with the command that derives it — **and names the exact destination path
for the delta** (a fresh seat has no recap yet to sit beside; the dispatcher
decides the home, the installee does not guess).
2. The installee runs every command and writes the dated delta at the named path.
3. The dispatcher verifies the artifact **exists** (not that it is claimed).
4. A second reader — anyone but the installee — checks the delta's **facts**. When
the project has only a dispatcher and an installee, the dispatcher performs the
fact check and the delta records that the check was self-performed — an honest
degradation, stated, never silent.
5. A factual error triggers a re-derive round: the installee re-runs the commands,
corrects, and reseals the delta. The correction round is value, not overhead:
it verifies consumption and calibrates the instrument in one act.
A volatile fact with **no deriving command** — one only a human or external party can
settle — goes on the list anyway, marked `UNVERIFIED` with an explicit
ask-instruction; the delta carries it forward unresolved rather than silently
adopting a value.
An install without a landed, checked delta is not complete. It is a broadcast.
## The fidelity law
Design detail that lives in conversation gets **deposited at mint time**. A work item
whose spec holds only a one-line intent, when the real design filled a discussion, is
a 20:1 compression — and building from it makes correctness a coincidence. The
convention therefore names where deposited design lives: the work item's own
**design-contract block** (or a DESIGN-INPUT file beside it). Layer-1 pieces point at
that full-fidelity record, never at the compressed intent line.
## The boundary
**The mission install stops at the mission root.** Everything above it — what the
wider system is, the team's shape, the culture — belongs to the world install, which
runs first. Tracing above the project root is a separate tool for a separate question.
One install, one tree.
## Composing with the shipped machinery
The layers compose from surfaces that already exist; no new mechanism is required:
- Pieces and contract: one `--mission` grant spans the mission and its slices —
`rig context profile <pack-ref> --situation <fresh|handover|post-compaction> --mission <mission>`
composes addressed sections with per-piece provenance and content hashes. The
`--situation` flag is required and is a SEPARATE input from the depth profile:
situation says which composition menu applies (fresh boot, occupant handover,
post-compaction restore); FULL vs HIGH-ALTITUDE says how much of each layer the
seat class receives. Choose both, independently.
- Named install profiles make that independent depth/ordering choice explicit.
`rig context profile world-public --situation fresh --runtime codex --profile codex-coverage
--rig <rig> --seat <seat> --mission <mission> --slice <slice> --json` is
delivery-free inspection: it reports the selected profile, ordered phases,
exact pieces, token estimates, hashes, and source provenance before anything
reaches a seat. The `codex-coverage` profile orders bootstrap → concrete
project/mission/role/task → coverage map; `guided` keeps the fuller authored
walk separately selectable. Both select from the same source graph.
- Position: the `--rig`/`--seat` grant serves the seat's lessons and recap.
- Runtime: `context profile` and `context work-install` both accept
`--runtime claude-code` (alias `claude`) or `--runtime codex`. Profile
composition retains the `claude` manifest/output key; skill projection
retains `claude-code`. Profile defaults from `OPENRIG_RUNTIME`, otherwise
Claude; work-install requires the flag to inspect or apply skills.
- Delivery: `rig walk <seat> --through-profile world-public --profile <profile>
--situation <situation> ...` applies the already-inspected profile;
`--through <files> --pace <interval>` remains the raw byte-walk path. Paced
sends naming addressed reads serve pointer-walks.
- The delta: a dated markdown file beside the seat's recap; verified by reading it.
Where these verbs are unavailable (a different toolchain, a plain filesystem), the
convention still holds: compose by copying addressed sections with their source paths,
deliver paced, assign the derive list, require the delta.
## Quickstart for any project
1. Sort the mission's artifacts: install content vs reference (the exclusion rule).
2. Write or update the arrangement document — how the pieces connect, what to trust.
3. Mark every contract piece with `updated-at`; mark underived facts `UNVERIFIED`.
4. Choose the profile: planning-class full, or execution-class high-altitude.
5. If mission/slice YAML selects execution components, include those files and
their addressed catalog sections at the appropriate layers.
6. Compose the ordered piece list with provenance; deliver it paced, pointer-first.
7. End with the derive list; require the dated delta; verify existence; second-read
the facts; re-derive on error.
A project of any kind fits this: for a book, the pieces are the outline's history and
voice decisions, the arrangement is how the drafts relate, the position is this
editor-seat's lessons, the contract is the current chapter plan with its dates, and
the derive list is "count the chapters, read the latest editorial notes, state today's
word count" — facts the new agent must touch, not trust.
+53
View File
@@ -0,0 +1,53 @@
# The planning dial — how much rigor a plan gets, chosen per piece
Planning is a spectrum, not a binary. When the wave model is selected, build care
varies by wave composition; planning needs the same flexibility, or every piece gets
one protocol — over-working the trivial and under-working the hard. Choose the rung
from the work's uncertainty and consequences.
## The rungs — dial up by what the piece is
- **P0 — mini-requirements + pointers.** A few observable outcomes and where to look;
the builder figures out the how. Right for simple, reversible, well-trodden work.
- **P1 — authored spec.** Intent, mini-requirements, a failing-test-first proof
contract, declared territory. The default.
- **P2 — plus a research round,** run BEFORE the spec freezes, never during the build.
The research prompt is authored by an agent who holds the product context (see the
gate below); execution may fan out to any agents — web legs, documentation legs,
code-reading legs in parallel. Research returns as CONTEXT with citations, never
prescriptions; synthesis against your own architecture and intent follows, and it
differs for greenfield (design the thing) versus brownfield (sharpen your approach).
- **P3 — plus an adversarial pass.** A non-author who holds the product context
attacks the plan in both directions with the product goals as judge. Amendments land
with proof-contract teeth — a new or changed checkbox — never as prose advice.
- **P4 — plus a blind design gate.** A from-scratch design is committed before reading
the priors, then diffed. Similar → proceed; significantly different → stop for a
design session with the owner.
## The context gate
Only agents with the product's world context installed may author research prompts,
run adversarial passes, or weigh in on architecture questions. Execution researchers
and builders need no such install — the agent CREATING their instructions must have
it. The reason: research shaped by someone who does not know what the product is for
returns answers to the wrong question, fluently.
## What earns P2 or P3 — importance is not the test
A piece earns **P2** when the spec would otherwise freeze on an unresolved external
unknown — a feasibility door or an open design space only research can close. A piece
earns **P3** when its failure mode is invisible to its own author: load-bearing
substrate where a plausible, self-consistent plan would ship the exact disease it
exists to kill. The discriminator is "would a wrong plan be expensive, hard to undo,
and invisible from inside" — never "is this piece central." Importance without an
unclosable unknown or an author-blind failure mode stays at P1. Planning is text-only
work; keep its scope proportional to the unresolved question and the cost of a wrong
plan, rather than assuming a fixed duration for a rung.
## Cost calibration
Scope research to the unknown that could change the plan; scope an adversarial
pass to the failure modes the author may miss. Record the questions resolved and
the resulting design changes so the owner can judge the value of the selected
rung. Run planned research before build dispatch, while its answers can still
shape the spec.
+343
View File
@@ -0,0 +1,343 @@
# Product-journey SDLC components
This is a reusable menu for building the smallest complete user outcome. A
mission or slice selects only the components its work needs and records their
order in mission/slice YAML. This is **not a mode, pipeline, topology, rig
shape, or mandatory gate set**. If no composition is present, use the light
Part A flow in `sdlc-conventions.md`.
## The doghouse and the moon base
The **doghouse** is the actual product outcome the user asked for. A
**missing door** is an implementation that may be polished internally but
cannot perform its basic user job. A **moon base** is elaborate machinery—
often guards, abstractions, proof ceremony, security theatre, or process
scaffolding—that consumes most of the effort without making the doghouse work.
Use that disclosure ladder in this order:
1. Name the doghouse in one sentence, without process vocabulary.
2. Exercise its door through the public surface before admiring internals.
3. Repair the smallest missing product seam.
4. Consider deeper machinery only when evidence shows the layer below is
causing the visible failure.
5. Remove or decline anything that does not protect or deliver a named user
outcome.
The same discipline works for a new mission and for rescuing one that drifted.
In a new mission, it prevents the moon base. In a rescue, it finds whether a
usable doghouse remains and separates it from machinery that should not be
carried forward.
## Component contract
Every component below states:
- **Use when** — the evidence that admits it.
- **Skip when** — the cheapest reason not to run it.
- **Input** — what the responsible agent must actually read or observe.
- **Action** — the bounded work performed.
- **Output** — the artifact or observable result handed to the next component.
- **Stop** — the boundary that prevents the component expanding itself.
- **Next** — the normal successor; YAML may choose another explicit edge.
Component IDs are stable addresses. Their prose may improve without forcing
mission YAML to change.
## Component catalog
### context.fresh-start
- **Use when:** prior context may carry stale architecture, verdicts, or
implementation momentum.
- **Skip when:** the current agent can independently state the user outcome,
starting candidate, and boundaries from primary sources.
- **Input:** identity, project/mission/slice YAML, and the selected addresses.
- **Action:** load the selected context into a fresh session, or reorient the
current session from live sources. Clearing a seat requires its separate
continuity decision; selecting this component does not authorize it.
- **Output:** a one-paragraph readiness statement naming role, user outcome,
starting candidate, and boundaries.
- **Stop:** do not reconstruct old queue lore merely because it exists.
- **Next:** `intent.recover` or `journey.choose`.
### intent.recover
- **Use when:** work already exists but may have drifted from its original user
intent.
- **Skip when:** this is greenfield work with one current, unambiguous intent.
- **Input:** the earliest authoritative intent, later mission/slice specs,
candidate code, and observed user behavior.
- **Action:** distinguish requirements from later planner elaboration; identify
the smallest still-required outcomes and explicit anti-goals.
- **Output:** a concise salvage map: keep, inspect, and not-required—never a
destructive verdict by itself.
- **Stop:** do not treat prior review intensity or sunk cost as product value.
- **Next:** `journey.probe`.
### journey.choose
- **Use when:** a new mission has intent but no proven product path yet.
- **Skip when:** `intent.recover` already names the next smallest outcome.
- **Input:** authoritative intent and current public product surface.
- **Action:** select one end-to-end user journey whose completion provides
standalone value.
- **Output:** one observable outcome plus its smallest credible public probe.
- **Stop:** one outcome only; adjacent capability is another composition.
- **Next:** `journey.probe`.
### journey.probe
- **Use when:** always, before product code is added or rescued.
- **Skip when:** never; scale the probe down instead.
- **Input:** the public CLI, API, UI, or other surface a real user operates.
- **Action:** run the smallest disposable end-to-end journey and inspect its
effects, not merely its exit status.
- **Output:** PASS with no code needed, or one concrete user-visible gap with
preserved evidence.
- **Stop:** a passing journey does not authorize speculative hardening.
- **Next:** finish on PASS; otherwise `depth.look-below` or
`build.minimal-gap`.
### depth.look-below
- **Use when:** the public failure recurs, crosses a component boundary, is
contradicted by lower-level state, or a surface patch would duplicate logic
already owned below.
- **Skip when:** the failure is local, understood, and repairable at the public
seam without duplication; also skip when the only argument is “more robust.”
- **Input:** one observed public failure and the immediately responsible seam.
- **Action:** descend **one layer only**; state the causal hypothesis, name the
lower-layer observation that would falsify it, and inspect or probe that
layer.
- **Output:** one of three depth decisions:
- `LOCAL` — repair the public seam;
- `BELOW` — repair the named load-bearing seam and replay the public journey;
- `HOUSEKEEPING` — repair environment/data/topology hygiene before changing
product code.
- **Stop:** one descent per admission. A second descent requires new evidence
and a newly stated hypothesis. “While here,” generalized prevention, and
unrelated cleanup are out of scope.
- **Next:** `build.minimal-gap`, `research.before-build`, or housekeeping.
This component is the balance point. Refusing ever to look below produces
surface patches and recurring defects. Looking below without an evidence gate
produces a moon base. The aim is the first load-bearing seam, not the deepest
possible explanation.
### research.before-build
- **Use when:** the causal hypothesis depends on an unstable external fact or
a product contract that cannot be derived locally.
- **Skip when:** source, runtime state, and the public probe already establish
the mechanism.
- **Input:** one answerable uncertainty and its decision consequence.
- **Action:** acquire only enough primary evidence to decide the implementation
shape.
- **Output:** decision, citations, and remaining uncertainty.
- **Stop:** research ends when the implementation decision is supported.
- **Next:** `build.minimal-gap`.
### build.minimal-gap
- **Use when:** a failed journey and causal seam are established.
- **Skip when:** `journey.probe` passed or the finding is housekeeping rather
than code.
- **Input:** the user-visible gap, depth decision, existing seam, and explicit
boundaries.
- **Action:** add the shortest readable change that completes the journey,
reusing the existing product seam.
- **Output:** one clean candidate and focused evidence of the failure changing
for the stated reason.
- **Stop:** no new framework, security layer, schema, abstraction, or adjacent
verb unless the outcome cannot work without it.
- **Next:** `qa.public-journey`.
### qa.public-journey
- **Use when:** a candidate claims a complete user outcome.
- **Skip when:** never before integration; it may be held by the builder on a
tiny change only when independence was not selected.
- **Input:** exact candidate, original public probe, and boundaries.
- **Action:** run the journey from the user surface in a disposable context;
inspect resulting state and partial-failure behavior.
- **Output:** CLEAR or one concrete mismatch.
- **Stop:** QA does not redesign the product or demand unselected ceremony.
- **Next:** `review.scope-maintainability` or back to `build.minimal-gap`.
### review.scope-maintainability
- **Use when:** independent review is selected for the outcome.
- **Skip when:** the composition deliberately keeps a low-risk change within
the light single-agent path.
- **Input:** exact candidate, intent, public QA evidence, and base diff.
- **Action:** judge correctness, readability, necessity, reuse of existing
seams, and whether the public journey—not an internal proxy—works.
- **Output:** CLEAR or a bounded source/product finding.
- **Stop:** a reviewer may not ratchet rigor merely because prior review found
something; review the product, not the ceremony.
- **Next:** `integrate.one-outcome` or back to `build.minimal-gap`.
### integrate.one-outcome
- **Use when:** the selected evidence for one outcome is complete.
- **Skip when:** the candidate is not clear or its base moved incompatibly.
- **Input:** exact candidate identity and selected QA/review results.
- **Action:** integrate the smallest coherent outcome and keep unrelated
candidates separate.
- **Output:** clean canonical source containing one additional user outcome.
- **Stop:** do not batch unrelated salvage because it shares a release label.
- **Next:** `runtime.adopt` or the next `journey.choose`.
### runtime.adopt
- **Use when:** the outcome needs installed-runtime or migration proof.
- **Skip when:** source-level proof is sufficient and no live behavior changed.
- **Input:** clean canonical commit, rollback boundary, and runtime SOP.
- **Action:** build/package/install the exact commit, switch atomically, and
verify health plus continuity.
- **Output:** exact live version and preserved rollback point.
- **Stop:** adoption is deployment, not permission to change topology or data.
- **Next:** `qa.installed-smoke`.
### qa.installed-smoke
- **Use when:** `runtime.adopt` ran or packaging/path resolution is part of the
product claim.
- **Skip when:** no installed artifact or runtime behavior is in scope.
- **Input:** exact live version and disposable public journey.
- **Action:** exercise the installed command path and verify effects and
cleanup.
- **Output:** installed-runtime CLEAR or one concrete mismatch.
- **Stop:** do not convert a smoke test into fleet-wide monitoring.
- **Next:** finish the outcome or return to `build.minimal-gap`.
## Composition rules
1. `project.yaml` may declare project defaults; `mission.yaml` may select the
catalog and override those defaults for the mission.
2. `slice.yaml` selects or overrides the components for that slice.
3. Edges are explicit data. Array order is presentation, not dependency.
4. Select the fewest components that honestly fit the work.
5. A component may be held by the same seat as its neighbor unless independence
is an intentional property of the composition.
6. YAML describes the work; it is not an enforcement engine and creates no
automatic gates.
Repository-address example:
```yaml
sdlc:
catalog:
root: repository
address: docs/reference/product-journey-sdlc.md#component-catalog
components:
- id: journey.choose
owner: planner
- id: journey.probe
owner: qa
- id: depth.look-below
owner: builder
admission: evidence-gated
- id: build.minimal-gap
owner: builder
- id: qa.public-journey
owner: qa
edges:
- from: journey.choose
to: journey.probe
- from: journey.probe
to: depth.look-below
when: public failure needs causal discrimination
- from: journey.probe
to: build.minimal-gap
when: local gap is already established
- from: depth.look-below
to: build.minimal-gap
when: decision is LOCAL or BELOW
- from: build.minimal-gap
to: qa.public-journey
```
Resolve repository addresses from `git rev-parse --show-toplevel`. Use the
`loading-addressable-markdown` skill for `path#h2-slug` and
`path#h2-slug/h3-slug` addresses outside the context library.
## Example compositions
### Greenfield product journey
`context.fresh-start → journey.choose → journey.probe →
build.minimal-gap → qa.public-journey → review.scope-maintainability →
integrate.one-outcome`
Add `depth.look-below` only when the probe meets its admission rule. A new
mission does not need rescue archaeology.
### Drifted-mission rescue
`context.fresh-start → intent.recover → journey.probe →
depth.look-below? → build.minimal-gap → qa.public-journey →
review.scope-maintainability → integrate.one-outcome`
The question is not “which old commits pass review?” It is “which required
user outcome works, what prevents the next one, and how little code completes
it?”
### Recurring cross-layer defect
`journey.probe → depth.look-below → research.before-build? →
build.minimal-gap → qa.public-journey → qa.installed-smoke?`
The recurrence admits one causal descent. It does not authorize a general
platform rewrite.
## Resolve the selected path
On a fresh start, restore, or new assignment:
1. Derive identity with `rig whoami --json`. Resolve the project's work root from
the assignment/workspace binding; `rig config get workspace.root` locates the
configured work tree, which may differ from the seat's code checkout.
2. Read `project.yaml` and its project-authority context. Follow its mission root
to the assigned `mission.yaml`, then its composition ref to the active
`slice.yaml`. Read the current intent and acceptance files those records name.
Do not choose a mission from an old onboarding packet or a folder's recency.
3. Resolve SDLC selection in order: project defaults, mission defaults, then the
active slice's explicit selection/override. A narrower explicit selection
replaces the broader component list; it does not append every ancestor's
gates. Follow an addressed wave map for its membership, review model and
boundary; an explicit named-slice exception applies only to that slice.
4. Read only the selected components and additional addressed context. Repository
references resolve from the code repository root; installed references from
`$OPENRIG_HOME/reference`; context-pack refs use `rig context get`. For a
Markdown `#section` address, use `loading-addressable-markdown` rather than
loading an entire unrelated manual. Expand the investigation when the task's
evidence requires it: the address map is a starting point, not a search limit.
5. State briefly the user outcome, current role, candidate, selected path and
next completion boundary. No composition means the light Part A flow in
`sdlc-conventions.md#a1-the-flow-in-one-pass`. Do not author a
composition merely to have one, or mint QA, guard, review, lock or proof gates
because those roles exist. On a tiny change the builder can verify the whole
outcome; independent review happens only when selected.
A missing referenced file is a named gap, not evidence of no selection. Inspect
or ask the owner to resolve a consequential ambiguity; do not silently fall back
from an explicitly selected rigorous path. An owner can explicitly amend the
selection for named work; record that decision at the work's authority. Current
command/source reality outranks stale YAML as a factual claim: record the
mismatch, re-derive, and preserve the distinction between scope and observed state.
Startup overlays, role guidance, available seats, old mode names and historical
packets cannot add steps to this selection. They supply capabilities. Skills load
when the selected work calls for them; there is no quiz, per-file ACK gate or
universal preloading requirement.
## Blank-agent start
Follow **Resolve the selected path** above before choosing work. For an authored
wave, build and verify each slice with its local discipline and integrate serially
where required; request independent review once when the authored wave boundary
arrives. Do not start per-slice reviews while waiting. A separately selected
rigorous slice retains its named checks. Review findings justify bounded repairs,
not an automatic increase in process for the next round.
+139
View File
@@ -0,0 +1,139 @@
# The product-management pass — what runs above planning, before planners touch a mission
Graduated 2026-08-28 from a working SOP born during live release preparation; the
originating team's channel specifics stay in its playbook.
## Why this layer exists (read this or the steps are ceremony)
The build is a lossy compression pipeline: product intent → planner spec → builder
code, and each stage is a narrower reader than the one above. Nothing leaks downward
by proximity — a dimension the product owner's seat holds and does not WRITE AND
ROUTE is deleted from the product, and the deletion is silent: the downstream agent
invents a plausible replacement rather than raising a hand. This pass is the only
point where product taste physically enters the pipeline. Its output is judged by one
measuring stick:
> A slice folder is ready when SPEC.md alone, plus only what SPEC.md explicitly
> routes to, reproduces the design intent in a reader who has none of your context.
## The steps
1. **GROUND IN THE RAW MATERIAL.** Survey where the ideas actually live: owner intent
layers, prior-era designs, the conversations. Enumerate cheaply first (intent
frontmatter, indexes); read the centerpiece documents yourself; fan out readers for
breadth, requiring citation-bearing reports (claims with openable paths; status:
shipped / partial / designed-never-built). Never grep-and-hope.
2. **SYNTHESIZE ONE SCOPE OF RECORD.** Theme; waves; binding design principles; an
explicitly-NOT-in list (decisions, not omissions); the owner-decision set, each
with a recommendation. Archive superseded planning where a cold reader cannot
mistake it for authoritative. Every other surface DERIVES from this document;
where they disagree, it wins.
3. **PRESENT DECISIONS DECISION-READY.** One at a time, lettered, recommendation
first, records cited. The owner's ruling gates everything downstream —
mechanically where possible (a blocked-on anchor), never by memory.
4. **MINT SLICES AT PLAN TIME** with real one-paragraph intents.
5. **DEPOSIT THE DESIGN — the fidelity law.** Every design-bearing slice's SPEC.md
carries the full design contract INLINE: the owner's words verbatim, the settled
do-not-reopen decisions, the live evidence, the shape of done, the anti-goals. A
one-line intent is a ~20:1 lossy compression; building from it alone matches the
design only by coincidence. Design that happened in conversation is deposited at
mint time or it dies with the author's context window. Mechanical slices with
durable named seeds may stay thin — thinness is a per-slice judgment, never a
default.
6. **ROUTE EVERYTHING — the spec is the router.** The downstream reader's world is
SPEC.md; their curiosity does not extend past it, and a file's existence is not a
route. The spec itself carries the complete per-slice reading list (the exact
sources YOU used, with one line of why each) and per-slice research routing
(which tool answers which question — including explicit NOs where outside
frameworks would contaminate an owner-designed model). Nothing load-bearing may
depend on a reader's initiative.
7. **FRAME THE PLANNING PER SLICE.** In each spec: the planning-dial rung WITH its
reason (`docs/reference/planning-dial.md`); what matters most; the required output
shape. Author the research prompts for research-round slices yourself — the
context gate: prompts come from the agent who holds the product context;
execution can fan out. Constraints binding every spec are stated in the spec,
never assumed known.
8. **THE THEORY-OF-MIND REREAD.** Before releasing the batch, reread each spec AS the
narrow downstream reader — an agent who got a queue ticket, will read this one
file, and will invent anything missing without asking. Every "but how would they
know X?" is a hole to fix now.
9. **SEQUENCE AND GATE.** Waves file-disjoint; anything that must precede dispatch
gates it as a row dependency, never a remembered intention; elaboration batons
carry the dial assignments and route back per slice.
10. **KEEP THE RECORD LIVE.** The owner-facing board re-derives from the scope of
record; corrections land in the governing document first and project outward.
## The failure modes this pass exists to prevent (each observed live)
- Thin intents shipped as if elaboration could recover fidelity the elaborator never
had.
- Beautiful context written to disk but never routed — write-only artifacts.
- Reading lists that were curated excerpts of the real source set.
- Research needs known to the shaper but never assigned a tool or a question.
- Dispatch before the gate; sequencing held in memory instead of a row.
- Knowing the world at ten dimensions and writing for readers as if the missing
seven would leak through on their own. They do not. Route it or it is gone.
## Addressing the wide-angle seat: context, never instructions
When any seat sends findings, problems, or proposals to a wide-angle seat (an
advisor, a product shaper), it frames them as CONTEXT ON THE TABLE — the problem
articulated, the grounding, candidate solutions if it has them — and explicitly
leaves the steering to the receiver. Never as instructions, even with a confident
recommendation in hand. Why: narrow-aperture seats produce narrow solutions and
default to imperative speech; a wide-angle seat that trusts the sender will comply,
and that is exactly how narrow decisions leak into a product unsynthesized. The
pattern converts the sender's intensity (deep, focused, current) into the receiver's
synthesis (wide, cross-cutting, temporal). This is the inbound half of the fidelity
law: outbound, the shaper deposits design so compression cannot eat it; inbound,
senders frame as context so aperture cannot bypass synthesis.
THE PROTOCOL IS BIDIRECTIONAL. Downward — orchestrator to builder — the same wire
discipline applies in mirror: put the goal, the acceptance criteria, and the
complete context ON THE TABLE, and leave the implementation steering to the
receiver. An orchestrator that converts context into step-by-step orders bypasses
the builder's synthesis exactly the way a narrow seat's imperative speech bypasses
a wide seat's. Empowerment is delivered as context, never as instructions — in
both directions.
## Aperture narrows by default — and that is the point
Do not read the above as "assign every seat an aperture." Agents inevitably narrow:
heads-down on one kind of work, a seat specializes, and the narrowing IS the
specialization effect — the system's core mechanism, never a flaw to correct.
Widening a specialist diffuses what made them elite. Never set a narrow aperture
intentionally; seats arrive at theirs on their own. The ONLY aperture you ever steer
is the wide one: wide-angle is itself a specialization, and its defining discipline
is MAINTAINING width against the same pull that narrows everyone else. State
aperture in a role only when the role must hold altitude.
## The topology pairing (blueprint note)
A strong topology pairs an ORCHESTRATION seat (flow: dispatch, sequencing, capacity —
wide across the board, narrow in the act) with a PRODUCT-SHAPING seat (taste:
coherence, contracts, temporal judgment — wide across the product), each with its
aperture stated in its role. The context-not-instructions rule is the wire protocol
between them.
## Price one layer lower — the right-layer check
Before accepting the fix your hands reached for, price ONE LAYER LOWER. The tendency
is to fix high — high fixes are easy to write and test — and narrow is sometimes
exactly right: sometimes all you need is a band-aid. But sometimes you are setting a
bone, and the tells are learnable:
- **The bone tell:** the lower fix collapses several open problems at once, and is
often SMALLER than the patch — it deletes or reuses instead of adding.
- **The band-aid tell:** the lower layer is stable, correct, and merely
inconvenient; the defect is genuinely local; nothing else downstream shares it.
- **The floor:** there is a sweet spot. A fix below the primitives you own is
someone else's lane, and a new engine where composition suffices fails the
simplicity bar.
- **The move, mechanically:** name the layer you instinctively chose; name the
layer below it; state what each fixes and what each leaves broken; choose with
the price visible.
This is the vertical twin of the wide-angle lens above: where
context-not-instructions protects synthesis across seats, ARE-YOU-AT-THE-RIGHT-LAYER
protects it across layers — a question to ask of every fix, and one layer lower
deserves a look every time.
@@ -0,0 +1,86 @@
schema: openrig.project/v0alpha1
kind: project
metadata: { id: example }
missions: { root: missions }
lifecycle:
profile: release-boundary-v0
profiles:
release-boundary-v0:
required_steps:
- mission-outcome
- exact-release-candidate
- capability-delta
- exact-cut-substance
- release-verification
- git-canonicalization
- public-release
- parent-adoption
- record-shipped
- release-boundary
workflow:
entry: { role: orchestrator }
# Entry/ordinary custody and exception selection are separate decisions.
# Replace the example target with a declared member (rig ps --nodes --json).
# Inspect with workflow compile/show; adopt later corrections with workflow revise.
exception_routing: { orchestrator_role: orchestrator }
roles:
orchestrator: { preferred_targets: [orch@example] }
steps:
- id: mission-outcome
actor_role: orchestrator
depends_on: []
objective: Judge the mission's promised outcomes against retained evidence; name remaining gaps.
allowed_exits: [handoff, waiting, failed]
- id: exact-release-candidate
actor_role: orchestrator
depends_on: [mission-outcome]
objective: Establish the exact candidate and tree to which subsequent receipts apply.
allowed_exits: [handoff, waiting, failed]
- id: capability-delta
actor_role: orchestrator
depends_on: [exact-release-candidate]
objective: Reconcile the shipped capability description with this candidate's actual behavior.
allowed_exits: [handoff, waiting, failed]
- id: exact-cut-substance
actor_role: orchestrator
depends_on: [capability-delta]
objective: Obtain and judge independent substance admission for the exact release cut.
allowed_exits: [handoff, waiting, failed]
- id: release-verification
actor_role: orchestrator
depends_on: [exact-cut-substance]
objective: Judge privacy, package shape, conventions and mirrors, builds, tests, and clean-tree evidence for the exact admitted release candidate.
allowed_exits: [handoff, waiting, failed]
- id: git-canonicalization
actor_role: orchestrator
depends_on: [release-verification]
objective: Obtain the authorized canonical Git identity and reconcile it with the verified candidate.
allowed_exits: [handoff, waiting, failed]
- id: public-release
actor_role: orchestrator
depends_on: [git-canonicalization]
objective: Obtain publication authorization and the release owner's exact public artifact receipt.
allowed_exits: [handoff, waiting, failed]
- id: parent-adoption
actor_role: orchestrator
depends_on: [public-release]
objective: Obtain separately authorized parent adoption and judge its effect receipt.
allowed_exits: [handoff, waiting, failed]
- id: record-shipped
actor_role: orchestrator
depends_on: [parent-adoption]
objective: Record durable shipped truth after judging ceremony receipts; the post-release boundary follows.
allowed_exits: [handoff, waiting, failed]
- id: release-boundary
actor_role: orchestrator
depends_on: [record-shipped]
objective: >-
After the release ceremony, author one attributed disposition record
covering seat renewal and re-prime, memory distillation, destination
queue sweep, substrate teardown or retention, board freeze and clean-box
baseline, capability-delta absorption and expiry, and packaged-product
discoverability. These are agent-judged guides, not seven daemon gates.
Inspect beyond this packet when useful. Record reasoned not-applicable
or deferred dispositions and wait on live dependencies. Finish after
this boundary when no successor is authored.
allowed_exits: [done, waiting, failed]
+94 -37
View File
@@ -1,9 +1,12 @@
# Project Workspace Contract
OpenRig's Project UI is a file-backed view over a workspace root. The folder
OpenRig's Project TUI is a file-backed view over a workspace root. The folder
shape is intentionally simple so humans and agents can create or repair it
without daemon-internal knowledge.
This subtree is part of the wider [OpenRig Instance Layout](instance-layout.md).
The instance initializer delegates these exact workspace bytes to this owner.
## Default Shape
`rig config init-workspace` creates the default workspace at
@@ -11,48 +14,101 @@ without daemon-internal knowledge.
```text
workspace/
README.md
STEERING.md
SPEC.md
project.yaml
workspace.yaml
.gitignore
missions/
README.md
idea-ledger/
README.md
PROGRESS.md
slices/
capture-product-ideas/
README.md
PROGRESS.md
IMPLEMENTATION-PRD.md
triage-product-ideas/
README.md
PROGRESS.md
IMPLEMENTATION-PRD.md
handoff-loop/
README.md
PROGRESS.md
slices/
route-work-packets/
README.md
PROGRESS.md
IMPLEMENTATION-PRD.md
verify-loop-evidence/
README.md
PROGRESS.md
IMPLEMENTATION-PRD.md
progress/
field-notes/
specs/
exhaust/
```
The scaffold is additive: it creates missing canonical entries and never
overwrites existing files, including with the deprecated `--force` flag.
`exhaust/` and local `.openrig/` runtime projections are ignored; authored
project context and mission/slice files remain versionable. `workspace.yaml`
is the project-location catalog. The project manifest exposes empty
`install.context` and `install.skills` selectors for ordered Markdown
addresses and stable managed-catalog skill IDs, but neither skill source nor a
System World belongs in this tree.
## Project-world install
`project.yaml` may select project context and managed skills together:
```yaml
schema: openrig.project/v0alpha1
kind: project
install:
intent: SPEC.md
context:
- conventions.md
skills:
- repository-maintenance
```
`install.context` contains project-relative Markdown addresses. `install.skills`
contains stable skill identities only. Skill source bytes live in the single
configured managed catalog (`skills.root`, default `$OPENRIG_HOME/skills`),
never under the project or workspace. `rig context work-install --runtime
<claude-code|codex>` resolves both parts; add `--apply-skills` to reconcile the
selected exact bytes into `.claude/skills/` or `.agents/skills/` under the
caller's current working directory. Use `--cwd` when the receiving agent works
somewhere else; the project-world metadata root is never assumed to be its code
working directory.
The generated harness directories and `.openrig/skill-loadouts/` ownership
receipts are projections, not source. Product repositories should ignore them.
Reconciliation removes a deselected entry only when its current bytes still
match OpenRig's last owned projection; unrelated and locally modified entries
are preserved or reported as conflicts.
Seats of the same runtime commonly share one working directory. Their topology
selectors are retained per canonical seat identity and projected as a union, so
starting one role cannot remove another role's skill. A changed projection is
visible only to a fresh harness process; reconciliation reports that boundary
instead of claiming a running seat hot-reloaded it.
An installed project selection is also retained in that working directory's
ownership receipt. A later seat start with no project-world input preserves it;
an explicit install whose `install.skills` is empty clears it. This keeps
"project not supplied" distinct from "project deliberately selects no skills."
## TUI project selection
Open **PROJECTS** (or enter `projects`) to choose an ID and root from the
configured workspace catalog. `project <id>` selects an exact catalog entry;
`mission <directory>` then opens that project's execution view. The historical
`scopes` machine section ID remains valid. Project selection and its canonical
root travel through mission, slice, source-file and Back navigation. Changing a
catalog root requires selecting it again. Missing or malformed sources remain
unavailable; another project's matching work ID is never a fallback.
`source` opens the current project, mission or slice's actual source through the
existing file-reader allowlist. Catalog membership does not grant file-read or
execution permission. These views only read; they do not select a lifecycle
operation or activate a mission. Execution and slice queue membership require
an exact `project:<id>` tag; lifecycle instances use their authored project
identity. Unscoped historical queue rows and global review artifacts without
project binding are excluded from project-specific claims.
The read API adds `GET /api/scopes/projects` and optional `project` and
`projectRoot` parameters to scopes, execution and slice-detail reads. Slice
detail also requires the selected mission directory. Existing reads without a
project parameter keep their legacy contract.
## UI Mapping
- `workspace.root` maps to the Project workspace.
- `workspace.catalog_path` maps to the `workspace.yaml` project catalog.
- `workspace.projects_root` is the default home for catalogued project worlds.
- `workspace.root/missions/<mission-id>` maps to a Project mission.
- `workspace.root/missions/<mission-id>/slices/<slice-id>` maps to a Project slice.
- Mission `PROGRESS.md` frontmatter supplies the mission status badge when the
file root is allowlisted.
- Slice `README.md`, `PROGRESS.md`, and `IMPLEMENTATION-PRD.md` frontmatter
supply display name, lifecycle status, mission id, and queue linkage hints.
- Mission and slice `SPEC.md` frontmatter supplies intent, advisory sibling
build-order `depends_on`, lifecycle status, and queue linkage hints.
- Slice `PROGRESS.md` is the durable acceptance checklist; `PROOF.md` and
`proof/` retain evidence paired to the SPEC proof contract.
Mission and slice ids should be stable kebab-case strings. Keep slice ids
unique inside the workspace so `/project/slice/<slice-id>` resolves without
@@ -90,12 +146,13 @@ default setup contract.
If Project shows a mission discovery warning:
1. Run `rig config get workspace.root --show-source`.
2. Run `rig config get workspace.slices_root --show-source`.
3. Confirm `workspace.slices_root` points at a folder containing mission
2. Run `rig config get workspace.catalog_path --show-source`.
3. Run `rig config get workspace.slices_root --show-source`.
4. Confirm `workspace.slices_root` points at a folder containing mission
directories with `slices/` children.
4. Confirm `files.allowlist` includes `workspace:<workspace.root>` so the UI
5. Confirm `files.allowlist` includes `workspace:<workspace.root>` so the TUI
can read mission `PROGRESS.md`.
5. If the workspace is missing, run `rig config init-workspace` after operator
6. If the workspace is missing, run `rig config init-workspace` after operator
approval.
No daemon restart is required for most config reads. Restart when changing
+90
View File
@@ -0,0 +1,90 @@
# The Refocus Channel
**How orientation content reaches a RUNNING seat.** Editing a file is not
delivery: a running seat read its configuration at session start, and nothing
in its context re-reads disk on its own. The refocus channel closes that gap —
it is the mechanism that makes shipped chain files (see
`chain-file-convention.md`) live doctrine rather than boot-time decoration.
## The mechanism
`openrig-core` ships `hooks/scripts/refocus.cjs`, registered on both runtimes:
| event | Claude Code | Codex | why |
|---|---|---|---|
| UserPromptSubmit | ✓ | ✓ | deliver due or on-demand context at a model-visible boundary |
| Stop | ✓ | — | catch a long Claude turn crossing the growth threshold |
| PostCompact | ✓ | ✓ | retain exact compaction due-state; deliver on the next prompt |
Claude's additional firing signal is **transcript growth** — not turns (one turn
can burn 200k tokens across fifty tool calls) and not wall-clock (the failure is
sustained work, not elapsed time). Its default threshold is ~2.6MB of JSONL
growth (≈300k tokens); tune with `OPENRIG_REFOCUS_BYTES`. Codex never uses that
threshold: its exact `PostCompact` lifecycle hook is the trigger, avoiding a
redundant double-fire around Codex's own compaction cadence. Set
`OPENRIG_REFOCUS_NOW=1` for an on-demand refocus and
`OPENRIG_REFOCUS_ENABLED=0` to disable the feature. Fresh `SessionStart` is
always a no-op: the default onboarding pack owns fresh orientation. Both
runtimes retain `PostCompact` due-state for the next prompt. Otherwise the hook
is a silent no-op, and it degrades to silence on unrelated hook errors. A
configured REF resolution failure instead degrades
loudly in the delivered payload while still completing the hook — a refocus
must never break a seat's turn.
Because the hook runs at the seat's own turn boundaries, delivery to a
RUNNING seat needs no relaunch, no operator action, and no message traffic:
the next prompt the seat processes carries the content. The latency bound is
"the seat's next turn," which is also the earliest moment new orientation
could have been acted on anyway.
## The content is configurable — and never project-specific in source
Resolution order:
1. `OPENRIG_REFOCUS_CONTENT_REF` — a path-like context-library ref resolved
through `rig context get`, so refocus receives the same assembled bytes as
on-demand pull. This wins when REF and FILE are both set.
2. `OPENRIG_REFOCUS_CONTENT_FILE` — an operator-authored file (per-seat or
per-rig via spec env).
3. `$OPENRIG_HOME/refocus/REFOCUS.md` — the instance's standing content.
4. The generic default at
`skills/refocusing/references/refocus.md`: three project-neutral orientation
questions, the ladder itself, and a pointer to the separately shipped
onboarding assets.
The hook names a resolved REF in the delivered payload. If REF resolution
fails, the payload starts with `REFOCUS CONTENT REF FAILED`, the exact ref, and
the resolver's reason, then continues with generic orientation. A broken ref
therefore stays visible without blocking the seat's session boundary. With REF
unset, the existing FILE and generic paths are unchanged.
Mission-, project-, or box-specific refocus text belongs in a context-library
entry or one of those FILES on the instance that needs it. **It must never be committed into product
source** — the shipped default carries no path, seat name, mission, or
practice that is not generally applicable.
Every delivered refocus pairs its content with the public `refocusing` skill's
path-only trace. Configure `OPENRIG_REFOCUS_TREES=topology|work|both` and
`OPENRIG_REFOCUS_DEPTH=light|full`; optional `OPENRIG_REFOCUS_TOPOLOGY_NODE`
and `OPENRIG_REFOCUS_WORK_NODE` select starts that cannot be derived. The
script resolves `topology.root` and `workspace.root` through live config and
reports broken links as gaps rather than following pointers.
This automatic hook path is additive to one-shot manual injection through
`rig send --context`; neither mode substitutes for the other.
## Relation to chain files
The chain files are the durable, altitude-addressed home of orientation
content; the refocus channel is its delivery schedule. A practice added to a
rig's `CRAFT.md` today reaches running seats through their next refocus
pointer, and future installs through the shipped defaults
(discovery → curation → ship, per the convention doc).
## What a refocus is NOT
A refocus corrects drift; it is not a wake (which restores liveness and must
not reframe work) and not a checkpoint (a deliberate phase-boundary pause).
Sending the heavy intervention when the light one was due is the most common
self-inflicted stall — the hook fires the light one automatically, which is
the point.
+120
View File
@@ -0,0 +1,120 @@
# The release boundary — housekeeping run once per fence
A CHECKLIST OF JUDGMENT GUIDES, not gates: every item says WHY, most items are one
act, and anything situational expires at the next boundary. Runs once per release
fence, after the prior release's ceremony completes. The project and seat owners
select the applicable actions and preserve their reasons in the boundary record.
## 1. Seats — reset or re-prime, per each seat's continuity needs
- **Choose continuity per seat, with its owner.** Fresh launch, managed compaction
with re-prime, or retaining the current session serve different needs. Role names
alone do not select one. Record the choice, the reason and custody of open work;
selecting a procedure does not itself authorize a lifecycle action.
- **Fresh launch** can clear accumulated context and load current startup guidance.
Before an authorized exit, preserve the seat's earned knowledge and continuation
(recap where provisioned); verify the new occupant's actual identity and install.
- **Managed compaction with re-prime** can retain planning continuity while
restoring the context needed for the next phase. **Retaining the current session**
can preserve ongoing custody. In either case, inspect which guidance is stale and
deliver the selected current context; an edit on disk is not delivery to a session.
- **Re-prime = the world walk + the current capability delta + the mission install**
per the mission-install convention (`docs/reference/mission-install.md`): the world
half restores where-you-are; the project half (five layers, two depth profiles,
paced pointer-walk) restores what-you-are-doing. Composing from named artifacts
keeps source precedence visible.
- **THE CEREMONY IS THE EXPERIMENT.** Every act above runs on the product's own
primitives, so the ceremony doubles as their live test. Observe passively: what did
each re-primed seat's first natural turns reach for, and what broke? Findings land
in one dated observations file beside the boundary record.
## 2. Memory — distill, never accumulate
- Per-seat learned files: a distillation pass. The cut test: "has this changed a
decision recently?" Keep those; move the rest to a dated archive section in the
same file. A learned file at volume stops constraining and starts justifying.
- Any shared agent memory: same pass, same test; retire entries the release
falsified.
- The finished release's mission notes: SEALED AS RECORD, carried as nothing. The
new mission starts thin. Rules do not migrate; requirements do, via the plan.
## 3. Queue — sweep by destination, disposition everything
- Every seat sweeps its DESTINATION for open rows (wakes are unreliable; the sweep
is the read). Each open row from the finished release gets exactly one of:
done-with-receipt · carried-to-the-new-mission (re-filed against the new plan,
never dragged) · closed-superseded. The sweep exposes obligations that a missed
wake or a stale remembered work list can hide.
- Verify every closing-paper inheritance has a live intake row with an owner. Zero
orphans.
## 4. Substrate — teardown as its own atom
- Retain the current release and the immediately previous release. At a boundary
for release N, worktrees and other release-scoped substrate from N-2 and older
are eligible for cleanup. Eligibility is not permission to discard unique dirty
bytes: archive or otherwise disposition those first. Identify live `main` and
runtime state by their actual refs and identity, never by a stale directory name.
- Prune worktrees; remove session-scoped scratch trees; verify merged branches
deleted (merge receipts authorize).
- Scratch homes and scenario roots in temp directories: swept.
- Runtime installs: keep LIVE + LAST-KNOWN-GOOD; archive the hotfix chain;
disposition launcher backups.
- Mid-release vendored-file backups removed once the release containing the real
fix is live.
- Snapshots of torn-down topology: indexed in one line each, or deleted.
## 5. Instruments and boards
- The finished release's tracking and planning boards: FROZEN as records, labeled
so. The new mission gets fresh boards — source-of-truth markdown first, board
derived.
- Frozen review criteria: archived beside their evidence, marked "available,
unweakened, not gates."
- Capture ONE clean-box baseline receipt at mission start (load, process counts,
health tail latency) — the next performance question deserves a before.
## 6. The capability-delta lifecycle (standing law)
Each release's capability delta is authored at the fence and bound to the exact
candidate, then reconciled to the published cut. Its baseline is the canon's actual
last-absorbed marker, not an assumption that the immediately prior semver was
absorbed. When canon is behind, the new delta is a cumulative synthesis of every
intervening delta, preserving supersessions and pivots rather than concatenating
old prose.
Canon absorbs the kept public-product truths. Expiry requires BOTH the canon header
naming that exact delta AND a distinct successor delta file existing. A header alone
does not expire it; missing or unreadable inputs remain unknown/live. At the actual
boundary, verify the event before recording expiry, then stop citing the expired
delta. Do not create a successor merely to make the condition true. Candidate
absorption is not proof of publication or served production adoption.
Private project topology, host practice, and release ownership go to the versioned
project world instead of leaking into the public package. The new release's delta
obligations start at ITS cut.
## 7. World absorption and discoverability
- Every changed capability gets one small map: **user situation · current capability
· authoritative source · natural first product encounter · durable context home ·
clean-agent probe**. Record an already-good route and leave it alone.
- Prefer point-of-need truth from the existing `--help`, error, command output, live
state, context catalog, work install, or skill router. The world install supplies
width and ontology; it should not copy exact facts a live surface can derive.
- The release mission owns this review before its cut when possible, so public
world/help changes ship with the capability. This boundary verifies the exact
packaged result, repairs any missed absorption as explicit debt, absorbs private
project truth, re-primes, and observes whether a clean world-installed agent
reaches the capability from its situation without being told its name.
- A missing route is a release finding. Repair the smallest existing seam or deposit
the evidenced gap into the derivable-context mission; do not create a second
capability registry, search service, or world store at the boundary.
## 8. What deliberately does NOT happen at the boundary
- No process migration: situational rules expired at the fence. The standing corpus
is the four checking principles — verify by effect · evidence at source · honest
verdicts including against yourself · never fabricate.
- No re-litigating the finished release's verdicts; the sealed paper is the record.
- No additions to this checklist without a lived specimen attached.
+24 -8
View File
@@ -1,7 +1,7 @@
# RigBundle Reference
Version: 2 (pod-aware)
Last validated against code: 2026-04-11
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.
### Install a bundle
@@ -222,12 +229,20 @@ rig bundle install <bundle-path> [--plan] [--yes] [--target <root>] [--json]
| Flag | Required | Default | Description |
|------|----------|---------|-------------|
| `<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. |
| `--json` | no | `false` | Emit machine-readable JSON. |
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.
### Launch directly
@@ -237,8 +252,9 @@ rig up <bundle-path> [--target <root>] [--cwd <dir>]
`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.
+92 -10
View File
@@ -174,7 +174,8 @@ edges:
| `name` | string | yes | — | Rig name. Used in session naming (`{pod}-{member}@{name}`), snapshot identification, and spec library lookup. |
| `summary` | string | no | — | Human-readable description. Shown in spec library, review surfaces, and `rig specs show`. |
| `culture_file` | string | no | — | Relative path to a rig-wide culture/constitution file. Must be a safe relative path (no `..`, no absolute). |
| `permission_policy` | string | no | — | Permission policy attached to the rig. Either a built-in (`builtin:locked`, `builtin:standard`, `builtin:open`, `builtin:operator`) or a safe relative path to a custom policy file (resolved from this spec's directory; no `..`, no absolute). Absent leaves the default floor. A member may set its own `permission_policy`, which takes precedence over the rig-level one. See "Attaching a permission policy" below. |
| `permission_policy` | string | no | — | Permission policy attached to the rig. Either a built-in (`builtin:locked`, `builtin:standard`, `builtin:open`, `builtin:yolo`) or a safe relative path to a custom policy file (resolved from this spec's directory; no `..`, no absolute). Absent leaves the default floor. A member may set its own `permission_policy`, which takes precedence over the rig-level one. See "Attaching a permission policy" below. |
| `managed_blocks` | map | no | `CLAUDE.md` | File that receives OpenRig's managed instruction blocks for Claude Code members. Only the `claude-code` key is accepted, with `CLAUDE.md` or `CLAUDE.local.md`. Codex members always use `AGENTS.md`. See "Choosing the Claude instruction file" below. |
| `docs` | Doc[] | no | — | Documentation files that should travel with the rig. Included in rig bundles. Each entry has a `path` field (safe relative path). The engine does not consume these — they are for humans and agents setting up the environment before launch. |
| `startup` | StartupBlock | no | — | Rig-level startup files and actions. Applied to all members via the startup layering model. |
| `services` | ServicesBlock | no | — | Optional managed services (Docker Compose). When present, services boot before any agent launches. |
@@ -193,12 +194,53 @@ permission_policy: builtin:standard
permission_policy: policies/my-cautious-dev.policy.md
```
Built-in policies (`locked` / `standard` / `open` / `operator`) are read-only and are
Built-in policies (`locked` / `standard` / `open` / `yolo`) are read-only and are
referenced as `builtin:<name>`. A custom policy lives in your own project and is
referenced by a safe relative path (no `..`, no absolute). A shipped example of the
custom shape is `packages/daemon/policies/examples/my-cautious-dev.policy.md` — copy it
into your project and edit it to taste.
This records a selection, not a live permission change. Flag-surface policies
select launch flags; config-surface policies still need native configuration
application and inspection. In particular, `builtin:yolo` selects Codex's
`danger-full-access` sandbox and `never` approval policy, and replaces
any `codex_config_profile` argument. See [practical permission choices](getting-started.md#opt-in-permissive-operation).
An explicit `rig seat set-permissions` choice overrides member/rig policy for
future managed launches of that stable seat; it does not rewrite this spec or
its inherited policy provenance. `inherit` removes that override. See
[per-seat permission mode](getting-started.md#per-seat-permission-mode).
### Choosing the Claude instruction file
OpenRig writes its instructions for Claude Code members into managed blocks in
the member's working directory. By default the file is `CLAUDE.md`. If your
repository tracks `CLAUDE.md`, write the blocks to `CLAUDE.local.md` instead:
```yaml
managed_blocks:
claude-code: CLAUDE.local.md
```
Claude Code loads `CLAUDE.local.md` from the working directory as well. By
convention the file is kept out of git, for example with a `.gitignore` entry.
- Accepted values are `CLAUDE.md` and `CLAUDE.local.md`. Any other value or
runtime key is rejected before a member launches.
- The setting applies to launch, restore, relaunch, handover, adding members,
and export. `rig down` removes OpenRig's blocks from the selected file only.
- OpenRig never edits, moves or deletes blocks in the other file.
A rig that already wrote blocks into `CLAUDE.md` keeps them there after you
switch. Until you remove them, `CLAUDE.md` stays modified and Claude Code loads
both copies. Delete each `<!-- BEGIN OpenRig MANAGED BLOCK: … -->` …
`<!-- END OpenRig MANAGED BLOCK: … -->` section by hand and keep the rest of the
file. If `CLAUDE.md` has no other uncommitted edits you need to keep, you can
instead run `git restore CLAUDE.md`; that command discards every unstaged change
to the file, not only OpenRig's blocks. Running `rig down` on a rig that still uses
the default is not a substitute: it strips every OpenRig block from that
directory's `CLAUDE.md`, including blocks written by other rigs.
---
## Pod
@@ -229,6 +271,7 @@ into your project and edit it to taste.
| `id` | string | yes | — | Member identifier. Must not contain dots. Must be unique within the pod. |
| `agent_ref` | string | yes | — | Reference to an AgentSpec. Must start with `local:` (relative) or `path:` (absolute). Exception: `builtin:terminal` for infrastructure nodes. |
| `profile` | string | yes | — | Profile name from the referenced AgentSpec. Use `default` for the default profile. Exception: `none` for terminal nodes. |
| `codex_config_profile` | string | no | — | Codex-only native profile passed as `-p <name>`; letters, numbers, `_`, `.`, `-`. Separate from the AgentSpec `profile`. With the normal launch mode, this replaces OpenRig's explicit workspace-write sandbox flag. A full-bypass policy instead emits danger-full-access and omits this profile argument. |
| `runtime` | string | yes | — | Agent runtime. Current supported values: `claude-code`, `codex`, `terminal`. |
| `cwd` | string | yes | — | Working directory for the agent. Resolved relative to the rig root (the directory containing the rig spec). Use `"."` for the rig root itself. Can be overridden at launch time with `rig up --cwd`. |
| `label` | string | no | — | Human-readable member name. Shown in UI when present. |
@@ -343,12 +386,51 @@ Startup blocks can appear at three levels: rig, pod, and member. They are merged
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `type` | string | yes | — | Action type. One of: `slash_command`, `send_text`. Note: `shell` is explicitly NOT supported in v1. |
| `value` | string | yes | — | The command or text to send. |
| `phase` | string | no | `after_files` | When to execute. One of: `after_files` (after startup files are delivered), `after_ready` (after harness readiness check passes). |
| `type` | string | yes | — | Action type. One of: `slash_command`, `send_text`, `startup_proof`. Note: `shell` is explicitly NOT supported in v1. |
| `value` | string | yes | — | Command/text to send, or `authenticated` / `none` for `startup_proof`. |
| `phase` | string | no | `after_files` | When to execute text/commands. One of: `after_files` (after startup files are delivered), `after_ready` (after harness readiness check passes). Proof selection is resolved before projection regardless of phase. |
| `idempotent` | boolean | yes | — | Whether this action is safe to replay on restore. **Required field.** Non-idempotent actions must NOT include `restore` in `applies_on`. |
| `applies_on` | string[] | no | `[fresh_start, restore]` | When this action runs. Subset of: `fresh_start`, `restore`. |
### Startup proof selection
Startup adds no orientation exercise by default. To select the authenticated
startup challenge, declare an action in an agent, profile, rig, pod, member, or
operator startup block:
```yaml
startup:
actions:
- type: startup_proof
value: authenticated
idempotent: true
```
Use `value: none` in a later layer to select lean startup explicitly. The last
applicable declaration wins in agent → profile → rig → pod → member → operator
order. Culture contributes files, not a proof selection. With no applicable
declaration, the result is `none`; the number of startup files never selects
proof. Invalid values and non-idempotent proof declarations fail validation,
including declarations overridden later. These actions declare policy and are
never typed into a terminal.
An authenticated selection challenges only a fresh or fresh-fallback managed
agent launch. Resumed, forked, rebuilt, and adopted sessions receive no new
challenge; terminal nodes never receive one. `applies_on` follows the requested
startup context, so a fresh fallback during restore uses `restore` selections.
Keep the default `[fresh_start, restore]` to cover both fresh launch paths.
Identity delivery, projection, readiness, and ordinary startup actions still
run. `startup_status: ready` means startup completed, while `oriented: missing`
means a selected proof awaits authenticated submission. Omission/`none` yields
`oriented: n-a` on a new fresh launch and retires an older challenge without
deleting its audit history. Retirement follows successful harness launch,
before readiness checks, so attention, timeout, or a readiness exception cannot
retain the preceding proof. A replacement that fails to launch does not retire
the current proof; resume/adoption also preserves existing proof history.
The effective selection is recorded on `node.startup_pending`; actions are
persisted in startup context for restore and fresh relaunch.
---
## Services Block
@@ -477,7 +559,7 @@ These rules are enforced by the validator. A spec that violates any of these wil
19. Each wait target must define exactly one of: `service`, `url`, `tcp`.
20. `condition` is only valid on `service` targets and must be `healthy`.
21. Startup file `delivery_hint` must be one of: `auto`, `guidance_merge`, `skill_install`, `send_text`.
22. Startup action `type` must be one of: `slash_command`, `send_text`. (`shell` is explicitly rejected.)
22. Startup action `type` must be one of: `slash_command`, `send_text`, `startup_proof`. (`shell` is explicitly rejected.) Proof selection requires `value: authenticated` or `none` and `idempotent: true`.
23. Startup action `phase` must be one of: `after_files`, `after_ready`.
24. Startup action `idempotent` is a required boolean.
25. Non-idempotent actions must not include `restore` in `applies_on`.
@@ -491,8 +573,8 @@ These are the built-in specs shipped with OpenRig. Read them as worked examples.
| Spec | Location | Pods | Members | Services |
|------|----------|------|---------|----------|
| `product-team` | `packages/daemon/specs/rigs/product-team.yaml` | orch, dev, rev | 7 (lead, peer, impl, qa, design, r1, r2) | no |
| `implementation-pair` | `packages/daemon/specs/rigs/implementation-pair.yaml` | dev | 2 (impl, qa) | no |
| `adversarial-review` | `packages/daemon/specs/rigs/adversarial-review.yaml` | orch, rev | 3 (lead, r1, r2) | no |
| `research-team` | `packages/daemon/specs/rigs/research-team.yaml` | orch, research | 3 (lead, analyst, synthesizer) | no |
| `product-team` | `packages/daemon/specs/rigs/preview/product-team/rig.yaml` | orch1, dev1, rev1 | 7 (lead, peer, impl, qa, design, r1, r2) | no |
| `implementation-pair` | `packages/daemon/specs/rigs/launch/implementation-pair/rig.yaml` | dev | 2 (impl, qa) | no |
| `adversarial-review` | `packages/daemon/specs/rigs/focused/adversarial-review/rig.yaml` | orch, review | 3 (lead, r1, r2) | no |
| `research-team` | `packages/daemon/specs/rigs/focused/research-team/rig.yaml` | orch, research | 3 (lead, analyst, synthesizer) | no |
| `secrets-manager` | `packages/daemon/specs/rigs/launch/secrets-manager/rig.yaml` | vault | 1 (specialist) | yes (Vault) |
@@ -0,0 +1,95 @@
# Scoped operating posture
Human-led work is quiet by default for process-only diagnosis. Delegated work can
receive oversight during planning as well as implementation. This preference does
not grant execution authority or change permissions, work phase, workflow reminders,
context/continuity health, or health policy.
## Inspect and choose
Use the existing `rig mode` surface. Give an exact rig or authored work scope:
```sh
rig mode effective --rig my-rig --json
rig mode effective --project my-project --mission release-1 --json
rig mode effective --qitem my-packet --json
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.
Project selection reads `workspace.yaml`'s declared `projects: [{id, root}]`
catalog, or the workspace's own `project.yaml` when no catalog exists. Multiple
projects require an explicit project selection. A sole project may be derived.
The project manifest validates identity and supplies `missions.root` (default
`missions`). Mission and slice identities come from those authored work nodes;
missing, duplicate or conflicting identities remain unknown. The selected work
must stay inside its canonical project root.
Qitem reads join explicit `project:`, `mission:`, `slice:`/`workstream:` tags,
destination rig, and the existing workflow packet binding. Workflow lifecycle
project/mission identity must agree with the tags and requested scope. The bound
packet's exact step is the phase source; phase names are not classified by guess.
An unlinked qitem does not silently inherit the default. Scope reads are observational.
Legacy ergonomic modes (`sleep`, `desk`, `mobile`, `away`, `focus`, `debug`) retain
their bindings and ten-field records. They do not imply either operating posture.
Only human-led/delegated bindings participate in posture precedence. The legacy
`effective` and `posture: known|unknown_posture` fields remain for compatibility;
`unknown_posture` there means no legacy mode binding and does not override the
new `operatingPosture.source: product-default` result. Setting another mode at the
same scope replaces that row; inspect effective posture after any mode change.
## Diagnosis effects
Process-category findings remain visible under human-led or unknown posture, but
new diagnosis presentations, re-presentations and human notifications require
explicit delegated posture. Retained occurrences are not canceled or rewritten
when posture changes. Their current finding is refreshed for inspection and
notification; unavailable current findings cannot reuse old delegated posture.
Notification admission is rechecked after readiness I/O.
Delegation is necessary for process interruptions and is insufficient by itself:
enabled diagnosis policy, detector selection, source freshness, custody, cooldown,
recurrence limits and human delivery readiness still apply. Other health categories
and ordinary queue/workflow reminders retain their existing behavior. The automatic
ceremony source reads declared catalog projects through the same resolver; unresolved
lineages remain visible with unknown scope instead of acquiring invented authority.
See [health diagnosis](health-diagnosis.md) for policy, evidence and disposition.
+116 -82
View File
@@ -1,52 +1,16 @@
# SDLC conventions — the markdown control plane
> **Succession rule (source of truth):** this document was derived from the
> corrective review-surface redesign spec
> (`CORRECTIVE-REDESIGN-review-surface-2026-07-05`, DRAFT v0.2, §2–§6,
> grounded against openrig main `c27f2aff`, 2026-07-05/06). **Once shipped,
> THIS repo document is the living SSOT for the SDLC conventions going
> forward**; the corrective spec is the historical design record. Scaffold
> templates, the advisory scope audit, the `mission-slice-sop` skill, and the
> shipped bootstrap overlay all point HERE — they must not restate this
> document in full (restatement is how drift is born).
> **Source of truth:** this repo document is the living SSOT for the SDLC
> conventions. Scaffold templates, the advisory scope audit, the
> `mission-slice-sop` skill, and the shipped bootstrap overlay all point HERE —
> they must not restate this document in full (restatement is how drift is born).
OpenRig is a software factory the human steers from a high altitude. The human
records intent; agents turn intent into a plan, the plan into a build, the
build into proof. The Living Notes UI is a **plain projection of well-formed
markdown on disk** — agents change the files, the UI re-projects. These
conventions define "well-formed." Everything here is **advisory / fail-open
for agents**: nothing below blocks a write; the audit records and advises.
## WHERE YOU ARE DECIDES WHICH LOOP YOU RUN — the VM inner loop vs the host outer loop
The parent host's SDLC was being applied wholesale to the VM. **They are different worlds with
different responsibilities**, and conflating them is what turned this document into friction.
| | **INNER LOOP — the VM** | **OUTER LOOP — the parent host** |
|---|---|---|
| **question it answers** | *does it actually work?* | *does what was delivered match what was intended?* |
| **method** | ground → fix → upgrade in place → **run it and look** → iterate | intent-vs-delivered comparison, commit-message review, privacy/leak scrub, release management, publish |
| **definition of done** | to the best of our knowledge the code is **completely functional and works**, and needs **no heavy refactoring** to be release-ready | the release is shippable and honest |
| **ceremony** | **Part A only.** Part B is not the default here. | **Part B applies.** The release itself earns it. |
**WHY the ceremony belongs there and not here — the rule that makes this self-enforcing:** the
proof ceremony is a **protocol for transmitting confidence to someone who was not there.**
Plan-lock, proof contracts, C1 drops and intent-vs-delivered all exist so a reader who did NOT
watch the work happen can verify that it did. On the host that is exactly the situation, so the
artifacts must carry the confidence. **On the VM the builder IS there** — they ran it and looked at
it. Running the ceremony there re-encodes, at high cost, something already known, for an audience
standing in the room.
**WHERE A FIX GOES — go where the validation is POSSIBLE, not where the ceremony lives:**
- **Needs a live daemon to verify → the VM.** That is the only place it CAN be verified. (Worked
example: proving the ACTIVITY telemetry was really fixed required cutting the runtime over and
reading real values; no amount of host-side review could establish it.)
- **Verifiable by reading — docs, comment scrubs, message hygiene → the host.**
**The VM's north star for a release:** hand the host code that, to the best of our knowledge, is
completely functional and works. Not a proof pack. **Effect-verified functionality.**
build into proof. The TUI projects authored work on disk and, where an owning
proof policy is selected, derives readiness from recorded judgments. These
authoring conventions are **advisory / fail-open for agents**: the audit
records and advises; the selected proof policy separately governs who may judge.
## HOW TO READ THIS DOCUMENT — two parts, and only one of them is the default
@@ -54,11 +18,43 @@ completely functional and works. Not a proof pack. **Effect-verified functionali
| | applies | contents |
|---|---|---|
| **PART A — THE SIMPLE SDLC** | **ALWAYS. This is the default.** | artifact conventions, proportionality, the advisory audit, the honesty rails |
| **PART B — THE RIGOROUS OVERLAY** | **ONLY when the founder, or an orchestrator relaying the founder, ASSIGNS the heavy path to a NAMED piece of work** | the proof-contract format, the two locks, C1 proof drops, the locked role contracts |
| **PART B — THE RIGOROUS OVERLAY** | **ONLY when the human operator, or an orchestrator relaying that decision, ASSIGNS the heavy path to a NAMED piece of work** | the proof-contract format, the two locks, C1 proof drops, the locked role contracts |
**You may not select Part B for yourself.** If you believe something earns it, say so in ONE
sentence and continue on Part A until told otherwise. Part A corresponds to the `mission-slice-sop`
skill; Part B to `mission-slice-intent-proof-sop`.
skill; Part B is defined below and applies only when assigned.
## THE COMPONENT MENU — this document is a menu, not a pipeline
The SDLC is assembled from **components chosen per mission**, not a single mandated flow.
This document describes the components and carries recommendations; a mission (or the team
running it) picks per phase. Multiple valid options can exist for the same phase — choosing
one does not deprecate the others. Nothing below deletes or overrides an existing component.
| phase | components available today |
|---|---|
| **PRODUCT / MISSION PREPARATION** | **The product-management pass** — the layer above planning, run by the advisor/PM seat before planners touch a mission. The build is a lossy compression pipeline (PM intent → planner spec → builder code); nothing leaks downward by proximity, so this pass grounds in the raw sources, synthesizes ONE scope of record, presents owner decisions decision-ready, mints slices at plan time, deposits the full design contract INLINE in each design-bearing spec (owner words verbatim, settled decisions, evidence, shape of done, anti-goals), routes the complete reading list and internet-research assignments from the spec itself, frames each slice's planning-dial rung with its reason, and rereads every spec as the narrow downstream reader. The measuring stick: SPEC.md alone, plus what it explicitly routes to, reproduces the intent in a reader with none of the author's context. Full reference: `product-management-pass.md`. |
| **PLAN** | **The planning dial** — planning rigor is a spectrum chosen per piece: P0 mini-requirements and pointers (simple, reversible work) · P1 an authored spec with a proof contract (the default) · P2 plus a research round before the spec freezes, run BEFORE build dispatch · P3 plus an adversarial pass by a non-author, judged against product goals, landing amendments with proof-contract teeth · P4 plus a blind from-scratch design diffed against priors before proceeding. Dial up by what the piece is, exactly as build care is priced below. Full reference: `planning-dial.md`. |
| **BUILD** | **Part A, the simple flow** (below — the default) · **product-journey components** — a mission-selectable menu for greenfield work, drift recovery, public journey probes, one evidence-gated look at the layer below, minimal repair, independent QA/review, integration, and installed smoke; compose only what the work needs in mission/slice YAML, never as a new mode or mandatory pipeline. Full reference: `product-journey-sdlc.md` · **the wave model** — slices build in parallel in disjoint file territories, an integrator merges serially, and independent review fires once per wave (two reviewers with different vantages, never the writers; fix rounds re-earn verdicts at one final revision; per-slice discipline stays failing-test-first and verify-by-effect). The wave IS the care dial: what shares a wave decides how much review each piece gets · **Part B, the rigorous overlay** (below — only when assigned). These combine: a wave can carry one Part-B piece. Full wave reference: `wave-sdlc.md`. |
| **DISPATCH DATA** | **The execution-data conventions (S27)** — three tiny data disciplines that make execution state derivable instead of hand-tabulated, consumed by `rig view show execution`. **EC-1, edges in frontmatter:** every open slice carries its hard edges in `depends_on:` (an inline JSON array; an explicit `[]` is a statement, absence is INDETERMINATE); soft/serialization edges are one machine-readable Territory line, exactly `SOFT-AFTER: [ids] — reason`. **EC-2, wave-as-data:** wave composition + review model live on a durable queue row tagged `wave-map` + `format:wave-map-v1` whose body carries one fenced ```json block (`{format:"wave-map-v1", mission, waves:[{id, slices, serialized_order?, review_model?}]}`) — composition only, never status labels (status derives at read time); the planning dial recorded at lock surfaces as the frontmatter field `approved-spec-dial:` beside the approve stamp. **EC-3, dispatch names the worktree:** build batons carry the exact body line `worktree_path=<path>` from dispatch onward — it is Q1's join key; batons without it join by naming only and the view marks them `fragile_join`. Existing batons are never rewritten. |
| **RELEASE** | The release ceremony conventions (publication identity, honest release notes) and the CAPABILITY-DELTA LIFECYCLE LAW: each release's delta is authored at the fence and bound to the exact candidate, then reconciled to the published cut. Expiry requires both the canon header naming the absorbed delta and a distinct successor delta existing; a draft header alone is insufficient. Verify the actual boundary event and stop citing an expired delta. Full statement: `release-boundary.md` §6. |
| **BOUNDARY** | **The release boundary** — housekeeping run once per fence so the next release starts clean: seats reset or re-primed per their continuity needs, memory distilled (keep what changed a decision recently), every queue swept by destination and dispositioned, substrate torn down, boards frozen as records, a clean-box baseline captured. A checklist of judgment guides, not gates. Full reference: `release-boundary.md`. |
> Full reference documents for each component (graduated 2026-08-28): the
> product-management pass — `product-management-pass.md` · the
> planning dial — `planning-dial.md` · the product-journey component catalog —
> `product-journey-sdlc.md` · the wave model — `wave-sdlc.md` · the release
> boundary (including the capability-delta lifecycle law) — `release-boundary.md` ·
> the mission install — `mission-install.md` (lands with its own slice). The
> summaries above remain the one-glance description; this document remains the SSOT
> pointer for the whole menu.
Mission and slice YAML may select component IDs from
`product-journey-sdlc.md` and record explicit edges between them. That YAML is
an address map and execution dashboard prototype, not an enforcement engine.
If it is absent, Part A remains the complete default; agents do not invent a
composition merely to make one exist.
# PART A — THE SIMPLE SDLC (applies always)
@@ -72,10 +68,18 @@ intent (what is this for, what does done look like)
→ hand off (rig queue handoff) or stop
```
One full-breadth review at the end, not per increment. Bring work when it is **done**. The human
should almost never see something that doesn't match what was planned — QA catches mismatches and
kicks them back. When the human looks, they map intent → plan → delivered at a glance, mostly by
scanning screenshots down a single column, and give the final 1% approval.
Verify the whole outcome at completion, not through a review chain per increment.
For a tiny change the builder may hold this check. Independent QA/review runs only
when selected by the project/mission/slice composition or an explicit owner
assignment; for a wave, at its authored boundary. Available roles never add gates.
The authority lookup is `product-journey-sdlc.md#resolve-the-selected-path`.
Bring work with evidence that its promised user outcome actually works.
For a release mission, `mission.yaml` also names the Git strategy before build
work starts: the branch/ref that is cumulative integration truth, its base, and
when that truth is reconciled to main. Continuous integration to main and a
release-line fold followed by one explicit merge are both valid; leaving the
choice implicit is not.
**The heavier flow — proof contracts, plan-lock, C1 drops, proof-lock — is PART B and is not in
force unless assigned.**
@@ -84,21 +88,41 @@ force unless assigned.**
Each slice directory carries:
- **`README.md`** (or the slice body) opening with the three convention
sections, in this order and with these exact headings:
- **`SPEC.md`** — the one authored node file. Its frontmatter carries
`intent:` and may carry `depends_on:` as a list of sibling work-node IDs;
`depends_on` orders siblings and is never followed by the path-only context
trace. The body opens with the three convention sections, in this order and
with these exact headings:
- **`## Intent`** — the recorded intent, verbatim. The UI projects this
text as the INTENT section.
- **`## Mini-requirements`** — the concise, one-glance requirement tier
(numbered list). This is the founder's first structured catchpoint;
(numbered list). This is the human operator's first structured catchpoint;
approval starts here.
- **`## Proof contract`** — a checkbox list of promised deliverables (see
§3). The UI's DELIVERED section pairs each item with its proof.
- **`IMPLEMENTATION-PRD.md`** — the full PRD. It OPENS with the
mini-requirements; everything between intent and proof is **elastic**
(see §7 — for a small slice the mini-requirements may BE the whole PRD).
- **`PROGRESS.md`** — the slice's acceptance checklist: the durable
mission/slice-level to-do checked at acceptance. In-process steps stay in
the working agent's own todo tool.
- **`PROOF.md`** — the retained proof summary, explicitly paired to the
`SPEC.md` proof contract.
- **`proof/`** — the proof-artifact directory. Curated canonical evidence
lands here via `rig proof` with valid C1 headers (§5).
Legacy `README.md`, `IMPLEMENTATION-PRD.md`, and pre-convention trees remain
readable indefinitely. New scaffolds do not create or mirror them, and repair
is additive: it never deletes, renames, or rewrites legacy bytes.
When the owning scope selects `proofPolicy.judges`, use `rig proof judge` to
record an attributed accept, reject or withdraw decision on a contract item,
and `rig proof show` to read derived readiness up through slice, mission and
project. The nearest slice, mission or project policy selects authorized judges;
`rig proof --help` describes setup, selectors and evidence requirements. A
correction preserves history and unrelated judgments without editing ancestor
status checklists. An artifact can identify non-code work without a fabricated
commit. Evidence capture (`proof add`), policy acceptance, higher outcome
judgment and publication are distinct. This optional component does not select
Part B or add a mandatory review gate.
The three sections project into the UI's one review structure: a vertical
stack of **INTENT → PLAN → DELIVERED**. A slice missing a section still
renders (the projection degrades to a muted "—", never invents content) —
@@ -140,21 +164,9 @@ either being wrong (for example, one over the frontmatter block and one over
the whole file). Coverage is what makes a cited hash verifiable rather than
merely impressive.
**Canonical convention (the ONE origin — cite it, do not restate it here),
cited in the form that convention itself prescribes — path + section anchor +
dated whole-file hash:**
**Use path + anchor as the authority and a whole-file hash only to date the
citation.** The address survives legitimate content changes; the digest does not.
- **path:** `artifact-cross-citation-path-plus-lineage.md` (internal authoring convention — not shipped in this public repo)
- **anchor:** *A cited hash STATES ITS COVERAGE — span is part of the definition*
(2026-08-08)
- **hash:** `sha256 14d9a273d542648ab1e0a82c7fd98e8a8c59cb02a83be76b8af9abfb717b34d2`
— **coverage: WHOLE FILE, bytes as stored, INCLUDING the trailing newline**
(0x0a terminal byte verified), 229 lines, as-of 2026-08-08.
The **path + anchor is the AUTHORITY**; the whole-file hash **DATES** the
citation. That artifact is a living doc: the hash goes stale at the next
legitimate append, the anchor does not — the CURRENT-line convention applied to
a citation.
## A4. When implementation finds the intent rested on something that does not exist
A slice's intent can only be honoured to the extent its assumptions hold. Sometimes implementation
@@ -188,7 +200,7 @@ Do not over-engineer this. The entire goal is an accurate story for whoever read
The SDLC has exactly three fixed capture points: **intent** → a
**proportional structured requirement** → **proof**. Everything between is
elastic. For a small slice (a bug fix, a research note), the
mini-requirements may BE the whole PRD — the convention sections must be
mini-requirements may BE the whole specification — the convention sections must be
present so the slice projects, but their contents scale to the work. Gates
are losslessness checks on the decompression from intent to delivery, not
paperwork. Scaffolding emits the sections; it must never mint ceremony.
@@ -203,19 +215,20 @@ reported as unknown, not failure.
## A7. Where the knowledge lives (the four pointers)
- **This document** — the SSOT.
- **Scaffold**: `rig scope slice create` emits the convention sections +
`proof/` + an `IMPLEMENTATION-PRD.md` skeleton for every template kind;
`rig scope mission create` emits the convention pointer.
- **Skills — TWO, matching this document's two parts**: `mission-slice-sop`
teaches **Part A**, the light default; `mission-slice-intent-proof-sop`
teaches **Part B**, the assigned overlay. Assigned-only, never self-selected.
- **Scaffold**: `rig scope slice create` emits `SPEC.md` + `PROGRESS.md` +
`PROOF.md` + `proof/` for every template kind; `rig scope mission create`
emits an intent-bearing `SPEC.md` + `NOTES.md`. Core emits only this
mode-neutral shape; mode plugins add richer material through the extension
seam rather than teaching core a mode.
- **Skill**: `mission-slice-sop` teaches **Part A**, the light default. **Part B**
is defined below and applies only when assigned — never self-selected.
- **Bootstrap**: the shipped agent overlay points fresh seats at the skill
and this document at boot.
# PART B — THE RIGOROUS OVERLAY (assigned only)
> **ENTRY CONDITION:** everything below applies **only** when the founder — or an orchestrator
> explicitly relaying the founder — has assigned the heavy path to a **named** piece of work.
> **ENTRY CONDITION:** everything below applies **only** when the human operator — or an orchestrator
> explicitly relaying that decision — has assigned the heavy path to a **named** piece of work.
> If you arrived here without an assignment, stop and use Part A.
>
> **Rails:** the locks are **two, and only two**. Nothing here authorizes a second gate round on
@@ -264,9 +277,9 @@ Two deliberate stamps, both written by the SAME shipped verb
(`rig scope slice approve`, one daemon-side write path: frontmatter stamp +
append-only audit row land together):
- **Plan-lock:** `rig scope slice approve <slice> --scope spec` — "the PRD
matches my intent; THIS artifact set is what gets built." Pins the locked
artifact set (spec/PRD/mockups) out of everything else in the folder.
- **Plan-lock:** `rig scope slice approve <slice> --scope spec` — "the spec
matches my intent; THIS artifact set is what gets built." Pins `SPEC.md`
(and planned mockups, when present) out of everything else in the folder.
- **Proof-lock:** `rig scope slice approve <slice> --scope delivery`
(the default scope) — the terminal "this is done" sign-off; fires the
freeze.
@@ -332,3 +345,24 @@ what it looks like now" evidence the agent stands behind — bounded, mapped
1:1 (or few:1) to deliverables. The fix-loop's full artifact history stays in
`proof/`, one drill-in down, NEVER in the primary view. The anti-pattern:
an append-only pile where the human can't tell final from superseded.
## Scratch-daemon hermeticity: the DB a scratch daemon opens is the one you named
A scratch daemon launch **strips the inherited environment's `OPENRIG_DB` and names an explicit
scratch DB path under the scratch `OPENRIG_HOME`** — both halves, every launch:
```bash
env -u OPENRIG_DB \
OPENRIG_HOME="$SCRATCH_HOME" \
OPENRIG_DB="$SCRATCH_HOME/openrig.sqlite" \
node <daemon entry> …
```
An inherited fleet DB reaching a scratch daemon is a **harness defect regardless of outcome** —
the scratch context read or wrote state it never owned, and whether anything visibly broke is
luck, not hermeticity.
**The discriminator that keeps this rule honest:** an explicit `OPENRIG_DB` still WINS by design
(`daemon-db-path.ts`) — deliberate split-path configuration remains legitimate and
untouched. This rule governs **inheritance into scratch contexts**, where nobody
decided anything: strip what you did not choose, then choose.
+90
View File
@@ -0,0 +1,90 @@
# Create your own Slack app for OpenRig (experimental)
> **Experimental in 0.6.0.** The manifest (`rig slack manifest`) and this setup guide are new
> and have not been confirmed against a real app creation. The Slack connector itself and its
> `setup`, `verify`, `enable`, `disable` and `status` commands are not experimental. If a step
> here does not match what Slack shows you, please
> [open an issue](https://github.com/mvschwarz/openrig/issues/new/choose) or send a pull request
> ([CONTRIBUTING.md](../../CONTRIBUTING.md)).
OpenRig's Slack connector talks to a Slack app that you create in your own workspace. OpenRig
ships the app's manifest; it does not host an app, run an install endpoint, or publish anything
to the Slack Marketplace. The app is a Socket Mode app, so it lives in the one workspace you
create it in.
`rig slack manifest` prints the manifest. It works offline, before any daemon or token exists:
```bash
rig slack manifest # the manifest as YAML
rig slack manifest --url # Slack's create-app link with the manifest prefilled
rig slack manifest --json # the manifest, its scopes and events, and why each scope is requested
```
The TUI shows the same link on the Connections page while Slack is not configured. Neither the
CLI nor the TUI opens a browser, accepts tokens, or creates an app.
## Steps
These are the expected steps, based on Slack's app-manifest documentation. They have not yet
been confirmed against a real creation run. Slack's form may ask for something the prefill did
not fill in; if so, follow the form.
1. Open the link from `rig slack manifest --url` in a browser.
2. Sign in to Slack if asked, pick the workspace, review the prefilled manifest, and click
**Create**.
3. Under **Socket Mode**, confirm it is enabled. Enable it if it is not.
4. Under **Basic Information → App-Level Tokens**, generate a token with the
`connections:write` scope and copy it (it starts with `xapp-`).
5. Install the app to the workspace and approve the requested scopes.
6. Under **OAuth & Permissions**, copy the **Bot User OAuth Token** (it starts with `xoxb-`).
7. Put both tokens in a private env file readable only by you (`chmod 600`):
```bash
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
```
8. Run `rig slack setup --channel <channel-id> --secrets-env-file <path>`, then
`rig slack verify`, then `rig slack enable`.
9. Invite the bot to the channel you configured (`/invite @OpenRig` in that channel).
## What the app asks for
Run `rig slack manifest --json` for the exact list and the reason for each scope. There are two
groups:
- **Baseline scopes**: posting messages, reading message history in public channels the app is
a member of, and reading channel details.
`rig slack verify` checks these.
- **Feature scopes**: `files:read` (download attachments people send), `files:write` (upload
attachments to Slack), and `app_mentions:read` (receive @-mentions of the app). `rig slack verify`
does **not** check these, so a READY from verify does not prove attachments or mentions will
work.
If a feature scope was not granted, the effect differs by feature:
- **Attachments** (`files:read`, `files:write`): the file download or upload call fails. A
message with an attachment that could not be downloaded is still delivered, with the failed
file named in it. A post whose attachment could not be uploaded still delivers its text, and
the failure appears only in the daemon log (`rig daemon logs`).
- **Mentions** (`app_mentions:read`): Slack does not deliver `app_mention` events to the app, and
nothing in OpenRig reports that they are missing.
So after installing, compare the granted scopes Slack shows for the app with all six scopes that
`rig slack manifest --json` lists.
The app subscribes to messages in public channels it is a member of (`message.channels`) and to
mentions of the app (`app_mention`). It does not request direct-message or private-channel access.
## What the connector does with the tokens
The tokens stay in the env file you created. The connector reads them from that file and uses them
to authenticate to Slack: it opens an outbound Socket Mode connection with the app-level token and
calls Slack's Web API with the bot token. This connector has no OpenRig-hosted component and
makes outbound connections only. That statement is about this Slack connector, not about every
part of OpenRig.
## Next
- `rig slack status` shows what is still missing, without contacting Slack.
- `rig slack verify` checks the granted baseline scopes and channel membership with Slack.
+244
View File
@@ -0,0 +1,244 @@
# Occupant-owned stream classification and shadow capture (experimental)
> **Experimental in 0.6.0, optional and off by default.** The Jev classification path and shadow
> capture may be incomplete or not work, and no accuracy or production-reliability claim is
> made. OpenRig's delivery, watchdog and routing decisions do not use their output. If they do
> not behave as described here, please
> [open an issue](https://github.com/mvschwarz/openrig/issues/new/choose) or send a pull request
> ([CONTRIBUTING.md](../../CONTRIBUTING.md)).
This source provides a bounded receiver for one real classifier occupant. It does
not start an agent, register a watchdog job, or enable capture by default.
Taxonomy, questions and decisions belong to the occupant; the daemon supplies
source facts and enforces its existing lease, execution and idempotency fences.
An explicit optional Jev mode supplies advisory decisions through the same worker.
Experimental labels have no demonstrated accuracy or calibrated confidence and
do not replace delivery, watchdog, or routing decisions.
## Optional foreground Jev experiment
With no configuration, the experiment is off. Choose an explicit local JSON path
in an existing private directory; the file contains only enabled state and bounds:
```sh
rig project experimental status --config ./experiment.json --json
rig project experimental enable --config ./experiment.json --max-requests 3 --timeout-ms 10000 --json
rig project experimental disable --config ./experiment.json --json
```
Enable does not start processing, capture, a daemon or an agent. Status and disable
make no provider request. The invoking process needs `OPENROUTER_API_KEY` in its
environment for an enabled run; this entry never searches credential files or
copies credentials into configuration. Missing credentials fail before attempts
or provider work. Normal key provisioning remains with the operator.
The selected route is `https://openrouter.ai/api/alpha/decisions`, pinned to
`typesafe/jev-1.13`, without provider fallback or HTTP redirects. A request declares
maximum provider prices of $0.05 per million prompt tokens, zero completion and
request price. These are admission ceilings, not a price quote, spend estimate or
credential-access claim. Each foreground run allows 1–20 requests (default 3),
each with a 1–30 second deadline (default 10), 24 KiB request and 256 KiB response
limits. Failed requests consume their count. Nothing retries in the background.
To process stream items from the actual selected occupant:
```sh
rig project wake --project PROJECT --taxonomy ./taxonomy.yaml \
--classifier-version experimental-jev-1.13 --evidence-epoch OWNER_EPOCH \
--limit 3 --experiment ./experiment.json --json
```
Use the taxonomy format described below. Each criterion must have a nonempty
string description; `__unknown__` is reserved. Questions stay outside the daemon.
The request sends the selected item's body, supplied taxonomy questions/criteria,
canonical scope IDs and current roster destinations to OpenRouter. Select inputs
appropriate for that external processing. It does not transmit the credential
in evidence or store it in the classification. The output binds current source,
taxonomy, candidate version, lease, occupant generation and attempt execution.
No action or confidence label is inferred. Duplicate judgment remains manual.
The smaller of `--limit` and the configured request maximum bounds the wake.
Unknown answers remain null; a wholly unknown answer abstains. Invalid model,
provider, answer keys, choices, probabilities or response bytes reject the whole
answer. Probability sums retain the strict 0.001 tolerance; there is no rounding
repair or partial salvage. Unavailable/invalid provider work stops that run with
no classification write. Inspect the returned attempt state: terminal abstentions
are not reopened, and failed/unknown daemon writes must be reconciled through the
existing ledger. Never change the evidence epoch merely to retry an experiment.
Disable is checked before each call and before applying its result. A separate
disable command cannot cancel an already forwarded remote request; its late
result is discarded, with the existing deadline bounding the wait. Ctrl-C or
SIGTERM requests immediate cancellation of the foreground run. An uncooperative
request remains reported pending and cannot cause a late write or a second call
in that run. Remote cancellation or refund is not proven by local cancellation.
The command registers no wake, returns no automatic continuation, and requires
an explicit later invocation to do more work.
For selected observations from an existing shadow archive:
```sh
rig project experimental capture --config ./experiment.json \
--input ./selected-shadow.jsonl --output ./new-advisory-results.jsonl --json
```
This reads at most 8 MiB and considers only the first configured maximum number
of records. It never captures a terminal. The input remains unchanged; output is
exclusively created at mode 0600. Keep prior output and choose another filename
only for a deliberately selected new run. It sends captured post-screen text to
the same provider, and retains the original attempt/node/occupant/pane binding and
capture hash beside the answer. Missing capture or binding is unavailable and
makes no provider call. State answers are experimental hints; delivery remains
`INDETERMINATE` because screen text cannot prove submitted/consumed/effect and the
archive does not retain the original sent text. No result feeds back into live
transport or inventory. Failed runs preserve already written output; inspect
`unavailable`, `remaining`, call count and stop reason rather than treating a file
as proof of success.
## One bounded wake
From the selected occupant, read candidates using the configured project ID:
```sh
rig project candidates --project PROJECT --taxonomy /private/taxonomy.yaml \
--classifier-version VERSION --evidence-epoch OWNER_EPOCH --limit 20 --json
```
The result includes eligible stream IDs, authored scope IDs with exact source
paths/hashes, the current running roster of the occupant's rig, observation time,
and a candidate version. Taxonomy YAML supplies `version` and
`fields.{kind,urgency,maturity,area}` with a `question` and `values` mapping. These
questions are returned to the occupant; draft values are not verified truth.
Confidence has no selectable values until calibration selects that contract.
Destination choices are the current roster sessions plus explicit `pool`.
Null/omitted destination remains unknown; it is not converted into `pool`.
The candidate hash binds the selectable values as well as source and taxonomy
hashes. Packets prepared before this pool correction require fresh preparation;
it does not reopen historical attempts or change the evidence epoch.
Read each eligible item with `rig stream show ITEM --json`. Duplicate candidates
are the latest 100 nonarchived items with exact body-hash evidence references;
their preview is capped at 2,000 characters and marked when truncated. Read the
complete candidate before selecting a duplicate. A retrieval miss means unknown.
Directory names, issue IDs and bare membership never supply scope IDs. Source
errors are explicit, and partial source snapshots cause abstention.
Write an occupant-owned decision file (at most 100 unique item IDs, at most 1 MiB):
```json
{
"candidateSetVersion": "sha256:<version returned by candidates>",
"decisions": [{
"streamItemId": "<eligible ID>",
"bodyHash": "sha256:<SHA256 of the exact UTF-8 item body>",
"decision": { "kind": "classify", "labels": {
"classificationType": "<selected taxonomy value>",
"scopeRef": "<selected canonical mission or slice ID>",
"needsHuman": null
}}
}]
}
```
An explicit abstention is `{"kind":"abstain","reason":"why unknown"}`. Other
label keys are `classificationUrgency`, `classificationMaturity`,
`classificationDestination`, `area`, and `classificationConfidence`. Omitted or
null labels stay unknown. A duplicate needs both `duplicateOfStreamItemId` and
the matching `duplicateEvidenceRef` from the candidates, plus occupant judgment
that it really is a duplicate. An existing item alone is not that judgment.
```sh
rig project wake --project PROJECT --taxonomy /private/taxonomy.yaml \
--classifier-version VERSION --evidence-epoch OWNER_EPOCH --limit 20 \
--decisions /private/decisions.json --json
```
This command actually invokes one `StreamClassificationWorker.wake()` through
the existing HTTP services. It derives sender identity through `DaemonClient`,
requires a current running occupant, and carries the captured generation through
each lease/attempt/classification request. It never invents a daemon holder.
Existing direct HTTP callers keep their documented sender-provenance behavior.
The command refreshes source candidates before acquiring a lease. Missing or
stale decision packets return unavailable without starting attempts; refresh
preparation if the roster, scopes, taxonomy or recent stream changed. During a
wake, an item with no exact body-bound decision, missing sources, or a selected
candidate that is unavailable abstains. This is deliberately strict; continuous
source churn can require repeated preparation and is not a throughput claim.
Each wake processes at most `--limit` items (1–100; default 20), starting with the
oldest eligible work rather than relying on a lossy notification cursor. Durable
terminal attempts stay excluded. A new candidate version does **not** authorize
another pass: only an owner-selected evidence epoch changes that attempt identity.
Retain the candidates output beside the decisions and result so its version is
reproducible. The CLI does not keep a second local ledger.
The result carries per-item outcomes, `moreEligible`, `nextWakeAt`, and a watchdog
`wakeRequest` descriptor with the real session/generation and a receiving
instruction. `registered:false` is literal. A periodic-reminder message asks the
occupant to prepare and run this entry; delivery of that message is not execution.
Registration/placement remains an explicit later action. Honor `nextWakeAt`:
lease maintenance uses TTL/3 opportunities; lease loss or an unavailable service
backs off 60 seconds. The daemon's retry/exhaustion ledger remains authoritative.
The ordinary client bounds each request at five seconds; a timed-out write has
an unknown outcome until the ledger is read, and is never reported as written.
There is no provider in the manual-decision path. The reusable asynchronous worker has one
pending classifier slot; timeout requests cancellation but cannot force it. An
uncooperative classifier blocks further calls in that instance and late results
cannot write. That in-memory pending slot is not cross-process cancellation.
## Separate bounded shadow drain
The production transport and inventory probes accept the same optional observer.
They record only captures already taken, with the node, occupant, pane and capture
target bound before awaits. The ordinary cheap inventory path still takes no
capture. Retained-no-write is a separate event, never delivery evidence.
Activation requires explicitly supplied `OPENRIG_SHADOW_CAPTURE` JSON at a later
authorized daemon start. Absence or invalid configuration leaves capture disabled.
This document supplies no live destination or activation instruction. Required
fields and engineering ceilings are:
| Field | Meaning | Maximum |
|---|---|---|
| `destination` | New absolute JSONL file in a canonical, current-user-owned 0700 directory | Explicit path |
| `maxRecords` | Total reserved sink records | 100,000 |
| `maxBytes` | Total reserved sink bytes, including newlines | 1 GiB |
| `capacity` | Queued observation count | 1,000 |
| `maxQueuedBytes` | Serialized bytes waiting in the queue | 8 MiB |
| `maxObservationBytes` | Serialized bytes per observation | 1 MiB |
All numeric fields are required positive safe integers. These are engineering
capacity limits, not approved corpus sizes or classification thresholds. One
in-flight drain can hold up to 64 additional observations, bounded by the queue's
byte allowance; queue plus drain can therefore retain up to twice that allowance.
The standalone observer defaults are 256 entries, 4 MiB queued and 256 KiB per
observation; explicit production configuration supplies each value.
`rig project shadow-status` only inspects configuration/counters.
`rig project shadow-drain` asks the existing HTTP service to drain up to 64 rows;
it requires an actor and never enables capture. No background drain timer is added.
`rig project shadow-stop` disables new enqueueing immediately, finishes any active
drain and the finite retained queue, then closes the sink. Repeated stop is
idempotent; it cannot enable capture or delete the existing archive. Existing
quota and sink-failure losses remain counted. A slow disk can leave stop pending;
inspect status after a client timeout instead of assuming the flush completed.
Enabling capture again requires a separately authorized daemon start with a new
destination; enabling the local provider experiment does not enable capture.
The file is exclusively created with mode 0600. An existing file is refused,
never overwritten, appended, rotated or deleted. A restart requires a new selected
destination. Private-file creation currently requires POSIX ownership support.
The hot path only performs bounded synchronous enqueue work; it never awaits disk
or drain completion. Slow sinks leave one drain pending; concurrent drains do not
start another writer. Overflow drops new observations. Capacity exhaustion stops
the sink. Errors stop writes, count losses and preserve any partial file; failed
batches are not replayed. `reservedRecords/Bytes` include attempted writes, while
`completedRecords/Bytes` count successful appends. `drained` means removed from
the observer, not necessarily persisted. Queue count drops, byte-bound drops,
record errors, missing/unavailable captures and sink failures remain separate.
Live collection, private destination/retention selection, corpus labels, hosted
calibration, native placement, package/install proof and independent visible /
submitted / consumed / effect evidence remain outside this source entry.
+102
View File
@@ -0,0 +1,102 @@
# The wave SDLC — parallel build, wave-level review, composition as the care dial
This is a selectable build and review model from `sdlc-conventions.md`'s component
menu. The mission owner decides whether it fits the work. Lock stamps in this model
are ADDITIVE: approval appends metadata and never rewrites authored bytes, so a
candidate's identity survives its own approval.
## The model in one paragraph
Slices build IN PARALLEL across every build-class seat, each in a DISJOINT FILE
TERRITORY, each merged serially by one integrator the moment its candidate freezes
with builder-proven receipts. Independent review happens ONCE PER WAVE over the wave's
whole accumulated range — two reviewers with different vantages (ideally different
runtimes), never the writers — and their findings drive fix rounds that re-earn both
verdicts at the round's final revision. Checks stay per-slice (failing-test-first
proof, verify-by-effect, honest receipts); rounds happen per-wave.
## The mechanics
1. **PLAN:** wave entries in the plan of record; the planner shapes specs; the plan
authority locks them. Every slice gets a real workspace or proof has no home.
2. **DISPATCH:** batons fan out with EXCLUSIVE file territories. THE BRIEF IS THE
UNIT: every assignment carries the goal, the acceptance criteria, the complete
context routes (exact paths), the territory — and this standing grant: "Navigate
the implementation yourself: derive what you need, research, spawn readers,
decide your own build order. Ask when stuck; the orchestrator is here to help."
DEFAULT SCOPE IS THE WHOLE SPEC, built in one sustained run to ONE frozen
candidate. Handing out anything smaller requires naming the reason on the brief
itself (an unresolved decision, a live territory conflict, a genuine
fork-in-the-road risk) — an unexplained small chunk is a dispatch defect.
Territory overlap = serialize. Conflict discovered mid-build = STOP AND RULE.
Why: the builder holds the spec and the code; decisions about how to build
belong where the building context lives. Detailed sequencing instructions were
scaffolding when models were weak; today they are a cage, and per-command
routing is the failure mode, not diligence.
THE BRIEF IS A MAP, NOT THE TERRITORY. Distillation is welcome — for a minimal
slice the brief may honestly be richer than the spec — but it is never the
receiver's ONLY context: the builder's first act is self-onboarding from the
routed reading list (the mission-install pattern at slice scale), then checking
the map against the ground it actually stands on. Nobody knows the ground floor
better than the builder; the wide angle lives a layer up. When the ground
disagrees with the map, that is a finding to raise — not a reason to follow
the map off a cliff.
3. **BUILD:** failing-test-first against a pristine base with committed final test
bytes; freeze a single candidate revision; receipts cite the FINAL revision
(re-earned after any rebase).
4. **MERGE:** integrator-only, serially, verified at source each time (candidate
parent equals pre-merge tip; changed files equal declared territory). Merge
announcements are REBASE TRIGGERS for every open candidate.
5. **WAVE REVIEW:** fires when the wave's build completes. Two independent reviewers,
base-scoped at the tip, writers excluded from reviewing their own work. The two
gates ask DIFFERENT questions — does the structure hold, versus does each claim
survive contact with source. That is the design, not redundancy.
Reviewers also review for DRIFT, not only defects: does the built thing still
match the acceptance criteria the brief stated — is this still the doghouse the
owner asked for, or did locally-defensible steps accrete toward a moon base?
Plot-loss is a first-class finding with the same standing as a logic defect.
Reviews also DISPOSE each miss by cause: CONTEXT-GAP — the spec or its routes
lacked what the moment needed (a planning finding; the fix lands in the spec) —
versus JUDGMENT-GAP — the context was there and the call was wrong (a builder
finding; the fix is a check). One word in the verdict artifact. The rate this
produces is the calibration the dispatch shape tunes on.
6. **FIX ROUNDS:** findings become forward-fix candidates (merges stand; merged is
not running). One re-review pass per round re-earns both legs at the final
revision. Narrow rechecks for narrow corrections.
7. **SEAL:** both legs clear at one revision = wave sealed; the next slot opens.
## THE WAVE IS THE CARE DIAL
Wave COMPOSITION is the control surface for how carefully a piece is handled: a
large, complicated, or load-bearing slice gets a wave OF ITS OWN (its review ceremony
fires for it alone); a big piece can even split across waves. This decouples what an
older model conflated: mission and slice workspaces stay flexible organizational
containers; waves are the separate layer that sequences work, assigns it, and prices
its handling care. Care is a per-piece property, never a per-mission one.
## When to use it
Many small-to-medium, root-caused, evidence-backed slices; enough seats to
parallelize; territories that partition cleanly; an integrator who can merge serially
and verify fast.
## When NOT to use it
Design-uncertain or shared-region work (one slice, classic two-leg review, or a
design session first). Anything irreversible within the hour (publish, cutover,
destructive operations) — that is the heavy path. And when no independent non-writing
reviewers exist: nothing ever self-reviews; without them the wave gate is theater.
## Failure modes and mitigations
- **Freeze-merge races:** a candidate freezes seconds before a merge moves the tip.
Mitigation: zero-overlap restack with content-identity proof (stable patch-id) in
an isolated worktree; builder-side self-catch on merge announcements.
- **Evidence theater:** a failing test that fails for the wrong reason reads
identical to a real one. Mitigation: reviewers re-derive the failures
independently; false characterizations retire to a correction history, never a
silent rewrite.
- **Presence-not-absence tests:** asserting the true line exists while the false
line still prints. Rule: pin the ABSENCE of the false claim in every encoding.
- **Phantom checkout status:** merge-by-reference leaves a shared checkout's index
stale. Never build or commit from a shared checkout; sync its index at fences.
+3 -3
View File
@@ -21,11 +21,11 @@ A worktree can therefore report green against code it does not contain.
## Rules
1. **Run `npm install` in each worktree** (~4s, one time). It is not the slow path; it is the
only path that resolves workspace packages to the worktree's own source.
1. **Run `npm install` in each worktree** so workspace packages resolve to the
worktree's own source.
2. **Never symlink `node_modules` from the primary checkout.** It appears to work — builds pass,
typechecks pass — which is exactly the danger.
3. **`npx tsc` with no `node_modules` installs an unrelated 12-year-old `tsc` package** and prints
3. **`npx tsc` with no `node_modules` installs an unrelated `tsc` package** and prints
"This is not the tsc command you are looking for". That message means *no install*, not a
TypeScript error.
4. **Build `@openrig/daemon` before typechecking `@openrig/cli`** in a fresh worktree: cli imports
+42 -3
View File
@@ -33,14 +33,53 @@ For each release:
1. Copy `_template.md` to `vX.Y.Z.md`.
2. Fill in the release summary, included changes, operator notes, known limitations, and verification performed.
3. Create a git tag for the release:
3. Run the **substance gate** at the exact release cut. The gate derives every
file under the shipped pack sources, verifies a hash-bound human judgment
and an individual disposition for each mechanical candidate, scans the
complete npm artifact set derived by the packager, and writes the durable
receipt. The receipt records both artifact and scanned file lists; their
artifact-minus-scanned diff must be empty. The review JSON carries one
entry per file:
```json
{
"surfaces": [{
"path": "packages/daemon/context-packs-src/example/guide.md",
"sha256": "<sha256 of the reviewed bytes>",
"verdict": "ship",
"reason": "Generic product guidance.",
"candidateDispositions": []
}]
}
```
Assemble the package first, then run the named gate from the same clean
worktree with the judge and cut SHA explicit. The assembly emits the
substance roots it actually staged; the gate never carries its own root
list. A non-shipping human verdict is one of `instance-fact`,
`internal-path`, `position-knowledge`, or `lore-class` and carries its
reason in the same per-file entry:
```bash
bash scripts/build-package.sh
npm run gate:substance -- \
--review /path/to/substance-review.json \
--receipt /path/to/substance-receipt.json \
--judge <seat-or-person> \
--cut-sha "$(git rev-parse HEAD)"
```
A missing/stale judgment, an undispositioned candidate, internal substance,
a lore-classed pack, a failed full artifact scan, or any artifact absent
from the scanned set refuses the cut.
4. Create a git tag for the release:
```bash
git tag vX.Y.Z
git push origin vX.Y.Z
```
4. Create a GitHub Release using the same file:
5. Create a GitHub Release using the same file:
```bash
gh release create vX.Y.Z \
@@ -49,7 +88,7 @@ For each release:
--notes-file docs/releases/vX.Y.Z.md
```
5. Publish the npm package if that is part of the release flow.
6. Publish the npm package if that is part of the release flow.
## Guidance
+56
View File
@@ -0,0 +1,56 @@
# OpenRig v0.5.1
## 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).
- GitHub Release: https://github.com/mvschwarz/openrig/releases/tag/v0.5.1
+89
View File
@@ -0,0 +1,89 @@
# OpenRig v0.5.10 — explainable System Health
OpenRig 0.5.10 makes fleet health inspectable without turning observation into
another source of work. Agents and operators can query the same bounded,
evidence-bearing findings in the CLI and TUI, hand uncertain patterns to an
agent for diagnosis, and route a verified human request when project policy
requires it. The release also carries authored mission context through workflow
waits, so lifecycle progress no longer depends on an orchestrator remembering
the ceremony.
## System Health
- **Inspect health at the scope you actually own.** `rig health` supports the
current seat, a stable seat ID, a rig, or the local instance. Each finding
retains its category, severity, status, confidence, source window, freshness,
rule, evidence, policy version, and smallest useful next inspection. Empty
output explicitly does not claim health.
- **See the same truth in the terminal UI.** Instance, rig, and seat views expose
compact health summaries, a HEALTH list, and finding drill-in from the same
canonical records used by the CLI. There is no second score or TUI-only health
authority.
- **Detect high-confidence operational failures without transcript sentiment.**
Deterministic detectors cover process, governance, and context signals such as
queue amplification, authority-boundary traffic, and context pressure. Stable
episode identity, source bounds, freshness, and explicit indeterminate states
prevent missing or stale evidence from reading as healthy.
- **Ask an agent to diagnose the cases code cannot settle.** Opt-in health policy
can present one bounded investigation packet for ceremony amplification. The
assigned agent may inspect beyond the packet, record an attributed disposition,
and request human notification only under configured policy and connector
readiness. Diagnosis traffic is excluded from its own detector input.
## Durable lifecycle and human delivery
- **Carry authored mission context through workflow waits.** A mission can select
an explicit boundary workflow in `mission.yaml`; compilation includes the
project and mission manifests plus declared context references. Waiting steps
can re-present the same packet with bounded exponential backoff, preserving one
obligation rather than manufacturing replacement rows.
- **Select SDLC care from authored components and waves.** Shipped role and pod
guidance follows the project and mission selection instead of inferring a
builder-to-QA-to-review chain from seat names. Role capacity does not create a
gate.
- **Address humans through registered delivery channels.** Registered
`<entityId>@external` addresses expose readiness and a next inspection. A
verified queue create persists the request before checking its receipt, while
posted, pending, failed, and indeterminate outcomes remain distinct. Direct
replies return through the correlated queue thread instead of being rewritten
as terminal input.
## Upgrade and setup notes
- Root, CLI, daemon, and web UI package versions advance to `0.5.10`. The
terminal TUI keeps its independent `0.1.0` version.
- This release adds no database migration beyond the `0.5.9` migration head.
Normal daemon restart is sufficient for schema state; runtime adoption remains
a separate operator decision.
- Health list and explain are read-only. Automated diagnosis is disabled until a
policy explicitly enables it, and no detector automatically retries, reroutes,
relaunches, edits scope, or changes lifecycle state.
- Human delivery requires a registered address and a configured, enabled, ready
connector. Connector setup or verification does not itself send a message.
## Known limitations
- Deterministic shipped detector domains are process, governance, and context.
Behavioral and epistemic conclusions remain attributed agent judgment rather
than opaque scoring.
- System Health observes bounded retained records. Unsupported, clipped, stale,
contradictory, or unavailable inputs stay indeterminate; absence of a finding
is not proof that the system is healthy.
- Mission-boundary workflows carry context and scheduling, but agents retain the
judgment about evidence, publication, and activation. The daemon does not
interpret Markdown receipts or invent the next mission.
- Slack is one human-delivery connector, not the addressing contract, and a
posted receipt proves connector acceptance rather than human readership.
## Release verification
- The accepted product frontier is exact commit
`b26419182d98f65ddc8f5cf1997d0b3a9bfe6f0f`, tree
`7b311e5dca2086266a682b7ebb43f68a70344d3b`.
- Mission evidence includes positive, negative, stale, and indeterminate health
fixtures; CLI/TUI parity; an author-excluded diagnosis, human-request, reply,
and workflow-wake journey with zero real external sends; and live negative
dogfood of the 5/10/20/40/60-minute workflow backoff.
- Release preparation rebuilds and tests the exact clean versioned descendant.
Publication, tagging, pushing, and runtime adoption are separate lifecycle
actions and are not implied by this private cut.
+101
View File
@@ -0,0 +1,101 @@
# OpenRig v0.5.11 — clearer first use, workflows, and everyday operation
**Status:** release candidate. Publication and final release verification are
separate steps.
OpenRig 0.5.11 makes it easier to start useful work in an ordinary repository,
understand the team and its workflow in the TUI, and diagnose configuration or
lifecycle failures without mistaking uncertain state for success.
## First useful work and a shared view
- The `first-project` starter provides a focused Codex owner/checker pair for
one repository. The README follows that path from launch through a useful
change, independent checking, and returning to the work.
- A new kernel includes a shared TUI terminal. `rig tui --shared` attaches to
that existing view; plain `rig tui` opens a standalone view. Joining the
shared view does not silently launch a missing terminal or agent.
- Herdr users can follow the same repository and terminal journey. This is
preparation for useful operation without a plugin; no Herdr plugin or
community-adoption result is part of this release.
## Understand work in the ordinary TUI
- Mission views expose workflows, their current step and owner, waits, and
the work packet behind a transition. Source navigation connects the view
to the authored work and specs.
- Specs presents the declared purpose, including a rig's `summary`, and
distinguishes missing content from a failed read.
- Connections shows the running daemon and CLI identities, effective instance
settings, configured and applied gateway state, and registered human routes.
It is a passive inspection surface; a configured connector is not proof
that a person received a message.
- Command completion, readable Recent activity, and a persistent configurable
timezone make daily navigation easier. The default display timezone is
America/Los_Angeles.
- A timed-out daemon probe stays unverified throughout the TUI chrome instead
of being labeled as a confirmed outage.
## More reliable coordination and lifecycle
- Project-owned release profiles provide reusable ceremony and post-release
boundary steps. Current-release closure does not depend on inventing a
successor mission. Agents still judge the evidence behind each obligation.
- Workflow exception recovery preserves the correct owner and clears resolved
exception state. Stuck-work findings retain the source evidence needed to
route them honestly.
- Terminal delivery preserves long authored context; paced-walk verification
checks the intended piece and the applicable runtime/generation record.
Delivery and native-turn completion remain distinct from comprehension.
- Context profile and work-install commands accept consistent Claude runtime
spellings (`claude-code` and `claude`) alongside `codex`, with invalid
explicit values rejected before projection.
- Startup uses the selected authored proof. It does not add an unrequested
exercise, and terminal-only members do not receive agent proof challenges.
- Daemon shutdown has a bounded budget and an attributed outcome receipt.
Failed or missing receipts remain visible on the supported stop paths.
- Daemon startup binds success and state publication to its actual child,
handles competing starts, and retains uncertainty when child liveness
cannot be established. Cross-host request attribution identifies the origin
independently of the selected destination.
## Operator notes
Root, CLI, daemon, and web-UI package versions are 0.5.11. The terminal TUI
retains its independent 0.1.0 package version. Node requirements are unchanged.
The ordered database migration head remains `079_workflow_lifecycle_parallel`;
this release adds no migration beyond the 0.5.10 source baseline.
Diagnosis `show` and `list` now return summary-depth `--json`, retaining current
state and decisions while identifying omitted evidence. Consumers needing the
complete record must use `--full --json`, or execute the printed expansion
command. Queue `show` retains its 512-codepoint body preview and adds explicit
size, completeness, and expansion metadata. HTTP responses and mutation JSON
remain complete. These defaults are not a global output-size cap.
Use the supported upgrade procedure with a database backup and continuity
checks. A daemon restart and a rig shutdown have different effects; the latter
stops seats. Existing kernels are not automatically rebuilt to acquire the new
shared terminal.
## Validation scope and limitations
The candidate retains earlier isolated, installed-package evidence for the
connected first-project/workflow/TUI journey and focused verification of the
purpose and unverified-state corrections. Those earlier runs are attributed
prior evidence, not new end-to-end or provider enrollment tests for this cut.
Package identity and bounded installed rendering checks accompany candidate
preparation; exact-cut substance and final release verification remain required
before publication.
- First-use evidence includes fresh re-entry; it does not establish
same-conversation resume or complete mixed-runtime enrollment.
- Controlled Connections evidence and passive inspection do not establish
external human delivery. Transient probe latency and delayed view population
remain observed qualifications, with no universal responsiveness claim.
- Authored completion, legacy provenance, and proof pairing are separate
readers. Updating a declaration does not create missing provenance or an
execution-wave history.
- A bounded health-analysis proof of concept produced an attributed diagnosis
and exited. It is research evidence, not a shipped cold-agent/run API,
provider abstraction, or proof of a cost or latency advantage over residency.
+114
View File
@@ -0,0 +1,114 @@
# OpenRig v0.5.12 — coherent work, quiet waits, and a normal TUI front door
**Status:** release candidate. Exact-cut substance review, final release
verification and publication remain separate steps.
OpenRig 0.5.12 connects everyday workflow actions to useful progress views,
recoverable ownership and a readable TUI. The goal is to let an operator or
agent start, understand, adapt and return to work without maintaining status
copies or knowing a private recovery recipe.
## Record a judgment once
An authorized, evidence-backed proof judgment derives the affected proof,
slice, mission and project readiness. Correcting that judgment updates its
dependent views while retaining unrelated work and the original evidence.
Code, attributed patch-equivalent work and non-code artifacts can carry
truthful provenance. A checkbox, uploaded file or closed queue item does not
become semantic acceptance, and readiness does not mean publication.
## Know which graph is running
Ordinary workflow inspection distinguishes authored scope from the bound
running graph, advisory ordering from executable dependencies, and planning
waves from runtime steps. `rig workflow revise` previews supported changes
before applying them, preserving completed work and current custody.
Incompatible edits name the decision that needs reconsideration. Operation
identity and `rig workflow operation` recover an ambiguous creation/revision
result without duplicating its effect. Selected development-method guidance
returns with workflow continuation, including the original story and useful
next action; the daemon does not judge the meaning of agent-authored prose.
## Give failures an owner and a recovery path
Exception readiness explains the selected owner before failure and keeps
missing configuration, a genuine no-match and unavailable routing evidence
distinct. Routing uses authored agent identities or registered human fallback.
An unhandled dependency-branch failure records its occurrence, owning task
and wake together, even when another branch remains live. A failed nudge does
not erase the task. After correcting the cause, use `rig workflow resume`
with `--occurrence <failed-qitem-id>` to select that failure: its obsolete
exception closes in the redrive transaction, while other unresolved failures
and completed work remain visible.
## Wait quietly without losing work
Healthy unchanged waits avoid repeated full-context packets. Overlapping
notifications for the same actionable episode share presentation and retain
provenance. Changed blockers, missed delivery and stranded work retain bounded
recovery or escalation. Ordinary reads expose the current owner, blocker and
next useful decision first, with explicit expansion for complete evidence.
Unknown activity remains unknown; a reminder is not progress.
## Start and return through the same TUI
Bare interactive `rig` is the normal front door, including first setup,
daemon-down startup and return after a restart. The TUI shows the intended
instance and observed prerequisites, offers daemon startup, and supports
selecting the rigs and seats to return to work. Kernel is recommended first;
unselected rigs keep their histories. Resume preserves the selected seat's
recorded conversation when available. An unavailable resume offers a named
repair or an explicitly confirmed fresh, re-primed conversation; it is never
silently replaced. Launcher and native startup fixes retain useful errors
when a runtime or dependency is unavailable.
## Browse general instance settings in CONFIG
CONFIG organizes instance, workspace/context, display/timezone, waiting,
snapshot and recovery settings into readable categories with source and
override provenance. Advanced settings and full values remain discoverable.
Slack and human connections are one child of CONFIG. Opening or refreshing
the browser is passive and secret-safe; configured values remain distinct
from observed application, connectivity and delivery. Users can return to
ordinary scope and work views without opening configuration files.
## Operator notes
Root, CLI, daemon and web-UI package versions are 0.5.12. The terminal TUI
retains its independent 0.1.0 version. Node requirements and the ordered
migration head `079_workflow_lifecycle_parallel` are unchanged from 0.5.11;
this release adds no database migration.
Use the supported upgrade and continuity procedure when adoption is authorized.
Preparing this candidate does not restart a daemon or renew existing seats.
Configuration inspection is not an editor, credential migration, automatic
repair or a promise that every setting hot-reloads. Editing an authored graph
alone does not adopt it into a progressed workflow. The exception repair does
not retroactively create tasks for failures already committed by older code;
inspect retained occurrences and use the supported recovery route deliberately.
## Validation scope and known limitations
The accepted development outcome includes one less-primed, returning/assisted
agent journey through two dependent slices: recording and correcting proof,
revising the graph, waiting and returning, recovering a real failed handout
check, and navigating CONFIG back to work. Focused mechanism reviews and
installed controls support that journey. Native startup, resume and wake
observations retain their original source cuts and times; they are reused
with attribution, not presented as new runs on the version-only candidate.
Candidate package and generated-version checks are recorded separately from
those development judgments.
- This was not an unfamiliar-human study, an unassisted greenfield usability
trial or all-platform verification. Setup and one-time native approvals
remain part of the recorded journey.
- The final exception task was consumed through public read/claim/action
after its placeholder destination could not receive a nudge. It does not
establish native exception delivery or external human receipt.
- Quiet-wait measurements establish the tested workload's attention and
recovery behavior, not a global latency guarantee or provider billing-token
savings. A retained startup catalog-unavailable control also failed on its
unchanged parent; the evidence is not an all-green test-suite claim.
- Whole-package substance review, exact-release verification and publication
must still bind this prepared candidate. These notes do not announce an
available npm release or authorize runtime adoption.
+131
View File
@@ -0,0 +1,131 @@
# OpenRig v0.5.13 — read, decide, and return to work
**Status:** release candidate, unpublished. Exact-cut substance review,
release verification and publication remain pending. This candidate has not
been adopted into a live installation.
OpenRig 0.5.13 connects the human TUI journey: enter the instance, choose a
project, read its current instructions, understand what needs your decision,
and deliberately open the terminals you need. Scoped oversight and explicit
Git context updates help agents work from the right instructions while keeping
authority, evidence and local authorship visible.
## Start with something useful
Bare interactive `rig` exposes Help, Skip and local reading while startup
checks are running, when the daemon is down, or when its state is unverified.
Selected project, mission and slice intent can be read from disk without
pretending it is live queue, execution or topology data. Read failures and
source provenance stay visible. Starting a daemon or returning seats to work
remains a deliberate action; reading and skipping do not imply consent.
## Read the current source and find your way back
PROJECTS keeps a selected project's identity and root through missions,
slices and source views, including projects with the same display name.
An unavailable source is reported for that project instead of substituting
another project's work. Refresh reads the current file.
The shared reader shows source paths, modification/hash information and
bounded content, with scrolling, local links, heading links and Back.
HTTP(S) links show a selectable destination for copying; no browser is opened.
Missing, unreadable, binary or out-of-root files remain named errors. Large files show the
1 MiB returned-content limit; a heading absent from a truncated prefix is
not claimed absent from the whole file. Back restores the prior reading
and navigation position.
## Know what needs your decision
ATTENTION separates **Action required** from **Updates**. Human requests
and explicit blockers show the request, urgency, scope and work it would
unblock. Outcome and health updates retain their own source and history.
Open the detail or linked evidence to understand it; viewing a request is
not approval or delivery, and closing a queue item is not proof of a
completed product outcome. Missing or incomplete reads stay visible.
## Preview terminals before opening them
TERMINALS lists saved and derived views with their members and resolved
layout. A preview is passive. Deliberate Open uses the inspected plan and
reports opened, absent or degraded members, including partial failures.
Help and side trips return to the same preview and page. The observed native
provider journey uses Herdr; broader provider limits are listed below.
CONFIG keeps full values, defaults and source/override provenance readable.
Help supplies command descriptions, grammar and examples, including during
startup failures. Narrow views retain scrolling and return controls. Health
keeps INFO severity separate from Unknown assessment, exposes stale and
unavailable reads, and explains that an empty findings list is **not a healthy
verdict**. The complete empty-health explanation is readable at 80 columns.
## Choose oversight for the work in front of you
Human-led work is quiet by default for process-only diagnosis. Explicit
delegation can enable that oversight during planning as well as execution,
subject to the existing diagnosis policy and delivery prerequisites. Ordinary
work reminders and operational health retain their behavior. Scope, phase
and posture come from explicit sources; unresolved identity stays unknown.
Use `rig mode effective` with an explicit scope to inspect the result.
The [scoped operating posture reference](../reference/scoped-operating-posture.md)
describes selection and precedence. A posture preference grants no execution
authority. Correction records can retain selected current guidance and
distinguish an assessment, an action actually taken and a later observed
effect. These are supported mechanics, not evidence of improved judgment:
natural behavioral effect remains **UNOBSERVED**, and human time cost remains
**UNMEASURED**. No cure or reduced recurrence is claimed.
## Update context without discarding local authorship
`rig context add <repository-path-or-URL> --git` retains the relationship
between a Git source, its checkout and a selected context pack. Use `--pack`
when selecting a pack within a repository. Existing Git credentials apply.
Use `rig context source inspect <ref>` to inspect local changes, conflicts,
revision and selection without fetching. `rig context source update <ref>`
explicitly fetches and merges upstream before selecting clean content.
Committed local work survives an ordinary merge. Dirty work, conflicting
changes, unavailable upstreams or edits to the served selection cause a
named refusal rather than a reset or overwrite. Resolve and commit, or abort
the merge, in the retained checkout before retrying; the prior served
selection remains available during a conflict. Updates do not push.
Checkout history, selected served bytes and actual agent consumption remain
different facts. Read the selected bytes with `rig context get <ref>` and
check the intended consumer. This does not automatically synchronize every
context library or adopt guidance into running agents.
## Operator notes and validation limits
Root, CLI, daemon and web-UI package versions are 0.5.13. The terminal TUI
retains its independent 0.1.0 version. Node requirements are unchanged.
The ordered migration head advances from 079 to
`080_scoped_operating_posture`, extending the existing mode-binding store
with scoped human-led/delegated choices while preserving legacy rows.
Use the supported upgrade and continuity procedure when adoption is
authorized; preparing this candidate performs no live migration or restart.
Accepted development evidence connects the journey at 140×42 and 80×24,
including unavailable/Unknown controls, two-project isolation, current-source
reading, deliberate native terminal opening and Git conflict preservation.
Those observations retain their original development cuts and attribution.
Version, release-note and package checks are separate from those journeys;
they are not a new whole-release usability study.
- Simultaneous default TUI clients can replace one another's programmatic
control-socket address. This remains a known, unfixed limitation, judged
nonblocking for the demonstrated single-client human journey. Unique test
sockets do not prove product-level multi-client isolation; historical lost
requests remain unverified.
- Native terminal proof covers Herdr. SSH login, cmux end-to-end behavior
and effects on a parent host remain unverified. Existing native continuity
evidence is reused only for unchanged paths, not new provider coverage.
- A private shutdown timed out while closing connections; clean completion
and pending effects remain unverified. Earlier sandbox public-start failures
remain recorded failures despite a later successful managed private start.
- Synthetic findings and requests exercise mechanics, not natural behavioral
improvement or real human approval/delivery. No unfamiliar-human study,
real-host reboot, all-platform run or live 0.5.13 adoption is established.
- Final exact-cut verification and publication still require their separate
evidence and authorization. These notes do not announce an available npm
release or authorize adoption.
+147
View File
@@ -0,0 +1,147 @@
# OpenRig v0.5.14 — clearer navigation, mission progress and human notifications
OpenRig 0.5.14 makes reading more predictable when opened or refreshed, keeps
human requests distinct from informational updates, and groups instance settings
and health where they are easier to find.
## Enter and keep reading
Bare interactive `rig` enters the ordinary TUI after confirming a running rig.
An instance without running rigs keeps the startup chooser. Help, Skip and Local
remain responsive during slow or unverified detection; a late observation does
not take navigation away after the user has chosen another route.
Pages distinguish initial loading, successful empty results, refresh and failure.
The navigator remains usable through page and rig changes. A previously read page
can stay visible with its original scope label while the selected page loads;
that prior content has no action targets. Same-scope refresh retains useful content,
with the previous read time and Retry. A failed Feed source retains its own data
while healthy sibling sources update. Confirmed denial, empty results and removals
clear affected content, and late replies cannot replace the current selection.
First visits retain navigation and local feedback. Topology first offers rig
selection, then reads that rig before unrelated fleet details. Terminal names are
available before detailed readiness and preview.
Projects keeps malformed mission and slice sources visible as local errors while
healthy neighbors remain readable. Correcting the affected source and refreshing
restores its content without showing its parser fragment above unrelated work.
Optional blocker descriptions load independently, so an unavailable description
does not hide primary queue facts or change the recorded human recipient.
Fleet reads share one elapsed request budget across stages and retain local
results when a remote source fails. Repeated proof-policy and delivery-receipt
reads avoid duplicate work while later reads still observe current inputs.
These changes reduce measured work; they do not establish a latency guarantee.
Selective restore and startup event indexes reduce inventory read work while
preserving event history and projection results. Private query replay does not
establish a general live response-time guarantee.
Terminal catalogs reuse one inventory read instead of repeating the same global
scan. The measured result was equivalent with less query work; this is not a
general latency guarantee. Existing saved-view discovery, passive preview and
deliberate Herdr Open remain available.
## Complete requests and quiet updates
Human notifications distinguish explicit decision requests from informational
updates. A request carries a complete brief and requested action; related
supplemental detail can continue in its thread. Content that exceeds supported
limits is refused with an author correction instead of silently losing essential
text. The shipped authoring guidance calls for concise, complete human briefs.
Informational FYIs require no action or reply and do not create approval
obligations. Delivery completion is separate from a human decision. Partial or
ambiguous delivery remains distinguishable, and retry/reply tracking keeps each
request and its thread separate. Existing callers without an explicit intent
retain decision behavior.
## Find requests and instance health
The six top-level entries are **Topology, Specs, Projects, Terminals, Feed and
System**. Feed explicitly covers the instance and all humans. **Human requests**
shows recipients and relevant scope; **Updates** includes confirmed delivered
FYIs alongside existing outcome and health updates. Delivered FYIs remain in a
bounded history after their delivery obligation closes. Reading is not approval.
Feed's Explorer contains these categories, with entries in the content pane.
Specs kind groups and Derived terminal views start collapsed and expand explicitly;
Saved views stay prominent. Saved membership still requires deliberate setup, and
opening a terminal view remains an explicit action.
System opens to instance **Health**, followed by **Configuration** and
**Connections**. Contextual Health, global Help and compatible direct commands
remain available, including `attention`, `needs`, `feed`, `config` and
`connections`.
## Understand mission progress
Projects puts completed outcomes, current work and owner, blockers, next dependency
and slice boxes before process prose at both terminal widths. Accepted required
proof judgments determine slice outcomes; all slice outcomes must be complete
before the mission reports outcomes complete. Evidence attachments, quiet activity
and handoff alone do not accept proof. Reopened or assigned unfinished work stays
open, including when there is no unassigned next slice.
Actual custody and planned ownership remain separate. Authored lifecycle and
publication are separate from outcome completion; unknown ownership or eligibility
stays unknown. Narrow screens retain a full first box and scroll for the remainder.
## Operator notes and verification limits
Root, CLI, daemon and web UI versions are 0.5.14; the terminal TUI remains 0.1.0.
Node requirements are unchanged. Migration 081 adds explicit intent and
supplemental detail to queue records. The migration head is now
`084_inventory_event_indexes`, adding three selective inventory read indexes;
083 adds the two review read indexes. Migration 082 adds nullable identity
provenance to archived queue transitions. Archival preserves recorded values and
refuses an archive schema that cannot carry them; legacy unknown values remain null.
Shared history, RECENT and OWNER/receipt readers now include archived records,
so their results can broaden even where the command or TUI code is unchanged.
Migration alone does not reconstruct previously lost metadata. Use the supported
upgrade and continuity process when adoption is authorized.
The private combined review exercised installed journeys at 140×42 and 80×24,
including entry, refresh, source failure, scope identity, detail/Back, saved
terminal Open and notification correlation. An initial Feed source-failure defect
was retained in the evidence and repaired, then independently rechecked. Owner
candidate-native evidence and the reviewer's child TUI against an unchanged
parent daemon remain separate observations.
Later operator inspection exposed disruptive navigation and a misleading mission
overview. The corrected private combined review covers the new navigation,
organizers and native mission states. Its false NEXT-completion finding was
preserved, repaired and independently rechecked at both widths. These private
results remain limited to the journeys and environments exercised.
VM inspection was accepted on September 14: generally zero-to-two-second
loading with occasional timeouts, along with formatting, icon colors and default
views. This observed result is not a general latency or availability guarantee.
Later recovery of the same candidate preserved sessions but did not establish a
durable responsiveness fix.
- Intermittent read timeouts remain. An inherited CLI health-check deadline can
report the daemon unresponsive before sending a queue request even when the
direct request succeeds. That defect is not fixed here; occasional server
delay also remains unattributed.
- Earlier notification checks used controlled HTTP responses and synthetic replies.
A later real single-consumer Slack exercise confirmed complete decision/detail/FYI
posting and reply correlation; the operator confirmed phone readership and
readability. That observation does not establish OS push behavior or all clients.
- Terminal provider evidence covers disposable Herdr members. Other provider,
remote and parent-host behavior is not established by this review.
- Saved Build watch discovery was exercised in the VM and its native judgment
recorded. That observation does not establish general navigation responsiveness.
- Simultaneous default TUI clients retain the existing programmatic control-socket
limitation. Unique test sockets do not prove general multi-client isolation.
- Version and package checks do not repeat the development journeys or establish
a new whole-source, all-platform or unfamiliar-human usability result.
- Observed shutdown-drain timeouts, recorded config-preservation uncertainties
and a prior `recorder_write_failed` observation are not claimed resolved.
Recovery evidence remains distinct from ordinary-entry verification and does
not establish whole-system health.
- Historical source-scan findings retain their recorded dispositions; a clean
changed-path scan is not a clean whole-source scan.
- Bundled Workflow Specs, including `basic-loop`, can disappear from discovery
after an upgrade changes the install directory despite their YAML remaining
intact. The discovery fix is explicitly deferred beyond 0.5.14; this release
contains no repair for it.
+117
View File
@@ -0,0 +1,117 @@
# OpenRig v0.5.15 — first-use recovery, software factory guidance and Pi fixes
Changes for version **0.5.15**. Publication status and date are recorded on the
[GitHub release](https://github.com/mvschwarz/openrig/releases/tag/v0.5.15) and npm.
OpenRig 0.5.15 improves startup and recovery, adds public guidance for a continuing
software team and user-chosen command permissions, and fixes Pi reply visibility,
shell context and terminal input handling.
## Start and return with clearer guidance
Before launching, read
[what OpenRig changes on your machine](../../README.md#what-openrig-changes-on-your-machine).
Provider integration can configure executable hooks and workspace trust; changing
only `OPENRIG_HOME` does not isolate provider configuration. The
[getting-started guide](../reference/getting-started.md) walks through your first
task and explains these setup effects.
Check the commands available in your installation:
```sh
rig --version
rig --help
rig up --help
```
Startup and recovery recognize native Codex through shell and Node launchers,
and report uncertainty when process identity cannot be established. Recognized
update notices are skipped without installing provider updates. Authentication
and trust choices remain yours.
Startup errors now describe the problem and a recovery action. If a name matches
both a starter and a stopped rig, `rig up <name> --existing` selects the existing
rig. Inspect its reported state before assigning more work.
## Continue useful reviewed work
Give your agent a real repository, an outcome and the decisions you want to keep.
The bundled OpenRig Software Factory recipe supports direct team work,
queue-supported orchestration or an optional explicit Workflow.
```sh
rig context show skills/core/openrig-software-factory --json
rig context get skills/core/openrig-software-factory/SKILL.md
rig context get skills/core/openrig-software-factory/references/worked-example.md
```
Start with the two-seat team. Add one or two seats with `rig grow` when the work
benefits from separate responsibilities. The example covers roles, context,
work ownership, independent review, concurrency and cost, and distinguishes
changes to the running team from saving an updated RigSpec. Use guidance matching
your installed version.
## Choose command permissions
Ask your agent to inspect the target harness, explain the available scopes and
apply your choice while preserving existing rules:
```sh
rig context get skills/applying-a-permission-policy/SKILL.md
```
Allowing the whole `rig` command family includes lifecycle and configuration
changes as well as reads. Choose narrower commands or retain approval prompts
when appropriate. Project-only setup requires supported, active and trusted
project configuration; if it is unavailable, the agent should explain the limit
instead of substituting user-wide permission. Back up touched files and verify
the selected rules in the actual conversation. Shipped sandbox, approval and
permission defaults are unchanged.
## Known compatibility limitation
On macOS arm64 with Node 24, installing the SQLite dependency can fail when a
suitable prebuilt binary is unavailable and compilation is not available.
Compiler-built SQLite has also failed during database cleanup at runtime.
**Use Node 22 on macOS arm64 for now.** This recommendation does not change the
declared Node support range, and this version does not update SQLite.
## Pi replies, input and recovery
Pi replies and tool progress are visible for the supported RPC event shapes.
Shell tools retain the managed OpenRig context needed for identity and routing.
Provider errors are shown with bounded text, and exhausted retries do not print
the same error repeatedly.
Pasted messages wait for explicit submission. The terminal runner accepts input
beyond the terminal's canonical buffer limit, preserves editing and cancellation,
and recovers after rejecting an oversized framed message. This is not a general
latency guarantee or an unrestricted input-size promise.
An added seat whose first startup failed during resource projection, before its
startup context was saved, can use the guarded retry after the setup problem is corrected.
Retain the complete original member fragment and its source root; a snapshot alone
does not reconstruct missing startup configuration. Use one retry at a time.
Ordinary conversation resume remains separate.
**Pi support is qualified and supervised.** Controlled coding, follow-up,
reporting and conversation continuity were verified with Pi 0.87.1 and OpenRouter
`z-ai/glm-5.3-flash`; ordinary-seat identity, tools and a reply also worked.
Completion of an ordinary useful task and unattended team operation remain
unverified. Context-profile selection and hook-generation coverage are limited;
do not assume parity with every Codex or Claude integration. The Software Factory
guide likewise does not establish fully unattended operation.
## Contributors and included pull requests
Thanks to [@danielkuykendall23-boop](https://github.com/danielkuykendall23-boop)
for the Pi reply fix in [#37](https://github.com/mvschwarz/openrig/pull/37), and to
[@mvschwarz](https://github.com/mvschwarz) for Codex startup and restore guidance
([#38](https://github.com/mvschwarz/openrig/pull/38)), Software Factory and permission
guidance ([#39](https://github.com/mvschwarz/openrig/pull/39)), contribution and
support guidance ([#45](https://github.com/mvschwarz/openrig/pull/45)), and the
first-use README ([#46](https://github.com/mvschwarz/openrig/pull/46)).
This release also includes the startup-identity refinements, guarded first-start
retry, Pi shell/error handling and terminal-input fixes described above. Project
work-policy features and the Node 22/SQLite 13 upgrade are outside this release.
+62
View File
@@ -0,0 +1,62 @@
# OpenRig v0.5.16 — choose the Claude instruction file
Changes for version **0.5.16**. Publication status and date are recorded on the
[GitHub release](https://github.com/mvschwarz/openrig/releases/tag/v0.5.16) and npm.
OpenRig 0.5.16 lets a rig keep its managed Claude Code instructions out of a
tracked `CLAUDE.md`, and adds an advisory portability report for pull requests.
## Keep a tracked CLAUDE.md unchanged
OpenRig writes its instructions for Claude Code members into managed blocks in
each member's working directory. By default that file is `CLAUDE.md`. If your
repository tracks `CLAUDE.md`, select `CLAUDE.local.md` in the rig spec:
```yaml
managed_blocks:
claude-code: CLAUDE.local.md
```
Claude Code loads `CLAUDE.local.md` from the working directory as well; keep it
out of git, for example with a `.gitignore` entry. The selection applies when
members launch, restore, relaunch or are added, and it is kept by `rig export`
and rig bundles. `rig down` removes OpenRig's blocks only from the selected file.
OpenRig never edits, moves or deletes blocks in the other file.
Only the `claude-code` key is accepted, with `CLAUDE.md` or `CLAUDE.local.md`;
any other key or value is rejected before a member launches. Without the field,
behavior is unchanged. Codex members always use `AGENTS.md`.
**Switching an existing rig:** blocks OpenRig already wrote into `CLAUDE.md`
stay there. Until you remove them, the file 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. See
[the RigSpec reference](../reference/rig-spec.md#choosing-the-claude-instruction-file).
## Portability report for pull requests
A new pull-request workflow lists added lines that contain values tied to one
machine, network or account: credentials, home and temporary paths, network
addresses and email addresses. Suspected credentials are withheld from the
report. Findings never fail the check; the author decides what to change. It is
pattern-based and does not guarantee that nothing private remains. Run it
locally with:
```sh
node scripts/portability-report.mjs
```
## Contributors and included pull requests
Thanks to [@hvpaiva](https://github.com/hvpaiva) for reporting and proposing the
Claude instruction-file option in [#25](https://github.com/mvschwarz/openrig/issues/25),
and to [@mvschwarz](https://github.com/mvschwarz) for the README badges and demo
([#54](https://github.com/mvschwarz/openrig/pull/54)) and the portability report
([#56](https://github.com/mvschwarz/openrig/pull/56)).
The Node support range and SQLite version are unchanged. The macOS arm64 with
Node 24 SQLite installation limitation noted for 0.5.15 still applies; use
Node 22 on macOS arm64.
+35
View File
@@ -0,0 +1,35 @@
# OpenRig v0.5.17 — install with Bun
Changes for version **0.5.17**. Publication status and date are recorded on the
[GitHub release](https://github.com/mvschwarz/openrig/releases/tag/v0.5.17) and npm.
OpenRig 0.5.17 can be installed with Bun as well as npm.
## Install with Bun
```sh
bun add -g @openrig/cli
```
Earlier versions listed the unpublished `@openrig/daemon` package as a dependency.
npm used the copy bundled in the package, but Bun looked it up in the registry
and failed with a 404. The package now imports the daemon copy it already ships
and no longer depends on `@openrig/daemon`, which also makes it smaller. npm
installation is unchanged.
OpenRig still runs on Node.js, so install Node.js 22 as well. Bun may block this
package's postinstall script. In that case the Node.js version and SQLite check
does not run at install time. See [what OpenRig changes on your machine](../../README.md#what-openrig-changes-on-your-machine).
The installation fix was verified with Bun 1.4.0 and npm on macOS arm64 with
Node.js 22. Other combinations are not verified.
## Contributors and included pull requests
Thanks to [@drewpayment](https://github.com/drewpayment) for reporting the Bun
installation failure in [#66](https://github.com/mvschwarz/openrig/issues/66),
fixed in [#68](https://github.com/mvschwarz/openrig/pull/68).
The Node support range and SQLite version are unchanged. The macOS arm64 with
Node 24 SQLite installation limitation noted for 0.5.15 still applies; use
Node 22 on macOS arm64.
+74
View File
@@ -0,0 +1,74 @@
# OpenRig v0.5.2
## 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.
### `rig policy` handles adversarial input honestly
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).
- GitHub Release: https://github.com/mvschwarz/openrig/releases/tag/v0.5.2
+70
View File
@@ -0,0 +1,70 @@
# OpenRig v0.5.3
## Summary — "The context release"
**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.
### Durable seat recap — `rig context recap-write`
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).
- GitHub Release: https://github.com/mvschwarz/openrig/releases/tag/v0.5.3
+62
View File
@@ -0,0 +1,62 @@
# OpenRig v0.5.4
## Summary - "The honesty release"
**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).
+97
View File
@@ -0,0 +1,97 @@
# OpenRig v0.5.5
## Summary — "The ambient-attention release"
**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`:
- `072_thread_seat_map.ts`
- `073_queue_transition_wakes.ts`
**New verbs**: `rig parked [seat]`, `rig seat handover --source fork:|rebuild` (execute path), `queue block --on <blocker>` (park-with-wake), `rig gateway human` fragment-lifecycle verbs, `rig view show escalations`. `rig ps` gains a typed `ACTIVITY` column.
## What Shipped
### `rig parked [seat]` — derived silent-stuck diagnosis
Activity × open obligations, with per-input confidence and remedy taught inline. Reach for it when a seat looks idle and you suspect dropped work — before any `claimedAt` arithmetic or pane capture. 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.
### `rig seat handover --source fork:|rebuild` — EXECUTE
`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.
- **`@external` direct-send routing** — 0.5.6 wave-2 routing fix.
- **Delivery preferences → rules engine** — 0.5.6 slice 01.
## See Also
- `CHANGELOG.md` — the `[0.5.5]` entry.
- `docs/releases/v0.5.4.md` — the prior honesty release.
- GitHub Release: https://github.com/mvschwarz/openrig/releases/tag/v0.5.5
+128
View File
@@ -0,0 +1,128 @@
# v0.5.6 — world-building as context engineering
OpenRig 0.5.6 ships the context system's next layer, makes long-running
agents durable, and makes messages to humans trustworthy. Three themes: the
context a seat runs on is structured, tagged, and verifiable; an agent seat
can now outlive its own context window without a human managing the
transition; and anything routed to a human carries proof of delivery.
## The context system: structured, tagged, verifiable
- **The mission install.** A documented five-layer convention
(`docs/reference/mission-install.md`) for installing project context into a
seat: the pieces, their arrangement, the seat's own position, the current
contract, and a derive-your-own-delta step that proves the install was
consumed rather than skimmed.
- **Lore routing.** What a seat learns locally routes into addressable,
rig-local lore packs (`docs/reference/lore-routing.md`) — carried with a
maturity stage from birth, and structurally excluded from anything that
ships.
- **Taxonomy everywhere.** Every pack entering `rig context` carries its
class — WORLD / LORE / SKILLS / MISSION — surfaced in listings, with a loud
refusal for untagged packs.
- **A public world pack with checkable claims.** The public onboarding pack
is authored as atoms whose claims each carry a check that can fail, with
the verifier shipped inside the pack — public content that cannot rot
silently.
- **A complete, self-retrievable skill index.** The skill router's index now
covers the full shipped set, is retrievable through the routes it teaches,
and carries a recurrence test so coverage gaps stay found.
- **A substance gate for what ships.** A whole-surface review pass over every
packaged file (bundle and npm surfaces included) runs as a named release
step; this release shipped with all 5,316 packaged files passing.
## Continuity: seats that outlive their context windows
- **Continuity policies in the agent spec.** `compaction_strategy` is now live
configuration with four modes: `default-compaction`, `managed-compaction`,
`handover`, and `apprentice-handover`. Modes apply to Claude seats
(positive runtime matching — Codex seats keep their native behavior) and
resolve through the same spec-default < profile < member precedence as the
rest of the agent spec.
- **The apprentice handover, automated.** A seat declaring
`apprentice-handover` gets two armed triggers: near its context threshold,
the system creates a fresh successor seat and runs its onboarding install;
nearer the wall, it mints an owned handover work item for the seat that
executes the swap. The executing seat is declared configuration (`mechanic:` on the
policy) — never inferred from topology, and arming refuses loudly if it is
missing.
- **Managed compaction with honest restores.** Seats in `managed-compaction`
mode get a pre-compaction prep nudge (deposit your state before the window
closes) and, after a configured restore, a durable receipt recording how
much usable context the restore actually recovered — so a restore that
silently refills the window instead of recovering width is visible.
- **Context-usage triggers as a primitive.** `rig watchdog` gains a
usage-threshold condition: fire any command when a seat's context usage
crosses a calibrated per-seat threshold. The continuity modes are built on
it; your own automation can be too.
## The human layer: delivery you can prove
- **No more silent fall-through.** One destination resolver classifies every
dispatch: terminal-bound seats use the terminal; registered humans and
virtual seats route through the gateway; nothing falls through to a
terminal lookup that cannot succeed. Unregistered external addresses get a
structured, teaching refusal.
- **Delivery receipts on the work item.** Every gateway post writes its
receipt atomically on the queue row: `posted` (with the message timestamp
and channel), `transport-failed` (with the error), or — derivably —
`never-posted`. `rig queue undelivered` now consults that ledger first and
returns only genuine failures. "Did this reach them?" is one read.
- **Notification levels, one vocabulary.** Queue transitions classify as
`RECORD` < `NOTICE` < `ALERT` at write time — a human-required decision is
`ALERT`; system ceremony events are `NOTICE`; the rest is durable record.
Two independent dials configure the minimum level that posts and the
minimum level that interrupts.
- **A delivery-rules engine.** Stored per-human preferences and availability
decide how each message class is delivered. An `off` preference suppresses
interruption — never the durable record, the post, or the receipt. Away
windows defer routine traffic into digests (4-hour and daily), with
exactly-once delivery by construction: durable episode identities, retry
until a real receipt, and structurally non-overlapping digests.
- **Files dropped in Slack become work.** A file or image upload lands as a
work item with the file transferred to local storage and referenced by
path — authenticated download, size-bounded, per-file failures named, and
no URL or token ever written to a row. A park blocked on a registered human
automatically enters the alert path and posts exactly once per episode.
## Operational honesty
- A rate-limited seat gets exactly one timed wake at its stated reset.
- A seat holding claimed live work while sitting idle gets woken by the
system, consuming the arbitrated activity oracle — with exactly one wake
per park episode.
- Refocus state keys to the current occupant of a seat; a fresh occupant
never inherits a predecessor's baselines.
- `rig ps` counts claimed in-progress work as assigned work.
- Sends refuse empty bodies; unknown daemon API routes return JSON 404s.
- Stuck-sweep findings stop re-minting after closure; cross-host custody
verification completes durably instead of refreshing forever.
- Blocker actuation is unified: one helper, cross-host closure honest,
delivery after the transaction commits.
- Rig expansion and serialization preserve what they were given:
`session_source` versions, `services`, and member overrides survive
round-trips; fork forwards model, role, restore policy, and label.
- `rig view show execution` derives who is building what, what is next, and
each item's completion rung (locked / built / reviewed / folded / adopted)
at read time — unknowns render as INDETERMINATE, never as idle or done.
## Process and reference
- The SDLC conventions document is now a component menu: choose per mission
from the simple flow, the wave model, or the rigorous overlay, with
planning rigor as a dial. Four full references ship at `docs/reference/`:
`planning-dial.md`, `wave-sdlc.md`, `release-boundary.md`, and
`product-management-pass.md`, joined by `mission-install.md` and
`lore-routing.md`.
## Upgrade notes
- Three database migrations apply automatically on daemon start:
`074_context_usage_watchdog`, `075_context_usage_watchdog_generation`, and
`076_owner_notification_levels`. No manual steps.
- Context packs: 41 ship (up from 39).
- Delivery defaults are deliberately wide open (post at `NOTICE` and above,
interrupt at `ALERT` and above). Tune down from measured volume with the
two dials rather than pre-filtering.
- Continuity modes are opt-in per seat; unconfigured seats keep today's
behavior exactly.
+74
View File
@@ -0,0 +1,74 @@
# 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.
+80
View File
@@ -0,0 +1,80 @@
# OpenRig v0.5.8 — recoverable operations and visible work
OpenRig 0.5.8 makes long-running rigs easier to recover, understand, and
operate. It adds exact selected-rig recovery after an abrupt outage, makes the
current mission and its execution story visible to agents and operators, and
tightens the diagnostics and guardrails that keep live state honest.
## Recovery and topology lifecycle
- **Recover the selected rig after an outage.** Restore now rejects stale
native identity, uses real provider-session evidence, and avoids substituting
a fresh or wrong occupant or starting unrelated sibling rigs.
- **Use one topology-creation path.** Public creation and growth converge on the
same daemon effect while preserving member working-directory overrides.
- **Keep topology declarations useful.** Rigs expose honest self-description,
workspace declarations survive export, and a validated workspace declaration
can be applied to an existing rig without changing its topology.
## Mission and context visibility
- **Open mission execution from SCOPES.** A selected mission now shows a
readable `DONE` / `NOW` / `NEXT` / `ATTENTION` story, truthful working-lane
and evidence-gap counts, bounded overflow, and stable mission switching.
- **Install the work that is actually current.** Work-install resolves complete
files and dot-form slice IDs, and refocus derives the seat's typed current
mission and slice instead of asking the agent to reconstruct them from
memory.
- **Discover shipped capability.** The public capability world and its
addressable context are available through the installed context routes.
## Operational honesty and safer setup
- **Wake once for the state that needs attention.** Park timers retire on every
exit path, new park episodes supersede old timers, and idle-gate watchdogs use
the same arbitrated activity oracle as the rest of the system.
- **Diagnose the mechanism that is actually live.** Producer-link health derives
from effective endpoints, topology freshness is bounded, and synthetic model
records no longer create false model-divergence findings.
- **Preserve user-owned Codex configuration.** Managed TOML projection refuses
malformed or root-scoped fragments before write and preserves user tables
across valid collisions.
- **Keep upgrades agent-led.** The shipped upgrade skill supplies bounded
inspection, verified database backup, and contained managed-plugin refresh
helpers while leaving cutover judgment with the operating agent.
- **Trust clean-checkout results.** The documented daemon test gate and the
same-SHA long-lived gate now agree on deterministic source truth.
## Operator / setup notes
- Root, CLI, daemon, and web UI package versions advance to `0.5.8`. The
terminal TUI keeps its independent `0.1.0` version.
- Database migration head advances from `077_node_session_source` to
`078_idle_gate_fired_condition`; normal daemon startup applies it
automatically. No manual migration step is required.
- Existing rigs do not need a topology rewrite. The new workspace-only apply
path deliberately leaves members, pods, edges, and running sessions alone.
## Known limitations
- Selected-rig recovery does not cover total database plus filesystem loss or
fleet-wide automatic restart.
- The existing-rig workspace story does not validate `target_repo`; that
separate concern remains outside this release contract.
- A pre-existing backend `attention_required` state is not part of the bounded
TUI topology-freshness work.
## Release verification
- Independent QA and review exercised the final SCOPES execution journey at
110 and 160 columns, including mission selection, readable evidence gaps,
bounded overflow, slice drill-in, and Escape navigation.
- Recovery testing used real Claude and Codex occupants across current, stale,
absent, and insufficient-evidence states. It also covers native-session
identity changing within the same OpenRig occupant record.
- The accepted product tree was replayed as a linear public lineage rooted on
0.5.7. Every author and committer uses the project noreply identity, and the
replayed product tree is byte-identical to the accepted local-main tree.
- Release acceptance includes package assembly, a hash-bound per-file
substance review, and installed-runtime QA and review against the exact
private cut.
+105
View File
@@ -0,0 +1,105 @@
# OpenRig v0.5.9 — developer-beautiful mission control
OpenRig 0.5.9 turns the terminal UI into a real mission-control surface and
gives the instance beneath it a cleaner, more durable operating model. The TUI
is easier to scan, navigate, and drive at ordinary terminal sizes; System
World, project context, and managed skills now compose explicitly; and restore,
fresh launch, scheduled wakes, and project workflows report the state they
actually acted on.
## Developer-beautiful terminal UI
- **Read the whole instance at a glance.** The topology view now presents one
continuous agent table across rigs, keeps pod separators visible, and shows
bounded `RECENT` work from material queue transitions without duplicating
archived rigs or inventing missing summaries.
- **Move from overview to action without losing context.** Instance, rig,
mission, slice, agent, and Explorer pages share one responsive visual grammar,
preserve rig identity while scrolling, and keep lifecycle evidence and
actions reachable at 160, 120, and 84 columns.
- **Use the terminal as a first-class interface.** Semantic state colors,
compact glyphs, restrained motion, resize redraw, filtering, pointer-local
scrolling, terminal-native selection/copy, attach actions, and socket-driven
commands work through the same production surface.
## A coherent instance, context, and skill model
- **Start from one additive instance layout.** CLI and daemon startup reconcile
the same canonical `$OPENRIG_HOME` structure, preserve existing content, and
refuse wrong-type managed paths before writing.
- **Compose a System World with the current project.** `context.root` is the
addressable context library, `context.system_world` selects the instance-wide
baseline, and `rig context work-install` reports System World, topology, and
project inputs together.
- **Manage skills as their own resource.** `skills.root` names one versionable
catalog. `rig skill loadout` composes system, topology, and project selectors;
its apply mode owns only its manifest-listed projection and refuses to erase
local or modified content.
- **Prime each runtime at the right width.** Provider-shaped onboarding gives
Codex a compact bootstrap plus addressable coverage map while retaining the
fuller guided route for runtimes that benefit from it.
## Lifecycle and recovery honesty
- **Create a deliberately blank occupant for one seat.** `rig seat launch
<seat> --fresh --reason <why>` is the explicit no-continuity path; it preserves
sibling seats and durable work and refuses unmanaged ambiguity.
- **Compile authored work into an executable lifecycle.** `rig workflow
compile` reads `project.yaml`, `mission.yaml`, and `slice.yaml` as one graph;
`instantiate-lifecycle` starts an eligible graph with an opaque replay key,
and typed acceptance evidence prevents a workflow advance from standing in
for release acceptance.
- **Know what a restore attempted.** Snapshot selection can be explicit,
snapshots can retain the intended seat roster, and `rig restore status`
derives the current intended-set result while keeping historical nodes and
unverified resume lineage out of a false success.
- **Keep automation attributable.** Watchdog reminders deliver the authored
message under an explicit scheduler identity, and effective-model detection
follows the identity-verified current occupant rather than a retained
predecessor session.
- **Choose judgment only where it earns the loop.** The shipped
`agent-operated-workflows` skill distinguishes AI-enabled software,
agent-operated workflows, and agent-operated software. The 0.5.9 instance
migration is its first reference implementation.
## Upgrade and setup notes
- Root, CLI, daemon, and web UI package versions advance to `0.5.9`. The
terminal TUI keeps its independent `0.1.0` version.
- Database migration head advances from `078_idle_gate_fired_condition` to
`079_workflow_lifecycle_parallel`; normal daemon startup applies it.
- Existing instances have a deliberate layout boundary: context telemetry moves
to `state/context-usage`, provider telemetry lives at
`state/provider-usage`, the addressable library moves to `context/`, and the
default System World lives at `context/system/system-world.yaml`.
- Do not perform that change as a directory rename while an old collector is
writing. Load the shipped `openrig-upgrade` skill and follow its 0.5.9
Agent-Operated Migration: inspect, prepare with a retained preimage, activate
the exact target runtime separately, verify paired canonical samples, then
invoke the non-destructive library finalizer with that exact receipt. The
complete command sequence and rollback contract are in the top-level README.
## Known limitations
- The migration helper deliberately does not stop or start the daemon, mutate
the database, relaunch seats, or delete legacy telemetry/library state. Those
lifecycle and eventual-retirement decisions remain explicit operator work.
- A restore attempt receipt derives what the durable event and current process
evidence can prove; missing or ambiguous native identity remains unresolved
instead of being guessed.
- The TUI remains a terminal surface. Font choice and terminal color rendering
stay under the user's terminal emulator rather than OpenRig.
## Release verification
- The accepted product candidate is exact commit
`102dabce4ea56d870243ecd3f6ef38185ec76537`, tree
`4b7898b6829bd3e28113bfd996f67bb1cc2761e9`, with independent whole-outcome
evidence for the composed source heads and focused QA on the final TUI repair.
- The build VM ran the accepted candidate as a healthy, clean daemon. SQLite
integrity passed, and the 0.5.9 migration verifier reported a complete fresh
canonical Claude telemetry pair with no legacy tails.
- Release preparation additionally rebuilds the exact versioned cut, scans the
complete npm inventory for private substance, installs the frozen tarball in
isolation, and binds its runtime identity and public capability reads to the
cut. Those receipts travel with the private publication packet.
+213
View File
@@ -0,0 +1,213 @@
# OpenRig v0.6.0 — works for strangers
Changes for version **0.6.0**. Publication status and date are recorded on the
[GitHub release](https://github.com/mvschwarz/openrig/releases/tag/v0.6.0) and npm.
OpenRig 0.6.0 requires Node.js 22 or 24. It adds per-seat permission choices, a
per-seat typing guard for seats where you type by hand, and a one-step way to open
a whole rig in Herdr. It fixes Pi seat activity and clarifies the kernel starter's
runtime choice.
It also includes two **experimental** features: Slack app manifest and setup
assistance, and an optional stream classifier (Jev with Rig Stream) that is off by
default. They may be incomplete or not work in your setup. See
[Report experimental problems](#report-experimental-problems).
## Breaking change: Node.js 22 or 24
OpenRig 0.6.0 supports Node.js 22 and 24 only, and uses better-sqlite3 13, which
requires Node 22 or newer. Node 20 is no longer supported: the install check
refuses it with an explanation. Node 26 and other versions are untested.
If you run OpenRig on Node 20, switch Node first, then reinstall the CLI under
the new Node. Your existing data stays where it is; the daemon reopens the same
database and applies pending migrations in place. See
[Moving off Node 20](../../README.md#moving-off-node-20).
OpenRig runs on macOS or Linux. Native Windows is not supported yet, and WSL2
has not been tested. On a Mac with Apple silicon, use Node.js 22 (see the
[compatibility history](v0.5.15.md#known-compatibility-limitation)).
## Choose permissions per seat
`rig seat set-permissions <seat> --mode <mode> --reason <text>` records an audited
permission choice for a seat's future launches: `floor`, `full_bypass`, or
`inherit` to clear the override. Codex `full_bypass` now passes
`-s danger-full-access -a never`, so it bypasses approvals as well as the
sandbox. Additional Claude Code modes such as `auto` are accepted only when the
seat's managed Claude executable advertises them; OpenRig checks this at
selection and again at launch, and refuses without a fallback.
The choice does not relaunch the seat or change its current process, history,
rules or hooks. `rig seat status` shows the selected mode and the last launch
arguments; neither proves the native permission behavior, so check the running
session before relying on it. Rig-level policy moves to
`rig policy permissions list|show|current|apply`; the old `rig policy` verbs
remain as aliases. See the [permission guide](../reference/getting-started.md#per-seat-permission-mode).
## Protect a seat where you type by hand
`rig seat set-typing-guard <seat> --enabled true --reason <text>` pauses all
automatic terminal input to that seat, even at an empty prompt. Messages and
wakes are kept in the outbox instead of being typed in; read them with
`rig seat held-messages <seat>` and retire reviewed ones with
`rig seat retire-held-message <seat> <id> --reason <text>`. Turning the guard off
allows new sends; it never replays held messages.
The guard is off by default, and other seats keep automatic delivery. Wait for
`rig seat status <seat>` to report it as effective before relying on it. It covers
OpenRig's own input paths, not other tools writing directly to the terminal.
`rig seat set-typing-guard --help` lists the retention limits.
## Experimental: Slack app manifest and setup assistance
`rig slack manifest` prints the Slack app manifest for the app you create in your
own workspace. It runs offline, before any daemon or token exists; `--url`
prints Slack's create-app link with the manifest prefilled, and `--json` lists
each requested scope with its reason. The TUI Connections page shows the same
link while Slack is not configured. OpenRig does not host an app, open a
browser or accept tokens here.
This manifest and the [Slack setup guide](../reference/slack-app-setup.md) are
experimental. The guide's creation steps follow Slack's manifest documentation
and have not been confirmed against a real app creation: we have not observed
which fields Slack prefills or a complete set of granted scopes. You still
create the tokens and the private env file by hand. The existing Slack connector
and its `rig slack setup`, `verify`, `enable`, `disable` and `status` commands are
not experimental; `setup` and `status` now also point you to the manifest.
`rig slack verify` checks only the baseline scopes, so a READY result does not
prove attachments or mentions work.
## Open a whole rig in Herdr
In the TUI, a rig's detail view has a `term ▸ rig <name>` link, and the command
bar accepts `terminal <view>`. With Herdr, every running seat of the rig opens in
a workspace named after the rig, up to 16 seats per tab (cmux keeps its own
limits). The empty starting tab is closed only when OpenRig can confirm the rig's
own tabs opened; otherwise it is kept, and the result says so. Seats that are not
running are reported as absent or degraded rather than opened.
## First-use and compatibility fixes
- Pi seat activity reports now carry the seat's occupant generation, and the
daemon accepts them only when that generation matches the seat's current
occupant. Reports with a missing, earlier or foreign generation are refused
instead of being attributed to the seat.
- The kernel starter's summary now says when it shows the library preview,
explains that the runtime variant is chosen automatically, and points to the
seats actually running.
- Codex seats launch with `--no-daemon` when the installed Codex supports it, so
each seat's tools run in its own app-server instead of a shared daemon. OpenRig
checks support before each launch and refuses the launch if the check fails.
- Missions and slices created with `rig scope` no longer fail the readiness
reader when their manifest has no `metadata` block.
- Choosing `CLAUDE.local.md` for Claude Code managed blocks, added in 0.5.16, is
included unchanged.
## Experimental: stream classification (Jev with Rig Stream)
OpenRig 0.6.0 includes an optional, experimental way for a classifier seat to
have Jev, through OpenRouter, label Rig Stream observations. It may be incomplete
or not work. It is off by default; turn it on for a local configuration file:
```sh
rig project experimental status --config ./experiment.json --json
rig project experimental enable --config ./experiment.json --max-requests 3 --timeout-ms 10000 --json
rig project experimental disable --config ./experiment.json --json
```
Enabling starts no processing, capture, daemon or agent. Work happens only when
the classifier seat runs one foreground command, `rig project wake … --experiment
./experiment.json`, which allows 1–20 requests (default 3) and never retries in
the background. The run needs `OPENROUTER_API_KEY` in that process's environment
and sends the selected items' text, the taxonomy questions, scope IDs and roster
destinations to OpenRouter. Disable stops further calls and discards late
results; Ctrl-C cancels the run, though a request already forwarded may still be
charged.
The labels are advisory. They have no demonstrated accuracy or calibrated
confidence, and this release adds no delivery, watchdog or routing consumer for
them. `rig project experimental capture` applies the same client to an existing
shadow-capture archive, and `rig project shadow-status`, `shadow-drain` and
`shadow-stop` manage capture, which needs its own separately configured daemon
start; `rig project shadow-stop` stops it. See
[stream classification](../reference/stream-classifier-worker.md) for the taxonomy
format, request limits and error handling.
## Report experimental problems
If the Slack manifest, the setup guide or the stream classifier does not work as
described, please [open an issue](https://github.com/mvschwarz/openrig/issues/new/choose)
with the bug template, or send a pull request following
[CONTRIBUTING.md](../../CONTRIBUTING.md). A problem with the existing Slack
connector or any other existing feature is a regular bug report.
## Known limitations
- Per-seat permissions: authenticated native enforcement across fresh launch,
resume and same-seat fork was not verified before release. Status output and
launch arguments do not prove a selected mode's effective permissions; inspect
the running session.
- Slack manifest and setup assistance (experimental): creating the app in a real
workspace, Slack's prefilled fields and the complete granted scopes have not
been observed against the published steps.
- Herdr: check that the rig opened as expected in your Herdr version.
- Pi: activity reporting from an installed Pi seat has not been verified end to end.
- Stream classification (experimental): an installed run and real OpenRouter
access have not been verified, and its output may be incomplete or wrong. No
accuracy or production-reliability claim is made.
- The Codex hook-trust dialog report in [#17](https://github.com/mvschwarz/openrig/issues/17)
has not been reproduced, and this release does not change it.
- Node 26 and odd-numbered Node versions are not supported or tested.
## Verification scope
Release preparation is tracked in [#109](https://github.com/mvschwarz/openrig/pull/109).
An ordinary candidate-tarball installation passed on a fresh macOS 15.7.3 ARM64
VM with Node.js 22.22.1 and the real npm postinstall. Checks covered
better-sqlite3 13.0.3, all 89 migrations, database writes and close/reopen, and
normal unauthenticated daemon startup and shutdown.
Prior candidate checks on Node 24 covered installed SQLite, migrations and
allocation-driven garbage collection. The relevant install and database
mechanisms are unchanged. That installation used a staged offline postinstall;
it was not a fresh-machine install. A fresh Node 24 VM installation was not
performed because of host resource pressure.
These checks do not establish an authenticated first-agent launch, a fresh
Linux or Windows installation, installation without compiler tools, or an
installation downloaded from the npm registry. The final artifact will be
separately bound to the merged release commit.
## Contributors and included pull requests
Thanks to:
- [@jimallen](https://github.com/jimallen) for reporting the Node 26 installation
failure and proposing the better-sqlite3 13 upgrade in
[#16](https://github.com/mvschwarz/openrig/pull/16).
- [@shravansumanthanan](https://github.com/shravansumanthanan) for the mission
readiness fix in [#91](https://github.com/mvschwarz/openrig/pull/91), and
[@kainne44](https://github.com/kainne44) for reporting it in
[#72](https://github.com/mvschwarz/openrig/issues/72).
- [@mvdpoel](https://github.com/mvdpoel) for repairing two daemon test fixtures in
[#84](https://github.com/mvschwarz/openrig/pull/84).
- [@reisalbuquerque](https://github.com/reisalbuquerque) for reporting the shared
Codex app-server identity problem in
[#69](https://github.com/mvschwarz/openrig/issues/69), fixed in
[#77](https://github.com/mvschwarz/openrig/pull/77).
- [@DoowanKang](https://github.com/DoowanKang) for
[#29](https://github.com/mvschwarz/openrig/issues/29) and
[#30](https://github.com/mvschwarz/openrig/issues/30),
[@djogss](https://github.com/djogss) for
[#33](https://github.com/mvschwarz/openrig/issues/33),
[@some-marketing](https://github.com/some-marketing) for
[#48](https://github.com/mvschwarz/openrig/issues/48),
[@shintaii](https://github.com/shintaii) for
[#26](https://github.com/mvschwarz/openrig/issues/26) and
[@tgrundtvig](https://github.com/tgrundtvig) for
[#21](https://github.com/mvschwarz/openrig/issues/21).
The platform statement in [#94](https://github.com/mvschwarz/openrig/pull/94)
answers the Windows question in [#88](https://github.com/mvschwarz/openrig/issues/88);
native Windows support is not part of this release.
+107
View File
@@ -0,0 +1,107 @@
# OpenRig 0.6.1 — First-use repairs and one help path
OpenRig 0.6.1 improves first-use guidance, Codex readiness and linked-worktree
launches, with a single help path and regression checks on pull requests.
## Find help from the installed version
Ask your agent to read:
```sh
rig context get help
```
The [help guide](../reference/help.md) brings together environment checks,
setup and restart symptoms, permissions, instance layout and escalation. Its
linked reference guides are included with the package too. If OpenRig cannot
install or `rig` cannot run, use the guide online at
[openrig.dev/help/agents](https://www.openrig.dev/help/agents), or read
`daemon/docs/reference/help.md` inside the installed `@openrig/cli` package.
For help, email **[email protected]**; a GitHub account is not required.
This is the canonical guide added in
[#113](https://github.com/mvschwarz/openrig/pull/113), with entry points from the
README and agent startup guidance.
## Read a catalogued project's execution view
```sh
rig view show execution --project <catalog-id> --mission <mission> --json
```
The CLI now forwards the selected project and mission to the existing daemon
resolver, which uses that project's configured missions root. Previously the
CLI could only use the default workspace for this view. Omitting `--project`
keeps the existing behavior. This does not add project inference from the
current directory. Thanks to [@dajiaohuang](https://github.com/dajiaohuang) for
[#105](https://github.com/mvschwarz/openrig/pull/105).
## Codex readiness and bundled guidance
- Startup/resume checks recognize Codex's `»` conversation prompt, including a
fresh prompt after a dismissed hook-review panel. Numbered selection menus
still require attention. Thanks to
[@dajiaohuang](https://github.com/dajiaohuang) for
[#111](https://github.com/mvschwarz/openrig/pull/111).
- The bundled Vault skill has valid frontmatter, with a regression check for
shipped skill headers ([#115](https://github.com/mvschwarz/openrig/pull/115)).
- Historical development evidence has been removed from the public source tree
([#118](https://github.com/mvschwarz/openrig/pull/118)). The retired
`spike/tui-drivability` prototype was also removed
([#119](https://github.com/mvschwarz/openrig/pull/119)); the shipped TUI remains.
## Identity readings and Codex launches
- **Boot-time comparisons on non-UTC hosts:** parse SQLite's zone-less boot
timestamp as UTC. Identity and context-usage readers then compare observations
with the actual generation start, avoiding time-zone shifts that could reject
current observations or accept older ones. Thanks to
[@Coder8124](https://github.com/Coder8124) for
[#124](https://github.com/mvschwarz/openrig/pull/124).
- **Custom Codex status lines:** recognize an interactive conversation when its
header has scrolled away and the status line puts a mixed-case model name
after other fields. Unresolved trust, hook, update and model-selection gates
remain in place. This is recognition only; the separate stale restore-warning
behavior from [#116](https://github.com/mvschwarz/openrig/issues/116) is not
repaired here. Thanks to [@Hexgunner69](https://github.com/Hexgunner69) for the
report and [@Aummadour](https://github.com/Aummadour) for screen-derived
regression cases included in [#125](https://github.com/mvschwarz/openrig/pull/125).
- **Linked Git worktrees:** Codex fresh launches resolve the worktree's Git
directory and common Git directory instead of treating its `.git` file as a
directory. Resume/fork arguments and permission modes are unchanged. Thanks to
[@mgall-ibizdigital](https://github.com/mgall-ibizdigital) for the report,
reproduction and suggested approach in
[#121](https://github.com/mvschwarz/openrig/issues/121), addressed by
[#126](https://github.com/mvschwarz/openrig/pull/126).
## Regression checks on pull requests
[#117](https://github.com/mvschwarz/openrig/pull/117) adds eight test jobs: build
and package-file inspection, type checking, repository checks, four separate
daemon/CLI/TUI/UI suites, and an installed queue-durability scenario. The latter
checks a normal run, an intentionally introduced lost-baton failure and a
healthy return. A checker that misses that seeded failure cannot pass.
The workflow uses Node 22, Linux runners for the build/repository/installed
scenario jobs, and macOS runners for the four package suites. It does not run
all eleven historical scenario seeds or certify every platform, provider or
authenticated native-agent workflow.
## Upgrade and compatibility
Node.js 22 and 24 remain the supported versions; Node 20 is unsupported. No
dependency, engine, permission-mode or database-migration change is introduced
by this maintenance release. Follow the existing
[upgrade procedure](../../skills/_canonical/core/openrig-upgrade/SKILL.md)
when updating an existing instance.
Package identities and the checks performed on each published artifact belong
with its [GitHub release](https://github.com/mvschwarz/openrig/releases). These
source notes do not certify every platform or authenticated native workflow.
Native permission enforcement across fresh launch, resume and fork retains
the verification limits recorded for 0.6.0.
Slack app manifest/setup assistance and Rig Stream classification remain
**experimental**. This release makes no new real-app, provider-run, accuracy or
reliability claim for them. The existing Slack connector is not experimental.
See the [0.6.0 limitations](v0.6.0.md#known-limitations) for retained boundaries.
+114
View File
@@ -0,0 +1,114 @@
# OpenRig 0.6.2 — Start with the team you already have accounts for
Choose your providers, give an owner one useful change in your repository, and
have a second agent check the result. OpenRig 0.6.2 adds first-project choices,
clearer permission guidance and repairs for bundle installation and agent delivery.
## A small team and one useful result
The [first-use guide](../reference/getting-started.md) offers the same owner and
checker workflow with three recipes:
| Team | Recipe |
| --- | --- |
| Two Codex agents | `first-project` |
| Two Claude Code agents | `first-project-claude` |
| Claude owner and Codex checker | `first-project-mixed` |
Use the accounts you already have; an unused provider's installation or login is
not a prerequisite. Codex seats retain `gpt-6-astra`; Claude seats use the native
configured default. Check model access before launching. Automatic kernel startup
continues to select independently from available authenticated providers.
Agent-guided setup asks once whether to allow OpenRig commands without repeated
permission prompts, with **Yes recommended**. An explicit Yes authorizes native
`rig` command rules at personal project scope unless you choose user-wide scope.
No or no answer leaves settings unchanged. The allowance includes lifecycle and
configuration commands, but does not grant general shell access or change sandbox
and network settings. The guide covers applying, checking and undoing the rules;
bootstrap does not silently apply them
([#147](https://github.com/mvschwarz/openrig/pull/147)).
## Bundle files stay where you install them
Schema-version-2 bundles now copy their verified contents into `--target` and
launch from there, keeping relative working directories and agent references
available after temporary extraction is removed. Different files at a bundle
path cause `target_conflict` before copying; identical files can be reused.
Without `--target`, `rig up <file>.rigbundle` installs into your current directory, so run it from the project folder you want.
`rig bundle create|inspect|install` and local `rig up --target` resolve relative
paths against your current directory. Files still live on the daemon's host;
this is not an upload. Install launches the rig. Legacy version-1 behavior and
the separate `--cwd` override are unchanged. See the
[bundle guide](../reference/rig-bundle.md#cli-surface)
([#146](https://github.com/mvschwarz/openrig/pull/146)).
## Delivery and restore diagnostics
Managed launches after a tmux/host restart preserve the named launch target
instead of confusing it with an old bare pane binding. The existing identity
checks still apply to each write. Thanks to
[@diaztunjano](https://github.com/diaztunjano) for reporting
[#141](https://github.com/mvschwarz/openrig/issues/141), addressed by
[#151](https://github.com/mvschwarz/openrig/pull/151). Actual reboot and power-loss
recovery remain subject to the verification limits below.
Message delivery now refuses a known bare-shell foreground when an agent runtime
should be running, so the message is not entered as shell commands. Terminal-only
seats still receive text; unreadable or unknown foregrounds retain the existing
advisory behavior ([#150](https://github.com/mvschwarz/openrig/pull/150), fixes
[#142](https://github.com/mvschwarz/openrig/issues/142)).
## Community fixes
- **Local sender identity:** recognize a sender qualified with this daemon's own
host ID as its local seat, avoiding false ownership refusals. Foreign host
qualifiers and destination handling stay unchanged. Thanks to
[@korallis](https://github.com/korallis) for
[#135](https://github.com/mvschwarz/openrig/pull/135), and
[@Yi-111-a](https://github.com/Yi-111-a) for the additional cases from
[#137](https://github.com/mvschwarz/openrig/pull/137), included through
[#144](https://github.com/mvschwarz/openrig/pull/144).
- **Terminal and status details:** parse tmux window fields with printable
separators ([#101](https://github.com/mvschwarz/openrig/pull/101)); distinguish
health that was not requested from a failed read
([#102](https://github.com/mvschwarz/openrig/pull/102)); use native realpath for
Codex hook trust ([#103](https://github.com/mvschwarz/openrig/pull/103)); pass
known Claude/Codex runtime hints to Herdr
([#106](https://github.com/mvschwarz/openrig/pull/106)); report missing fresh
startup context or adapters as needing attention
([#107](https://github.com/mvschwarz/openrig/pull/107)); accept numeric Claude
rate-limit reset timestamps ([#108](https://github.com/mvschwarz/openrig/pull/108)).
Thanks to [@dajiaohuang](https://github.com/dajiaohuang).
- **Pi input and portable checks:** retain line editing with `TERM=dumb`, and
correct macOS path and timezone assumptions in tests. Thanks to
[@shravansumanthanan](https://github.com/shravansumanthanan) for
[#123](https://github.com/mvschwarz/openrig/pull/123).
- **Configured restore paths:** restore-check uses the configured shared-docs
root. This does not repair every issue in
[#130](https://github.com/mvschwarz/openrig/issues/130). Thanks to
[@jbaehova](https://github.com/jbaehova) for
[#139](https://github.com/mvschwarz/openrig/pull/139).
- **Anthropic gateways:** permit `ANTHROPIC_BASE_URL` in configured provider-auth
environment forwarding. Thanks to [@FenjuFu](https://github.com/FenjuFu) for
[#143](https://github.com/mvschwarz/openrig/pull/143).
- **Terminal authentication maintenance:** reuse the shared token-comparison
helper. Thanks to [@Sunil56224972](https://github.com/Sunil56224972) for
[#145](https://github.com/mvschwarz/openrig/pull/145).
## Upgrade and verification limits
Node.js 22 and 24 remain supported; Node 20 is unsupported. This maintenance
change adds no dependency, engine or database-migration change. Follow the
[existing upgrade procedure](../../skills/_canonical/core/openrig-upgrade/SKILL.md).
Installed first-use and reboot/power-loss verification remain pending. Source
and regression checks do not establish native permission enforcement or recovery
of an authenticated team after a reboot.
Slack app manifest/setup assistance and Rig Stream classification remain
experimental, with no new real-app or provider-run validation here. The existing
Slack connector is not experimental. See the
[retained limitations](v0.6.0.md#known-limitations).
+62
View File
@@ -0,0 +1,62 @@
# OpenRig 0.6.3 — Managed Claude messaging repair
This patch repairs a 0.6.2 regression that refused messages to running Claude
Code seats launched with an explicit permission mode. It also fixes snapshot
helper cleanup and interference from archived rigs with duplicate names.
## Managed Claude seats can receive messages
OpenRig's managed launch can leave a shell wrapper in front of Claude Code.
The delivery check previously treated that wrapper as a bare shell and refused
`rig send`, even though Claude was running beneath it.
The check now recognizes a stable, foreground Claude process descended from the
bound pane, with the expected native session identity. A bare shell alone still
does not qualify. Fork arguments that identify the parent rather than the
current occupant remain insufficient evidence.
Thanks to [@dmelo](https://github.com/dmelo) for the detailed
[#197 report](https://github.com/mvschwarz/openrig/issues/197), addressed by
[#220](https://github.com/mvschwarz/openrig/pull/220).
## Snapshot helpers and archived rigs
- Clean up a newly created snapshot helper when its input target cannot be
established and its ownership can still be proven. This prevents unusable
`rigged-refresh-*` helpers accumulating on repeated attempts. If ownership
cannot be established, preserve the session and report the limitation.
Thanks to [@z4cc](https://github.com/z4cc) for
[#188](https://github.com/mvschwarz/openrig/issues/188), addressed by
[#189](https://github.com/mvschwarz/openrig/pull/189).
- Exclude archived rigs from seat-reference resolution, prefer unarchived
delivery targets, and preserve another live seat's session and queue work
when removing a stale duplicate node. Removal reports whose session it kept.
Thanks to [@Farkinell](https://github.com/Farkinell) for
[#174](https://github.com/mvschwarz/openrig/issues/174), addressed by
[#181](https://github.com/mvschwarz/openrig/pull/181).
## Proof media and repository checks
`rig proof add` rejects binary files supplied as artifact text, requires a
Markdown artifact name, and requires `--replace` before overwriting an existing
artifact. Attach screenshots and other binary evidence with `--media`. This
prevents a proof write from replacing media with a Markdown body
([#177](https://github.com/mvschwarz/openrig/pull/177)).
Repository script tests now run serially because packaging tests rebuild daemon
output that other tests import. This removes the reproduced shared-output race
without weakening those tests
([#221](https://github.com/mvschwarz/openrig/pull/221)).
## Upgrade and verification scope
Node.js 22 and 24 remain supported. This patch changes no dependencies, engine
requirements or database migrations. Use the
[existing upgrade procedure](../../skills/_canonical/core/openrig-upgrade/SKILL.md).
The fixes have source reviews and focused regression coverage. The Claude
process checks use synthetic process metadata; an installed Claude/Fedora send
has not been demonstrated by that evidence. These checks do not establish
native receipt, automatic recovery, or protection against a process exiting
between verification and input. Final package and publication verification
remain separate from this source preparation.
+34 -473
View File
File diff suppressed because it is too large. Load diff
+6 -3
View File
@@ -1,6 +1,6 @@
{
"name": "openrig",
"version": "0.5.1",
"version": "0.6.3",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -13,7 +13,7 @@
"scripts": {
"build": "npm run build --workspaces",
"test": "npm run test:repo && npm run test:workspaces",
"test:repo": "node --test scripts/*.test.mjs && node scripts/check-docs-guard.mjs && node scripts/mirror-skills.mjs --check",
"test:repo": "npm run build -w packages/daemon && node --test --test-concurrency=1 scripts/*.test.mjs && node scripts/check-docs-guard.mjs && node scripts/mirror-skills.mjs --check && node scripts/generate-context-packs.mjs --check",
"test:workspaces": "npm run test -w packages/daemon -w packages/cli -w packages/tui",
"test:ui": "npm run test -w packages/ui",
"typecheck:prep": "npm run build -w packages/daemon",
@@ -22,9 +22,12 @@
"test:tui-package": "node scripts/test-tui-package.mjs",
"mirror-skills": "node scripts/mirror-skills.mjs",
"mirror-skills:check": "node scripts/mirror-skills.mjs --check",
"generate-context-packs": "node scripts/generate-context-packs.mjs",
"generate-context-packs:check": "node scripts/generate-context-packs.mjs --check",
"gate:substance": "node scripts/check-substance-gate.mjs",
"gate": "node scripts/gate-lane.mjs"
},
"engines": {
"node": "^20 || ^22 || ^24"
"node": "^22 || ^24"
}
}
+3
View File
@@ -1,3 +1,6 @@
# Build artifacts assembled by scripts/build-package.sh
daemon/
ui/
# Root license copied by the CLI build
/LICENSE
+6 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@openrig/cli",
"version": "0.5.1",
"version": "0.6.3",
"type": "module",
"description": "Local control plane for multi-agent coding topologies",
"keywords": [
@@ -30,6 +30,7 @@
"openrig-tui": "tui/dist/main.js"
},
"files": [
"LICENSE",
"dist",
"daemon",
"ui",
@@ -37,7 +38,7 @@
"scripts"
],
"scripts": {
"build": "tsc && chmod +x dist/bin-wrapper.js && mkdir -p dist/schemas && cp src/schemas/*.json dist/schemas/ && mkdir -p dist/lib/scope-templates && cp src/lib/scope-templates/*.md dist/lib/scope-templates/",
"build": "tsc && chmod +x dist/bin-wrapper.js && mkdir -p dist/schemas && cp src/schemas/*.json dist/schemas/ && mkdir -p dist/lib/scope-templates && cp src/lib/scope-templates/*.md dist/lib/scope-templates/ && cp ../../LICENSE LICENSE",
"postinstall": "node scripts/check-abi.mjs",
"prepublishOnly": "bash ../../scripts/build-package.sh",
"test": "vitest run",
@@ -47,27 +48,24 @@
"@hono/node-server": "^1.13.0",
"@hono/node-ws": "^1.3.1",
"@modelcontextprotocol/sdk": "^1.28.0",
"@openrig/daemon": "*",
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",
"better-sqlite3": "^11.8.0",
"better-sqlite3": "^13.0.3",
"commander": "^13.0.0",
"hono": "^4.7.0",
"jsonc-parser": "^3.3.1",
"smol-toml": "^1.8.0",
"tar": "^7.5.13",
"ulid": "^2.3.0",
"yaml": "^2.8.3",
"zod": "^4.3.6"
},
"bundledDependencies": [
"@openrig/daemon"
],
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
},
"engines": {
"node": ">=20"
"node": ">=22"
}
}
+120 -66
View File
@@ -1,103 +1,153 @@
#!/usr/bin/env node
// Postinstall ABI sanity check for @openrig/cli.
// Fails loudly if better-sqlite3 native binary does not match the active
// Node runtime. Layered on top of the engines.node constraint in
// package.json — engines warns at npm level; this hard-blocks at install
// time with a copy-paste fix.
// Postinstall native-SQLite check for @openrig/cli.
// OpenRig supports Node.js 22 and 24. better-sqlite3 13 requires Node 22 or
// newer; on Node 20 it installs and loads, then crashes (SIGSEGV) the first
// time a database is opened. This check refuses unsupported majors BEFORE any
// native code runs, then proves the binding can actually open a database.
// The open runs in a child process so a native crash is reported, not silent.
const SUPPORTED_MAJORS = [22, 24];
const box = (lines) =>
[
"",
" ╔══════════════════════════════════════════════════════════════╗",
...lines.map((line) => ` ║ ${line.padEnd(60)}║`),
" ╚══════════════════════════════════════════════════════════════╝",
"",
].join("\n");
const FIX_INSTALL = "Fix: nvm install 22 && npm install -g @openrig/cli";
/**
* @param {{ nodeVersion: string, loadNativeAddon: () => void }} deps
* @returns {{ ok: true } | { ok: false, message: string }}
* @param {{
* nodeVersion: string,
* loadNativeAddon: () => void,
* openNativeDatabase?: () => { ok: true } | { ok: false, detail: string },
* }} deps
* @returns {{ ok: true, warning?: string } | { ok: false, message: string }}
*/
export function checkAbi({ nodeVersion, loadNativeAddon }) {
// Phase 1: version-range check (fast path).
// Even-numbered Node majors (20, 22, 24) are LTS lines with native addon
// prebuilds. Odd-numbered majors (21, 23, 25) generally lack them.
export function checkAbi({ nodeVersion, loadNativeAddon, openNativeDatabase }) {
// Phase 1: version-range check. Runs before any native code is loaded.
const match = nodeVersion.match(/^v?(\d+)/);
const major = match ? parseInt(match[1], 10) : 0;
if (major < 20) {
if (major < SUPPORTED_MAJORS[0]) {
return {
ok: false,
message: [
message: box([
"@openrig/cli requires Node.js 22 or 24 (LTS).",
`Current: ${nodeVersion}`,
"",
" ╔══════════════════════════════════════════════════════════════╗",
` ║ @openrig/cli requires Node.js 20, 22, or 24 (LTS). ║`,
` ║ Current: ${nodeVersion.padEnd(49)}║`,
" ║ ║",
" ║ Fix: nvm install 22 && npm install -g @openrig/cli ║",
" ╚══════════════════════════════════════════════════════════════╝",
"Node 20 is no longer supported: the SQLite binding",
"(better-sqlite3 13) crashes when it opens a database there.",
"",
].join("\n"),
FIX_INSTALL,
]),
};
}
if (major % 2 !== 0) {
return {
ok: false,
message: [
message: box([
"@openrig/cli does not support odd-numbered Node releases.",
`Current: ${nodeVersion}`,
"",
" ╔══════════════════════════════════════════════════════════════╗",
` ║ @openrig/cli does not support odd-numbered Node releases. ║`,
` ║ Current: ${nodeVersion.padEnd(49)}║`,
" ║ ║",
" ║ Odd Node versions (21, 23, 25, …) lack native addon ║",
" ║ prebuilds for better-sqlite3. The daemon will fail. ║",
" ║ ║",
" ║ Fix: nvm install 22 && npm install -g @openrig/cli ║",
" ╚══════════════════════════════════════════════════════════════╝",
"Supported: Node.js 22 and 24 (LTS).",
"",
].join("\n"),
FIX_INSTALL,
]),
};
}
// Phase 2: ABI load check (catches edge cases like local Node builds
// or prebuild packaging gaps on supported versions).
// Even majors above the supported range are untested, not refused.
const warning = SUPPORTED_MAJORS.includes(major)
? undefined
: box([
`Node ${major} is untested with @openrig/cli.`,
`Current: ${nodeVersion}`,
"",
"Supported: Node.js 22 and 24 (LTS). The SQLite check below",
"passed, but other behavior on this Node is unverified.",
]);
// Phase 2: load the native addon (ABI / packaging / permission problems).
try {
loadNativeAddon();
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
const isAbiMismatch = detail.includes("NODE_MODULE_VERSION");
if (isAbiMismatch) {
return {
ok: false,
message: [
"",
" ╔══════════════════════════════════════════════════════════════╗",
" ║ better-sqlite3 native binary does not match this Node. ║",
` ║ Current: ${nodeVersion.padEnd(49)}║`,
" ║ ║",
" ║ Fix: npm rebuild better-sqlite3 ║",
" ║ or: nvm install 22 && npm install -g @openrig/cli ║",
" ╚══════════════════════════════════════════════════════════════╝",
"",
` Detail: ${detail}`,
"",
].join("\n"),
};
}
return {
ok: false,
message: [
"",
" ╔══════════════════════════════════════════════════════════════╗",
" ║ better-sqlite3 native addon failed to load. ║",
` ║ Current: ${nodeVersion.padEnd(49)}║`,
" ║ ║",
" ║ Fix: npm rebuild better-sqlite3 ║",
" ║ If that fails, reinstall with a supported Node version: ║",
" ║ nvm install 22 && npm install -g @openrig/cli ║",
" ╚══════════════════════════════════════════════════════════════╝",
"",
` Detail: ${detail}`,
"",
].join("\n"),
message:
box(
isAbiMismatch
? [
"better-sqlite3 native binary does not match this Node.",
`Current: ${nodeVersion}`,
"",
"Fix: npm rebuild better-sqlite3",
" or: nvm install 22 && npm install -g @openrig/cli",
]
: [
"better-sqlite3 native addon failed to load.",
`Current: ${nodeVersion}`,
"",
"Fix: npm rebuild better-sqlite3",
"If that fails, reinstall with a supported Node version:",
" nvm install 22 && npm install -g @openrig/cli",
],
) + ` Detail: ${detail}\n`,
};
}
// Phase 3: open an in-memory database. Loading alone does not prove the
// binding works (Node 20 + better-sqlite3 13 loads, then segfaults here).
if (openNativeDatabase) {
const opened = openNativeDatabase();
if (!opened.ok) {
return {
ok: false,
message:
box([
"better-sqlite3 loaded but could not open a database.",
`Current: ${nodeVersion}`,
"",
"Fix: npm rebuild better-sqlite3",
"If that fails, reinstall with Node 22 or 24:",
" nvm install 22 && npm install -g @openrig/cli",
]) + ` Detail: ${opened.detail}\n`,
};
}
}
return warning ? { ok: true, warning } : { ok: true };
}
/**
* Open an in-memory database in a child process, so a native crash
* (e.g. SIGSEGV) is observed as a signal instead of killing this script.
* @param {string} addonPath absolute path resolved from this package
*/
export function openInChildProcess(addonPath, spawnSync) {
const probe =
`const Database = require(${JSON.stringify(addonPath)});` +
`const db = new Database(":memory:");` +
`db.prepare("select sqlite_version() as v").get();` +
`db.close();`;
const result = spawnSync(process.execPath, ["-e", probe], {
encoding: "utf8",
timeout: 30_000,
});
if (result.error) return { ok: false, detail: result.error.message };
if (result.signal) return { ok: false, detail: `database open was killed by ${result.signal}` };
if (result.status !== 0) {
const stderr = (result.stderr || "").trim().split("\n").slice(-3).join(" ");
return { ok: false, detail: `database open exited ${result.status}${stderr ? `: ${stderr}` : ""}` };
}
return { ok: true };
}
@@ -109,15 +159,19 @@ const isMain =
if (isMain) {
const { createRequire } = await import("node:module");
const { spawnSync } = await import("node:child_process");
const require = createRequire(import.meta.url);
const result = checkAbi({
nodeVersion: process.version,
loadNativeAddon: () => require("better-sqlite3"),
openNativeDatabase: () => openInChildProcess(require.resolve("better-sqlite3"), spawnSync),
});
if (!result.ok) {
console.error(result.message);
process.exitCode = 1;
} else if (result.warning) {
console.error(result.warning);
}
}
+12
View File
@@ -52,6 +52,10 @@ function maybeReexecWithSiblingNode(argv = process.argv): void {
if (!preferredNode) return;
const scriptPath = argv[1] ? realpathSync(argv[1]) : fileURLToPath(import.meta.url);
// An alias is not runtime authority. Keep a working interpreter; only switch
// when the installed native dependency actually loads under the alternative.
if (canLoadInstalledNative(process.execPath, scriptPath)) return;
if (!canLoadInstalledNative(preferredNode, scriptPath)) return;
const result = spawnSync(preferredNode, [scriptPath, ...argv.slice(2)], {
stdio: "inherit",
env: {
@@ -67,6 +71,14 @@ function maybeReexecWithSiblingNode(argv = process.argv): void {
process.exit(result.status ?? 0);
}
export function canLoadInstalledNative(node: string, wrapper: string): boolean {
const probe = spawnSync(node, ["--input-type=module", "-e",
"import {createRequire} from 'node:module'; const require=createRequire(process.argv[1]); const D=require('better-sqlite3'); new D(':memory:').close();",
wrapper,
], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "pipe"] });
return probe.status === 0;
}
export async function run(argv = process.argv): Promise<void> {
const normalizedArgv = [...argv];
if (normalizedArgv[1]) {
+1 -1
View File
@@ -112,7 +112,7 @@ export function renderDaemonTransportError(
fact: e.message,
consequence: "The command's outcome is UNKNOWN — the daemon did not answer in time.",
action:
"The daemon is slow or unresponsive (check load); retry once conditions ease. This is a slow response, not a stopped daemon.",
"Check the command's effect before any retry, then inspect daemon load with 'rig status' and the daemon logs if it remains slow. This is a slow response, not a stopped daemon.",
};
} else if (e instanceof DaemonConnectionError) {
parts = {
+61 -38
View File
@@ -3,6 +3,7 @@ import path from "node:path";
import { ConfigStore } from "./config-store.js";
import { readOpenRigEnv } from "./openrig-compat.js";
import { fetchWithTimeout, FetchTimeoutError } from "./fetch-with-timeout.js";
import { readLocalOrigin } from "./local-origin.js";
export function terminalAuthHeaders(): Record<string, string> {
const token = resolveTerminalToken();
@@ -23,14 +24,7 @@ export const SENDER_IDENTITY_HEADER = "X-OpenRig-Session";
export function senderIdentityHeaders(originSelfHostId?: string): Record<string, string> {
const session = readOpenRigEnv("OPENRIG_SESSION_NAME", "RIGGED_SESSION_NAME")?.trim();
if (!session) return {};
// A4 HTTP-path origin-triple carry: on a REMOTE-targeting client, compose the origin TRIPLE
// (member@rig@<this host's selfHostId>) at stamp time so the remote daemon's wrapPaneEnvelope
// renders the ORIGIN host, not the destination's (its preserve branch fires on a 3-part sender).
// The identity stays DERIVED — env session + THIS host's own selfHostId — never a caller-supplied
// string (Rail 1 anti-forgery: `originSelfHostId` signals only THAT the client targets a remote host,
// never WHO it is). An already-3-part origin (an upstream relay's triple) is preserved verbatim,
// never re-stamped with this host's id (which would forge the origin). Local clients pass no
// `originSelfHostId` and stamp the 2-part value, byte-identical to today.
// Preserve qualified senders; append only a locally derived instance id.
const value =
originSelfHostId && originSelfHostId.length > 0 && session.split("@").length < 3
? `${session}@${originSelfHostId}`
@@ -38,18 +32,7 @@ export function senderIdentityHeaders(originSelfHostId?: string): Record<string,
return { [SENDER_IDENTITY_HEADER]: value };
}
/**
* A4 — the SINGLE remote-origin construction point (shape b). Every remote-targeting client is built
* HERE so the origin triple is stamped uniformly. Two structural laws the A4 grep-guard enforces:
* (1) no remote site constructs a client by calling `clientFactory(<registry host>.url)` directly —
* they all route through here; (2) `originSelfHostId` is ASSIGNED in exactly ONE place: this function
* (the sole-assignment law, so pin 3 "no caller-supplied identity path" is structurally true).
*
* `originSelfHostId` is THIS host's own id (resolved fail-open via `fetchSelfHostId` on the LOCAL
* daemon), NOT a caller-supplied identity — the caller only signals it targets a remote host. Preserves
* the injected-mock `clientFactory` test seam: the client is built by the injected factory; this only
* sets a field on it afterward (an injected mock harmlessly carries the extra property).
*/
/** Construct registered remote clients with the origin derived from this instance's durable store. */
export function remoteDaemonClient(
clientFactory: (baseUrl: string) => DaemonClient,
url: string,
@@ -57,6 +40,7 @@ export function remoteDaemonClient(
): DaemonClient {
const client = clientFactory(url);
client.originSelfHostId = originSelfHostId;
client.remoteTarget = true;
return client;
}
@@ -93,7 +77,7 @@ export class DaemonTimeoutError extends DaemonConnectionError {
}
/**
* Bad-response: the daemon replied, but the body could not be read as JSON
* Bad-response: the daemon replied, but the body could not be read or parsed as JSON
* (truncated / unparseable / non-JSON — a real symptom under daemon saturation).
* DISTINCT from a stopped or unreachable daemon: the request WAS delivered and
* the outcome is unknown, so this must never render as daemon-not-running.
@@ -129,15 +113,41 @@ export class DaemonClient {
readonly baseUrl: string;
private fetchImpl: typeof fetch = fetch;
private timeoutMs = 5_000;
/**
* A4 HTTP-path origin-triple carry: THIS host's `selfHostId`, set ONLY when this client targets a
* REMOTE host, so `senderIdentityHeaders` stamps the origin TRIPLE (member@rig@selfHostId) instead of
* the 2-part local value. Undefined for local clients (2-part stamp, unchanged). Assigned in EXACTLY
* ONE place — `remoteDaemonClient` — enforced by the A4 grep-guard (the sole-assignment law); this is
* NOT a caller identity string (Rail 1: it is THIS host's derived id, never who the caller claims to
* be). Public (not ctor-injected) to keep the injected-mock `clientFactory` test seam untouched.
*/
/** Set by remoteDaemonClient; absent origin is labelled explicitly on remote requests. */
originSelfHostId?: string;
remoteTarget = false;
private identity?: Promise<Record<string, string>>;
private originUnknown = false;
private async identityHeaders(): Promise<Record<string, string>> {
const localHeaders = senderIdentityHeaders();
const session = localHeaders[SENDER_IDENTITY_HEADER];
if (!session || session.split("@").length >= 3) return localHeaders;
const directTarget = readOpenRigEnv("OPENRIG_URL", "RIGGED_URL");
if (!this.remoteTarget && !directTarget) return localHeaders;
const origin = this.originSelfHostId ?? readLocalOrigin();
if (!origin) {
this.originUnknown = true;
return { ...localHeaders, "X-OpenRig-Origin-Unknown": "true" };
}
if (!this.remoteTarget) {
// Loopback may be a forwarded endpoint. Only matching instance identity proves locality.
const controller = new AbortController();
let timer: ReturnType<typeof setTimeout> | undefined;
try {
const target = await Promise.race([
(async () => {
const res = await this.fetchImpl(`${this.baseUrl.replace(/\/+$/, "")}/healthz`, { signal: controller.signal });
return res.ok ? (await res.json() as { selfHostId?: unknown }).selfHostId : undefined;
})(),
new Promise<undefined>(resolve => { timer = setTimeout(() => resolve(undefined), Math.min(this.timeoutMs, 1000)); }),
]);
if (target === origin) return localHeaders;
} catch { /* Unproved locality: carry the known origin, never the endpoint's identity. */ }
finally { clearTimeout(timer); controller.abort(); }
}
return senderIdentityHeaders(origin);
}
constructor(baseUrl?: string, options?: DaemonClientOptions) {
if (baseUrl) {
@@ -206,26 +216,36 @@ export class DaemonClient {
}, options);
}
private async fetch(path: string, init: RequestInit, options?: DaemonRequestOptions): Promise<Response> {
private async fetch(path: string, init: RequestInit, options?: DaemonRequestOptions, consumeResponse?: (response: Response) => Promise<void>): Promise<Response> {
const timeoutMs = options?.timeoutMs ?? this.timeoutMs;
if (options?.headers) {
init = { ...init, headers: { ...(init.headers as Record<string, string> ?? {}), ...options.headers } };
}
// P18 sender-provenance: stamp the seat-derived identity header LAST, so the transport — never a
// caller-supplied header or a request body — decides the caller identity the channel of record records.
// A4: a remote-targeting client (originSelfHostId set by remoteDaemonClient) stamps the origin TRIPLE;
// local clients stamp the 2-part value, byte-identical to today.
init = { ...init, headers: { ...(init.headers as Record<string, string> ?? {}), ...senderIdentityHeaders(this.originSelfHostId) } };
// Known remote or unproved direct endpoints carry origin; proven local requests remain bare.
this.identity ??= this.identityHeaders();
init = { ...init, headers: { ...(init.headers as Record<string, string> ?? {}), ...await this.identity } };
let responseStatus: number | undefined;
try {
return await fetchWithTimeout(
const response = await fetchWithTimeout(
this.fetchImpl,
`${this.baseUrl}${path}`,
init,
{
timeoutMs,
consumeResponse: consumeResponse ? async (response) => {
responseStatus = response.status;
await consumeResponse(response);
} : undefined,
timeoutMessage: `Request to ${this.baseUrl}${path} timed out after ${timeoutMs}ms`,
},
);
if (response.ok && this.originUnknown) {
console.error("Origin instance unknown: local durable identity is unavailable; delivery continues with origin-unknown provenance.");
this.originUnknown = false;
}
return response;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
// Slow-response (a timed-out request) is a DISTINCT class from no-connect:
@@ -234,18 +254,21 @@ export class DaemonClient {
if (err instanceof FetchTimeoutError) {
throw new DaemonTimeoutError(`The OpenRig daemon at ${this.baseUrl} did not respond in time: ${msg}`);
}
// Headers prove that the daemon received the request, but not that a write
// finished. A dropped body must preserve unknown-outcome guidance.
if (responseStatus !== undefined) throw new DaemonResponseError(responseStatus, "");
throw new DaemonConnectionError(`Cannot connect to the OpenRig daemon at ${this.baseUrl}: ${msg}`);
}
}
private async requestJson<T>(path: string, init: RequestInit, options?: DaemonRequestOptions): Promise<DaemonResponse<T>> {
const res = await this.fetch(path, init, options);
let text = "";
const res = await this.fetch(path, init, options, async (response) => { text = await response.text(); });
// Read the raw body once, THEN parse — so a truncated / unparseable response
// (a real symptom under daemon saturation) surfaces as a typed
// DaemonResponseError carrying the status + a bounded snippet, instead of a
// raw SyntaxError bubbling to a cryptic (json) or silent (human) CLI exit.
// A well-formed non-2xx body still parses and returns {status,data}; 204 has none.
const text = await res.text();
if (res.status === 204) return { status: res.status, data: undefined as T };
try {
return { status: res.status, data: JSON.parse(text) as T };
@@ -255,8 +278,8 @@ export class DaemonClient {
}
private async requestText(path: string, init: RequestInit, options?: DaemonRequestOptions): Promise<DaemonResponse<string>> {
const res = await this.fetch(path, init, options);
const data = await res.text();
let data = "";
const res = await this.fetch(path, init, options, async (response) => { data = await response.text(); });
return { status: res.status, data };
}
}
+1 -1
View File
@@ -84,7 +84,7 @@ export function attachCommand(depsOverride?: StatusDeps): Command {
}
const hasNode = !!opts.node;
const hasPodFields = !!opts.pod || !!opts.member || !!opts.runtime;
const hasPodFields = !!opts.pod || !!opts.member;
if (hasNode && hasPodFields) {
console.error("Specify either --node or --pod + --member + --runtime, not both.");
process.exitCode = 1;
+10 -1
View File
@@ -9,6 +9,7 @@ import { emitCrossHostError, emitRemoteHttpFailure } from "../cross-host-cli-hel
import { runRemoteHttpOp } from "../remote-host-ops.js";
import { resolveContextRef, walkSizedWarning } from "../context-resolve.js";
import { resolveSenderSession, SENDER_FALLBACK } from "../sender-identity.js";
import { refuseEmptyMessage } from "./send.js";
/**
* OPR.0.4.6.MH4 C3 — the cross-host broadcast deadline, named at the call
@@ -71,11 +72,12 @@ selection, not the agent@rig@host sugar.`)
process.exitCode = 1;
return;
}
if ((text === undefined || text.length === 0) && !opts.context) {
if (text === undefined && !opts.context) {
console.error("Provide a message to broadcast (or --context <ref>).");
process.exitCode = 1;
return;
}
if (!opts.context && refuseEmptyMessage(text!, "broadcast", Boolean(opts.json))) return;
// P18 DELIVER-AND-LABEL — the seat boundary no longer refuses. Resolve the broadcasting seat from the
// seat env; when it is unresolvable the broadcast STILL proceeds carrying the honest `<unknown sender>`
@@ -117,6 +119,7 @@ selection, not the agent@rig@host sugar.`)
const warn = walkSizedWarning(resolved);
if (warn && !opts.json) console.log(`Advisory: ${warn}`);
}
if (refuseEmptyMessage(payload, "broadcast", Boolean(opts.json))) return;
const body: Record<string, unknown> = { text: payload, force: opts.force };
if (opts.rig) body.rig = opts.rig;
if (opts.pod) body.pod = opts.pod;
@@ -228,6 +231,12 @@ function renderBroadcastResults(data: Record<string, unknown>): void {
}
}
console.log(`${data["sent"]}/${data["total"]} delivered`);
// S2 (OPR.0.5.4.3): surface additive advisories (e.g. the unknown-sender
// sign-it notice) — an env-less operator must see it, not "sent" lines alone.
const advisory = data["warning"] as string | undefined;
if (advisory) {
console.log(`Advisory: ${advisory}`);
}
if ((data["failed"] as number) > 0 || results.some((r) => !r.ok)) {
process.exitCode = 1;
+12 -4
View File
@@ -62,8 +62,9 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
.option("--notes <text>", "Operator notes captured in bundle provenance metadata")
.option("--min-daemon-version <ver>", "Minimum daemon version required to install this bundle (Item 2 compatibility)")
.option("--min-cli-version <ver>", "Minimum CLI version required to install this bundle (Item 2 compatibility)")
.option("--allow-drift", "Bundle a spec that disagrees with the running rig of the same name; the divergence is stamped into bundle provenance")
.option("--json", "JSON output")
.action(async (spec: string, opts: { output: string; name: string; bundleVersion: string; includePackages?: string[]; rigRoot?: string; notes?: string; minDaemonVersion?: string; minCliVersion?: string; json?: boolean }) => {
.action(async (spec: string, opts: { output: string; name: string; bundleVersion: string; includePackages?: string[]; rigRoot?: string; notes?: string; minDaemonVersion?: string; minCliVersion?: string; allowDrift?: boolean; json?: boolean }) => {
const deps = getDepsF();
const client = await getClient(deps);
if (!client) { process.exitCode = 1; return; }
@@ -84,12 +85,15 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
// bundle sizes; if a bundle author hits this, the daemon-side
// operation is async-completable in a future /bundles/jobs/<id>
// surface (not in slice-05 scope).
// Paths are resolved here, against the operator's cwd: the daemon would otherwise resolve
// them against ITS cwd. Nothing is uploaded — the files must exist on the daemon's host.
const res = await client.post<Record<string, unknown>>("/api/bundles/create", {
specPath: spec, bundleName: opts.name, bundleVersion: opts.bundleVersion, outputPath: opts.output,
specPath: nodePath.resolve(spec), bundleName: opts.name, bundleVersion: opts.bundleVersion, outputPath: nodePath.resolve(opts.output),
includePackages: opts.includePackages,
rigRoot: opts.rigRoot ? nodePath.resolve(opts.rigRoot) : undefined,
provenance: buildClientProvenance(opts.notes),
...(hasCompatibility ? { compatibility } : {}),
...(opts.allowDrift ? { allowDrift: true } : {}),
}, { timeoutMs: 120_000 });
if (opts.json) {
@@ -101,6 +105,9 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
console.log(`Bundle created: ${opts.output}`);
console.log(` Name: ${res.data["bundleName"]} v${res.data["bundleVersion"]}`);
console.log(` Hash: ${res.data["archiveHash"]}`);
// The daemon has returned this on every drifted export since Build B and the human path
// dropped it — the operator saw a clean success while shipping a rig that does not exist.
if (typeof res.data["warning"] === "string") console.warn(`\n${res.data["warning"]}`);
});
// rig bundle inspect <path>
@@ -112,7 +119,7 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
const client = await getClient(deps);
if (!client) { process.exitCode = 1; return; }
const res = await client.post<Record<string, unknown>>("/api/bundles/inspect", { bundlePath });
const res = await client.post<Record<string, unknown>>("/api/bundles/inspect", { bundlePath: nodePath.resolve(bundlePath) });
// Check for structured failures (200 with error or failed integrity)
const hasError = typeof res.data["error"] === "string";
@@ -161,7 +168,8 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
// daemon completed the mutating install — operator-unsafe retry
// path (rig_name_collision on second attempt). Bumped to 120s.
const res = await client.post<Record<string, unknown>>("/api/bundles/install", {
bundlePath, plan: opts.plan ?? false, autoApprove: opts.yes ?? false, targetRoot: opts.target,
bundlePath: nodePath.resolve(bundlePath), plan: opts.plan ?? false, autoApprove: opts.yes ?? false,
targetRoot: opts.target ? nodePath.resolve(opts.target) : undefined,
// Item 2 / slice-05 Checkpoint 3.3: send CLI version + skip flag for the
// daemon-side install-time compatibility check. CLI version read at call
// time (no module-level constant) via the existing getCliVersion helper.
+2 -1
View File
@@ -40,7 +40,7 @@ Examples:
rig capture --rig my-rig
rig capture --pod dev --rig my-rig
rig capture --rig my-rig --json
rig capture --host vm-claude-test dev-impl@my-rig --lines 50
rig capture --host remote-dev dev-impl@my-rig --lines 50
Supported notes:
- Multi-target capture reports unsupported external_cli nodes as explicit per-target failures.
@@ -73,6 +73,7 @@ Supported notes:
}
session = targetResolution.target;
crossHostHint = targetResolution.hint;
if (targetResolution.warning) console.error(targetResolution.warning);
opts.host = explicitHost ?? targetResolution.sugarHost ?? opts.host;
}
@@ -1,605 +1,31 @@
// User Settings v0 — `rig config init-workspace` workspace scaffolding.
//
// Creates the mission-aware default workspace at ~/.openrig/workspace/ (or a
// caller-supplied --root). The same scaffold is also used idempotently by
// daemon startup so a fresh install has a browsable Project workspace before
// the user discovers the explicit command.
//
// Behavior:
// - Reads workspace.root setting (default ~/.openrig/workspace/).
// - Creates canonical subdirs: missions/ progress/ field-notes/ specs/ dogfood-evidence/.
// - Seeds two example missions with multiple slices.
// - Drops workspace README.md + STEERING.md.
// - --dry-run: report what would be created without acting.
// - --force: overwrite existing files (NOT directories — never deletes
// operator content).
// - Idempotent without --force: existing-dir + existing-readme is a no-op.
// Public adapter for the S01 project-workspace owner used by S05 instance
// initialization. The bytes and behavior live in @openrig/daemon.
import { Command } from "commander";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import {
ensureDefaultWorkspace,
workspaceScaffoldDirs,
workspaceScaffoldFiles,
type InitWorkspaceResult,
} from "@openrig/daemon/instance-initialization";
import { ConfigStore } from "../config-store.js";
import { renderMissionBriefTemplate, renderMissionNotesTemplate, renderSliceProofTemplate } from "../lib/scope/templates.js";
export { workspaceScaffoldDirs, workspaceScaffoldFiles };
export type { InitWorkspaceResult };
export interface InitWorkspaceOpts {
root?: string;
/** Deprecated compatibility input. Existing files are always preserved. */
force?: boolean;
dryRun?: boolean;
json?: boolean;
}
export interface InitWorkspaceResult {
root: string;
/** True when this run would (or did) create the root dir; false if root existed. */
rootCreated: boolean;
subdirs: Array<{ name: string; path: string; created: boolean }>;
files: Array<{ relPath: string; absPath: string; created: boolean; skipped: "exists" | null }>;
/** True when --dry-run; nothing was actually written. */
dryRun: boolean;
}
type DefaultSlice = {
id: string;
/** §1 dot-ID per conventions/scope-and-versioning (release-0.3.2
* slice 01 GA-coherence pass). Mirror of the daemon constant in
* packages/daemon/src/domain/workspace/default-workspace-scaffold.ts
* — keep in lockstep for the slice-21 parity test. */
dotId: string;
title: string;
status: "active" | "draft";
objective: string;
};
type DefaultMission = {
id: string;
/** §1 mission dot-ID — escape band for non-release missions. */
dotId: string;
title: string;
status: "active" | "draft";
objective: string;
slices: DefaultSlice[];
};
// Release-0.3.2 slice 01 (OPR.0.3.2.1) — seeded missions/slices now
// carry §1 dot-IDs in frontmatter so the scaffold coheres with
// `conventions/scope-and-versioning`. Mirror of the daemon constant;
// the slice-21 parity test asserts both sides emit identical bodies.
const DEFAULT_MISSIONS: DefaultMission[] = [
{
id: "getting-started",
dotId: "OPR.99.0.1",
title: "Getting Started",
status: "active",
objective: "Launch the conveyor starter and learn how OpenRig moves work through queue-backed slices.",
slices: [
{
id: "first-conveyor-run",
dotId: "OPR.99.0.1.1",
title: "First Conveyor Run",
status: "active",
objective: "Move one small packet through intake, planning, build, review, and close on the conveyor starter.",
},
{
id: "inspect-project-evidence",
dotId: "OPR.99.0.1.2",
title: "Inspect Project Evidence",
status: "draft",
objective: "Open Project, Queue, Story, and Tests to inspect the evidence created by the first conveyor run.",
},
],
},
];
const WORKSPACE_DIRS = [
"missions",
"artifacts",
"evidence",
"progress",
"field-notes",
"specs",
"dogfood-evidence",
...DEFAULT_MISSIONS.flatMap((mission) => [
`missions/${mission.id}`,
`missions/${mission.id}/slices`,
...mission.slices.flatMap((slice) => [
`missions/${mission.id}/slices/${slice.id}`,
`missions/${mission.id}/slices/${slice.id}/proof`,
]),
]),
] as const;
function subdirReadmeContent(subdir: string): string {
switch (subdir) {
case "missions":
return "# missions\n\nProject missions live here. Each mission folder maps to one Project mission in the UI and owns a `slices/` child folder.\n\nExpected shape:\n\n```text\nmissions/<mission-name>/README.md\nmissions/<mission-name>/PROGRESS.md\nmissions/<mission-name>/slices/<slice-name>/README.md\n```\n\nEvery mission and slice README carries a stable dot-ID in its frontmatter (`id: OPR.<ver>[.<n>]`) per the scope-and-versioning convention. Use `rig scope mission create <name>` and `rig scope slice create <mission> <slug>` to mint conformant artifacts; the CLI handles the dot-ID + auto-numbering for you. The folder name is the operator-facing slug; the `id:` is the stable handle that survives renames.\n";
case "artifacts":
return "# artifacts\n\nWork products live here: plans, drafts, generated outputs, and other files that a slice may reference before closure.\n";
case "evidence":
return "# evidence\n\nHuman-readable verification notes live here. Use this for compact proof summaries that are not tied to a screenshot or video packet.\n";
case "progress":
return "# progress\n\nPROGRESS.md tree. OpenRig's Progress browse view scans this directory recursively for PROGRESS.md files and renders them as a hierarchical tree.\n";
case "field-notes":
return "# field-notes\n\nOperator field notes. Free-form markdown notes from your daily work. OpenRig surfaces these alongside missions and slices for context.\n";
case "specs":
return "# specs\n\nWorkspace specs (rig specs / agent specs / workflow specs / context packs / skills). OpenRig's Library browses this directory alongside bundled specs.\n";
case "dogfood-evidence":
return "# dogfood-evidence\n\nProof packets live here. Each proof packet folder is matched to a Project slice by folder-name tokens and may contain markdown, screenshots, videos, traces, and other verification artifacts.\n";
default:
return `# ${subdir}\n`;
}
}
const WORKSPACE_README = `# OpenRig Workspace
This workspace is file-backed. The Project UI mirrors this structure:
- \`missions/<mission-id>\` becomes a Project mission.
- \`missions/<mission-id>/slices/<slice-id>\` becomes a Project slice.
- Queue items should mention or tag the mission id and slice id so Project can attach live work to the right slice.
- \`artifacts/\` is for work products that a slice needs to keep.
- \`evidence/\` is for proof notes and verification summaries.
- \`dogfood-evidence/<proof-packet-id>\` becomes Tests proof when the packet id contains the slice id tokens.
Use stable kebab-case names for mission and slice folders. Every mission/slice README also carries a stable dot-ID in its frontmatter (\`id: OPR.<ver>[.<n>]\`) per the \`scope-and-versioning\` convention; mint conformant artifacts via \`rig scope mission create <name>\` and \`rig scope slice create <mission> <slug>\`. Folder names are operator-facing slugs; the \`id:\` is the rename-proof handle.
`;
const STEERING_PLACEHOLDER = `---
title: Priority Stack
status: placeholder
---
# OpenRig Priority Stack
This file is a placeholder created by \`rig config init-workspace\`. Edit it
to record your top priorities. OpenRig's Steering surface reads this file
alongside your Project workspace.
## Top 3
1. Run the \`conveyor\` starter rig.
2. Move one packet through \`basic-loop\` or \`conveyor\`.
3. Inspect the Project, Queue, Story, and Tests surfaces.
## In Motion
(Active slices land here as you push them through the priority rail.)
## Loop State
(Health gates + loop diagnostics land here.)
`;
function missionReadme(mission: DefaultMission): string {
return `---
id: ${mission.dotId}
title: ${mission.title}
status: ${mission.status}
mission: ${mission.id}
---
# ${mission.title}
${mission.objective}
## Slices
${mission.slices.map((slice) => `- [${slice.title}](slices/${slice.id}/README.md)`).join("\n")}
`;
}
function missionProgress(mission: DefaultMission): string {
return `---
title: ${mission.title} Progress
status: ${mission.status}
mission: ${mission.id}
---
# ${mission.title} Progress
- [ ] Keep mission README current.
- [ ] Keep active slices queue-backed with mission and slice ids.
`;
}
// V0.3.1 slice 21 onboarding-conveyor — narrative content for the
// getting-started mission's two slices. PARITY CONTRACT with
// `packages/daemon/src/domain/workspace/getting-started-narrative.ts`:
// the daemon side is the canonical source; this CLI-side mirror MUST
// stay byte-identical. The narrative-parity test in
// `packages/daemon/test/getting-started-narrative-parity.test.ts`
// (added in this slice) asserts byte-identity by reading both this
// file and the daemon module — if either drifts, the test fails.
// If you update content here, update the daemon mirror in lockstep.
const FIRST_CONVEYOR_RUN_README = `# First Conveyor Run
Welcome to OpenRig. This is the first slice in your getting-started mission. It exists to teach you what a **conveyor run** is + how OpenRig moves work through a multi-agent topology.
## What a conveyor run is
A **conveyor** is OpenRig's central work-movement primitive. One conveyor run = one complete flow of work through your topology:
1. **Slice declared** — operator (you) or an agent declares a slice with intent + acceptance criteria
2. **Work routed** — orchestrator picks the slice up + routes it to the right agent
3. **Agents collaborate** — work moves agent-to-agent via durable handoffs (queue items)
4. **Evidence accumulates** — each step leaves a trail: commits, files, proof packets, screenshots
5. **Slice closes** — acceptance criteria met; orchestrator routes the result back
You're looking at a mocked conveyor run RIGHT NOW. The other tabs (Story / Progress / Artifacts / Tests / Queue / Topology) each show a different slice of this same run.
## Walking the tabs
- **Overview** (you're here) — read what a slice intends + how to use the rest of the tabs
- **Story** — narrative timeline of what happened, beat by beat
- **Progress** — acceptance checklist; what's done, what's pending
- **Artifacts** — files produced, commits, proof packets — the durable evidence trail
- **Tests** — pass/fail summaries + screenshots + verification proof
- **Queue** — operational qitems showing how work moved between agents
- **Topology** — the rig graph; which agents touched this slice + which edges fired
## Click through
Try them in order. Each tab teaches itself.
`;
const FIRST_CONVEYOR_RUN_TIMELINE = `# Story — First Conveyor Run
## 2026-04-15 09:00 — Slice declared
Operator request: "Build a CLI tool that lints todo lists." Slice declared in \`getting-started/slices/first-conveyor-run/\`. Acceptance: \`tdl lint <file>\` exits non-zero on malformed entries.
## 09:02 — Orchestrator routes
\`orch-lead@getting-started\` picks up the slice. Routes to \`driver@getting-started\` via \`rig queue handoff\`. Driver receives nudge; opens IMPL-PRD; reads acceptance.
## 09:05 — Driver picks up
Driver claims qitem. Reads slice scope. Surveys existing tdl repo (this is a worked example — imagine the repo is real). Plans: parse YAML; validate entries; exit code per finding.
## 09:30 — First commit
\`feat(lint): parse + validate todo entries\` — driver commits 4 files. Stream event emitted; visible in Artifacts tab.
## 10:15 — Driver hands to reviewer
Acceptance met locally. Driver hands off to \`reviewer@getting-started\`. Reviewer opens diff; reads tests.
## 10:25 — Reviewer flags concern
"Edge case: what about UTF-8 entries with combining characters?" Hands back to driver as \`concerning\`.
## 10:40 — Driver addresses
Driver fixes UTF-8 handling. Re-runs tests; all pass. Hands back.
## 11:00 — Reviewer accepts
\`accept\` decision; closes qitem with \`closure-reason: handed_off_to orch-lead\`. Conveyor proceeds.
## 11:05 — Slice ships
Orchestrator merges. Proof packet generated. Slice marked SHIPPED.
---
**What you just read**: one conveyor run from declaration to ship. ~2 hours wall-clock; 3 agents involved; 1 handoff with concern + remediation; 5 stream events; 1 proof packet.
The Story tab shows this kind of narrative for any real slice. Driver-authored \`timeline.md\` files live in each slice folder; this is yours to read + your operator's to update as work moves.
`;
const FIRST_CONVEYOR_RUN_PROGRESS = `---
title: First Conveyor Run Progress
status: active
mission: getting-started
rail-item: getting-started
slice: first-conveyor-run
---
# Progress — First Conveyor Run
## Acceptance criteria
- [x] Parse YAML todo entries
- [x] Validate per-entry shape (id, title, status, due-date)
- [x] Exit non-zero on malformed entries
- [x] Handle UTF-8 entries with combining characters
- [x] Tests pass (12/12)
- [x] Reviewer accepts
- [x] Slice merged
## Status: SHIPPED
This was a mocked conveyor run — no real code was produced. But the Progress tab works the same way for real work: as acceptance criteria are met, boxes get checked; the operator can scan at a glance whether the slice is on track.
## How the Progress tab works
The tab renders the slice's \`PROGRESS.md\` markdown file. Driver updates it as work progresses. Operator scans it during walks. Founder reviews at slice closure.
Try writing one for a real slice you start. It's just markdown; live updates appear here.
`;
const INSPECT_PROJECT_EVIDENCE_README = `# Inspect Project Evidence
The previous slice (First Conveyor Run) showed you what a conveyor run looks like as it happens. This slice teaches you how to inspect the evidence after a run completes.
## Why inspect evidence?
In OpenRig, agents work autonomously. The operator (you) doesn't watch every keystroke. Instead, the operator:
1. Declares slices with clear acceptance
2. Routes them to agents
3. Inspects evidence when slices close
The Artifacts + Tests + Queue + Topology tabs are your inspection surface. They show you what happened, with enough detail to:
- Verify the acceptance was actually met
- Spot subtle issues a passing test might miss
- Build trust in the agents over time
## Walking the tabs
- **Overview** — you're here; learn what to look for
- **Story** — read the narrative of what happened (high-level)
- **Progress** — acceptance status at a glance
- **Artifacts** — the durable evidence: files, commits, proof packets. **THIS IS THE TAB YOU LEARN MOST FROM.**
- **Tests** — pass/fail + screenshots + verification proof
- **Queue** — who handed what to whom (audit trail of decisions)
- **Topology** — which agents touched this; which edges fired
## Key inspection skills
When walking a finished slice:
1. **Read the Story** for context — what was the operator trying to do?
2. **Check Progress** — did acceptance actually pass? Any TODO boxes?
3. **Open Artifacts → Files** — sample-read 2-3 files; does the code match what the Story claimed?
4. **Open Tests → Proof packets** — do screenshots actually show what was claimed?
5. **Read Queue** — were there concerning decisions? Hand-offs to escalation?
6. **Skim Topology** — were unexpected agents involved?
Trust comes from repeated successful inspections, not from a single proof packet.
`;
const INSPECT_PROJECT_EVIDENCE_TIMELINE = `# Story — Inspect Project Evidence
This slice mirrors the previous one (First Conveyor Run). The same agents shipped the same work. But this view is from the inspector's angle, not the runner's.
## 11:10 — Operator opens project
Operator (you, in the real world) opens \`/project/slice/first-conveyor-run\` after the slice ships. First read: the Progress tab. All boxes checked.
## 11:12 — Read Story
Operator reads the Story narrative. Notices the reviewer's UTF-8 concern + driver's remediation. Confidence-building: there was friction, friction was caught, friction was addressed.
## 11:15 — Open Artifacts
Operator clicks Artifacts. Sees 4 commits, 5 files, 1 proof packet, 2 screenshots. Opens commit \`feat(lint): parse + validate todo entries\`. Reads the diff.
## 11:18 — Read tests
Operator opens Tests tab. 12/12 passing. Opens the proof packet — screenshot shows CLI output \`Error: malformed entry at line 3\`. Visually verifies the acceptance was met.
## 11:22 — Skim Queue
Operator scrolls Queue. 4 qitems in chronological order. Sees the \`concerning\` decision + subsequent \`accept\`. Confidence reinforced.
## 11:25 — Mark complete
Operator marks slice acceptance complete. Closes the inspection.
---
**What you just read**: a clean inspection in ~15 minutes. Operator builds trust through the evidence trail. This is the OpenRig usage pattern at scale: declare → route → inspect.
`;
const INSPECT_PROJECT_EVIDENCE_PROGRESS = `---
title: Inspect Project Evidence Progress
status: active
mission: getting-started
rail-item: getting-started
slice: inspect-project-evidence
---
# Progress — Inspect Project Evidence
## Inspection checklist
- [x] Read Story (context: what was being done)
- [x] Check Progress (acceptance status)
- [x] Sample-read Artifacts files
- [x] Verify Tests proof packets visually
- [x] Skim Queue for concerning decisions
- [x] Skim Topology for unexpected agents
## Status: COMPLETE
This was a mocked inspection — a worked example of the inspection workflow.
## How to apply this
For any real slice that ships in YOUR project: open the slice page, walk the tabs in this order, check each box mentally. Over time you'll develop instincts for what passes the smell test vs what needs deeper investigation.
`;
const GETTING_STARTED_NARRATIVE: Record<
string,
{ readme: string; timeline: string; progress: string }
> = {
"first-conveyor-run": {
readme: FIRST_CONVEYOR_RUN_README,
timeline: FIRST_CONVEYOR_RUN_TIMELINE,
progress: FIRST_CONVEYOR_RUN_PROGRESS,
},
"inspect-project-evidence": {
readme: INSPECT_PROJECT_EVIDENCE_README,
timeline: INSPECT_PROJECT_EVIDENCE_TIMELINE,
progress: INSPECT_PROJECT_EVIDENCE_PROGRESS,
},
};
function sliceReadme(mission: DefaultMission, slice: DefaultSlice): string {
// V0.3.1 slice 21: getting-started slices ship rich narrative.
const narrative = GETTING_STARTED_NARRATIVE[slice.id];
if (narrative) {
return `---
id: ${slice.dotId}
title: ${slice.title}
status: ${slice.status}
mission: ${mission.id}
rail-item: ${mission.id}
slice: ${slice.id}
---
${narrative.readme}`;
}
return `---
id: ${slice.dotId}
title: ${slice.title}
status: ${slice.status}
mission: ${mission.id}
rail-item: ${mission.id}
slice: ${slice.id}
---
# ${slice.title}
## Intent
${slice.objective}
## Mini-requirements
1. The work is visible in the Project slice and its queue items link back to \`${slice.id}\`.
## Proof contract
- [ ] The work is visible in the Project slice — captured (see IMPLEMENTATION-PRD.md).
## Queue Mapping
Queue items for this slice should mention or tag:
- mission: \`${mission.id}\`
- slice: \`${slice.id}\`
This lets Project attach queue activity to the slice story, queue, tests, and topology tabs.
`;
}
function sliceTimeline(mission: DefaultMission, slice: DefaultSlice): string | null {
const narrative = GETTING_STARTED_NARRATIVE[slice.id];
return narrative ? narrative.timeline : null;
}
function sliceProgress(mission: DefaultMission, slice: DefaultSlice): string {
const narrative = GETTING_STARTED_NARRATIVE[slice.id];
if (narrative) return narrative.progress;
return `---
title: ${slice.title} Progress
status: ${slice.status}
mission: ${mission.id}
rail-item: ${mission.id}
slice: ${slice.id}
---
# ${slice.title} Progress
- [ ] Define the next concrete packet.
- [ ] Attach queue work to this slice id.
- [ ] Capture proof or notes before closing.
`;
}
function slicePrd(mission: DefaultMission, slice: DefaultSlice): string {
return `---
title: ${slice.title} Implementation Notes
status: ${slice.status}
mission: ${mission.id}
rail-item: ${mission.id}
slice: ${slice.id}
---
# ${slice.title} Implementation Notes
## Intent
${slice.objective}
## Mini-requirements
1. The work is visible in the Project slice and its queue items link back to \`${slice.id}\`.
## Proof contract
- [ ] The work is visible in the Project slice — captured.
- [ ] Queue items include enough body or tag context to link back to \`${slice.id}\`.
- [ ] Proof artifacts are referenced from the slice before closure (drop via \`rig proof add\`).
> Conventions SSOT: \`docs/reference/sdlc-conventions.md (installed: $OPENRIG_HOME/reference/sdlc-conventions.md)\` — section names, proof-contract format, the two locks, C1 headers. For a small slice the mini-requirements may BE the whole PRD.
`;
}
export function workspaceScaffoldDirs(): string[] {
return [...WORKSPACE_DIRS];
}
export function workspaceScaffoldFiles(): Array<{ relPath: string; content: string }> {
const files: Array<{ relPath: string; content: string }> = [
{ relPath: "README.md", content: WORKSPACE_README },
{ relPath: "STEERING.md", content: STEERING_PLACEHOLDER },
{ relPath: "missions/README.md", content: subdirReadmeContent("missions") },
{ relPath: "artifacts/README.md", content: subdirReadmeContent("artifacts") },
{ relPath: "evidence/README.md", content: subdirReadmeContent("evidence") },
{ relPath: "progress/README.md", content: subdirReadmeContent("progress") },
{ relPath: "field-notes/README.md", content: subdirReadmeContent("field-notes") },
{ relPath: "specs/README.md", content: subdirReadmeContent("specs") },
{ relPath: "dogfood-evidence/README.md", content: subdirReadmeContent("dogfood-evidence") },
];
for (const mission of DEFAULT_MISSIONS) {
files.push(
{ relPath: `missions/${mission.id}/README.md`, content: missionReadme(mission) },
{ relPath: `missions/${mission.id}/PROGRESS.md`, content: missionProgress(mission) },
{ relPath: `missions/${mission.id}/MISSION_BRIEF.md`, content: renderMissionBriefTemplate(mission.title) },
// FR-5e A1 — reuse the FR-3 MISSION_NOTES scaffold helper so a
// fresh `rig config init-workspace` produces a getting-started
// mission that doctor check #7 (mission_notes_presence) marks
// ok. Same env-var-pivot (OPENRIG_MISSION_NOTES_TEMPLATE_PATH >
// built-in bundled template) FR-3's `rig scope mission create`
// honors.
{
relPath: `missions/${mission.id}/MISSION_NOTES.md`,
content: renderMissionNotesTemplate({
mission_id: mission.dotId,
mission_name: mission.title,
created_date: new Date().toISOString().slice(0, 10),
}).rendered,
},
);
for (const slice of mission.slices) {
files.push(
{ relPath: `missions/${mission.id}/slices/${slice.id}/README.md`, content: sliceReadme(mission, slice) },
{ relPath: `missions/${mission.id}/slices/${slice.id}/PROGRESS.md`, content: sliceProgress(mission, slice) },
{ relPath: `missions/${mission.id}/slices/${slice.id}/PROOF.md`, content: renderSliceProofTemplate({ id: slice.dotId, title: slice.title }) },
{ relPath: `missions/${mission.id}/slices/${slice.id}/IMPLEMENTATION-PRD.md`, content: slicePrd(mission, slice) },
);
// V0.3.1 slice 21: getting-started slices ship timeline.md.
const timeline = sliceTimeline(mission, slice);
if (timeline) {
files.push({
relPath: `missions/${mission.id}/slices/${slice.id}/timeline.md`,
content: timeline,
});
}
}
}
return files;
}
export function initWorkspaceCommand(configPath?: string): Command {
const cmd = new Command("init-workspace")
.description("Scaffold the default workspace at ~/.openrig/workspace/ with mission/slice folders")
.description("Scaffold a repo-ready project workspace")
.option("--root <path>", "Override workspace root (default: workspace.root setting)")
.option("--force", "Overwrite existing scaffolded files (does NOT remove directories)")
.option("--force", "Deprecated compatibility flag; existing files are still preserved")
.option("--dry-run", "Show what would be created without writing")
.option("--json", "JSON output")
.action((opts: InitWorkspaceOpts) => {
@@ -614,8 +40,8 @@ export function initWorkspaceCommand(configPath?: string): Command {
for (const sub of result.subdirs) {
console.log(` ${sub.created ? "+" : " "} ${sub.name}/`);
}
for (const f of result.files) {
console.log(` ${f.created ? "+" : " "} ${f.relPath}${f.skipped ? ` (skipped: ${f.skipped})` : ""}`);
for (const file of result.files) {
console.log(` ${file.created ? "+" : " "} ${file.relPath}${file.skipped ? ` (skipped: ${file.skipped})` : ""}`);
}
if (result.dryRun) console.log("(dry-run; no files written.)");
}
@@ -630,48 +56,12 @@ export function initWorkspaceCommand(configPath?: string): Command {
export function runInitWorkspace(opts: InitWorkspaceOpts & { configPath?: string }): InitWorkspaceResult {
const store = new ConfigStore(opts.configPath);
const root = opts.root ?? (store.get("workspace.root") as string);
const dryRun = !!opts.dryRun;
const force = !!opts.force;
// FR-5e BLOCKER-1 — precompute the full scaffold file list BEFORE
// any filesystem mutation. workspaceScaffoldFiles() now invokes
// renderMissionNotesTemplate(), which throws on invalid
// OPENRIG_MISSION_NOTES_TEMPLATE_PATH. If we mkdir first then
// throw on render, the operator is left with a half-created
// workspace that the next `rig workspace doctor` will misread as
// partially-initialized. Verify-first-then-write (banked from
// FR-3 self-lesson) — fail before any side effect.
const scaffoldFiles = workspaceScaffoldFiles();
const scaffoldDirs = workspaceScaffoldDirs();
const rootExists = existsSync(root);
const result: InitWorkspaceResult = {
root,
rootCreated: !rootExists,
subdirs: [],
files: [],
dryRun,
};
if (!rootExists && !dryRun) mkdirSync(root, { recursive: true });
for (const sub of scaffoldDirs) {
const subPath = join(root, sub);
const subExists = existsSync(subPath);
if (!subExists && !dryRun) mkdirSync(subPath, { recursive: true });
result.subdirs.push({ name: sub, path: subPath, created: !subExists });
const result = ensureDefaultWorkspace({ root, dryRun: !!opts.dryRun });
if (!result.ok) {
const detail = result.conflicts
.map((conflict) => `${conflict.path}: expected ${conflict.expected}, found ${conflict.actual}`)
.join("; ");
throw new Error(`Workspace initialization blocked: ${detail}`);
}
for (const file of scaffoldFiles) {
const absPath = join(root, file.relPath);
const exists = existsSync(absPath);
if (exists && !force) {
result.files.push({ relPath: file.relPath, absPath, created: false, skipped: "exists" });
} else {
if (!dryRun) writeFileSync(absPath, file.content, "utf-8");
result.files.push({ relPath: file.relPath, absPath, created: true, skipped: null });
}
}
return result;
}
+20 -3
View File
@@ -54,16 +54,29 @@ Examples:
rig config set workspace.slices_root /path # configure a workspace path
rig config reset # delete config file, revert all to defaults
rig config reset workspace.slices_root # clear one key, revert to default
rig config init-workspace # scaffold ~/.openrig/workspace/ with missions + slices
rig config init-workspace # scaffold a repo-ready project workspace
Keys:
daemon.* port, host
db.path
transcripts.* enabled, path, lines, poll_interval_seconds
workspace.* root, slices_root, steering_path, field_notes_root,
specs_root, dogfood_evidence_root, operator_seat_name
(lines/poll_interval set scrollback CAPTURE depth + cadence.
Thin CLAUDE transcripts usually are NOT these — they mean the
seat's fullscreen renderer, whose alternate screen emits no
scrollback. OpenRig launches Claude with
CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 by default; set
OPENRIG_CLAUDE_DISABLE_ALTERNATE_SCREEN=0 to opt back into fullscreen.)
workspace.* root, slices_root, steering_path, specs_root,
projects_root, catalog_path, operator_seat_name
topology.root the topology tree root (instance altitude at its top; default $OPENRIG_HOME/topology)
context.root addressable context library for 'rig context add' (default $OPENRIG_HOME/context)
context.system_world default System World, replacement manifest path, or disabled
skills.root versioned skill catalog (default $OPENRIG_HOME/skills)
onboarding.default_pack.enabled deliver the two-part fresh-seat mental-model pack (default on)
health.context_pressure.* context-pressure warning/critical percentages (defaults 95/99)
files.allowlist name:/abs/path,name:/abs/path
progress.scan_roots name:/abs/path,name:/abs/path
ui.timezone TUI IANA timezone (default America/Los_Angeles; reopen TUI after changing)
ui.preview.* refresh_interval_seconds, max_pins, default_lines
recovery.* auto_drive_provider_prompts, provider_auth_env_allowlist
agents.* advisor_session, operator_session
@@ -76,6 +89,10 @@ Keys:
policies.idle_gate_qitem.scan_interval_seconds
policies.idle_gate_qitem.active_wake_interval_seconds
snapshots.periodic.* enabled, interval_seconds, retention_keep
queue.* pickup_stall_threshold_minutes (S04 pickup-receipt stall threshold),
stuck_sweep_interval_seconds, stuck_sweep_unclaimed_age_minutes (S02 standing stuck sweep),
wake_retry_interval_seconds, wake_retry_cap, wake_unconfirmed_window_minutes,
wake_swap_grace_seconds (S01 wake-or-escalate ladder)
retention.* enabled, transitions_days, watchdog_days,
watchdog_keep_per_job, batch_size
terminal.status_bar show the inner tmux status bar on launch (default off)
+592 -148
View File
@@ -7,7 +7,7 @@
//
// Each delegates to /api/context-packs/library/* against the daemon.
// The `add` verb installs a pack from a directory at
// ~/.openrig/context-packs/<name>/ — host-symlink-free contract,
// $OPENRIG_HOME/context/<name>/ — host-symlink-free contract,
// matches `rig specs add` shape (regular files only; no symlinks).
import { Command } from "commander";
@@ -16,23 +16,41 @@ import {
existsSync,
lstatSync,
mkdirSync,
readdirSync,
mkdtempSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { basename, extname, isAbsolute, join, resolve } from "node:path";
import { dirname } from "node:path";
import { basename, join, resolve } from "node:path";
import { parse as parseYaml } from "yaml";
import { getDefaultOpenRigPath } from "../openrig-compat.js";
import { assertSafeInstallRef, assertTreeHasNoSymlinks, assertDestinationNamespaceContained, validateContextPackManifestForInstall } from "../lib/context-install.js";
import { addGitContext, inspectGitContext, updateGitContext } from "../lib/context-git.js";
import { ConfigStore } from "../config-store.js";
import { DaemonClient } from "../client.js";
import { enumArg } from "../cli-error.js";
import { getDaemonStatus, getDaemonUrl , statusGuardMessage} from "../daemon-lifecycle.js";
import { resolveWorkPosition, type WorkInstallPlan } from "../lib/work-install.js";
import {
reconcileSkillLoadout,
resolveSkillLoadout,
type ReconcileSkillLoadoutResult,
type SkillLoadout,
} from "@openrig/daemon/skill-loadout";
import { realDeps } from "./daemon.js";
import type { StatusDeps } from "./status.js";
const contextRuntimeArg = enumArg(["claude-code", "claude", "codex"]);
interface ContextPackEntryWire {
id: string;
kind: "context-pack";
name: string;
version: string;
purpose: string | null;
/** OPR.0.5.6.10 — pack-level classification from the daemon's ATOM_TAXONOMIES enum. */
taxonomy: string;
sourceType: "builtin" | "user_file" | "workspace";
sourcePath: string;
relativePath: string;
@@ -49,6 +67,26 @@ interface ContextPackEntryWire {
}>;
}
function selectedIds(ids: string[], none: string): string {
return ids.length > 0 ? ids.join(", ") : none;
}
function printWorkInstallSelectors(result: WorkInstallPlan, topologySkills: string[]): void {
const world = result.systemWorld;
const identity = world.id ? ` ${world.id}@${world.version}` : "";
const path = world.manifestPath ? ` ${world.manifestPath}` : "";
console.log(`system ${world.state} [${world.source}]${identity}${path}`);
for (const selection of world.context) {
const profiles = selection.profiles
? ` (${Object.entries(selection.profiles).map(([runtime, profile]) => `${runtime}=${profile}`).join(", ")})`
: "";
console.log(`context system ${selection.ref}${profiles}`);
}
console.log(`skills system=${selectedIds(world.skills, "(none)")}`);
console.log(`skills topology=${selectedIds(topologySkills, "(none)")}`);
console.log(`skills project=${selectedIds(result.skills, "(none)")}`);
}
interface PreviewWire {
id: string;
name: string;
@@ -60,122 +98,97 @@ interface PreviewWire {
missingFiles: Array<{ path: string; role: string }>;
}
// Slice-03 Atom 2 — mirror the daemon's per-segment ref contract at the
// local install boundary. This must run before creating the context store.
const SAFE_REF_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
// Slice-03 lineage repair (R2 HIGH-2): the install boundary mirrors the daemon
// bounded, delimiter-free version token (ref-safety.SAFE_VERSION) so an unsafe
// version is rejected BEFORE any local write — matching the per-segment ref
// mirror above.
const SAFE_INSTALL_VERSION = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,31}$/;
function assertSafeInstallRef(ref: string): void {
const safe =
ref.length > 0 &&
ref.split("/").every(
(segment) => segment.length > 0 && segment !== "." && segment !== ".." && SAFE_REF_SEGMENT.test(segment),
);
if (!safe) {
throw new Error(
`unsafe install ref '${ref}' — a ref must be one or more '/'-separated segments, each matching ` +
`[A-Za-z0-9][A-Za-z0-9._-]{0,63} (no '.'/'..', no absolute path, no empty segment, no ` +
`whitespace or injection), so packs stay inside the context store root.`,
);
function assertSafeTopologySegment(kind: "rig" | "seat", value: string): void {
if (value === "." || value === ".." || !SAFE_REF_SEGMENT.test(value)) {
throw new Error(`unsafe ${kind} segment '${value}' — topology addresses require one bounded path segment`);
}
}
function assertTreeHasNoSymlinks(root: string): void {
const stack = [root];
while (stack.length > 0) {
const current = stack.pop()!;
for (const entry of readdirSync(current, { withFileTypes: true })) {
const absPath = join(current, entry.name);
if (entry.isSymbolicLink()) {
throw new Error(`Context pack directories must not contain symlinks: ${absPath}`);
}
if (entry.isDirectory()) stack.push(absPath);
}
function assertLocalGitClient(client: DaemonClient): void {
if (!["127.0.0.1", "localhost", "[::1]"].includes(new URL(client.baseUrl).hostname)) {
throw new Error("Run Git source selection/inspection/update on the daemon host through its loopback URL; these commands use local Git and filesystem paths.");
}
}
// Slice-03 lineage repair (R2 HIGH-1): a lexically-safe path-like install ref
// can still escape the store if one of its parent namespace segments is a
// symlink (or a non-directory). Walk every ANCESTOR segment under the store root
// and reject before the copy — the FS-canonical containment the lexical ref
// check alone cannot give, mirroring the daemon compose namespace walk
// (context-pack-library-service.ts). A not-yet-created segment (ENOENT) is safe:
// cpSync will materialize it as a real directory.
function assertDestinationNamespaceContained(targetRoot: string, installName: string): void {
const segments = installName.split("/");
let cursor = targetRoot;
for (const segment of segments.slice(0, -1)) {
cursor = join(cursor, segment);
let stat: ReturnType<typeof lstatSync>;
try {
stat = lstatSync(cursor);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") break;
throw err;
}
if (stat.isSymbolicLink() || !stat.isDirectory()) {
throw new Error(
`unsafe install ref '${installName}' — its namespace segment '${cursor}' is a symlink or non-directory, ` +
`so the copy would escape the context store root. Remove it or install under a different --name.`,
);
}
}
function isHttpUrl(source: string): boolean {
return /^https?:\/\//i.test(source);
}
const ALLOWED_CONTEXT_PACK_SUFFIXES = new Set([".md", ".markdown", ".yaml", ".yml", ".txt"]);
function validateContextPackManifestForInstall(manifestPath: string): void {
let parsed: unknown;
async function fetchTextOrThrow(url: string, what: string): Promise<{ text: string; finalUrl: string }> {
let res: Response;
try {
parsed = parseYaml(readFileSync(manifestPath, "utf-8"));
res = await fetch(url);
} catch (err) {
throw new Error(`manifest at ${manifestPath} is not valid YAML: ${(err as Error).message}`);
throw new Error(`Could not reach ${what} at ${url}: ${(err as Error).message}`);
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`manifest at ${manifestPath} must be a YAML object at the root`);
}
const obj = parsed as Record<string, unknown>;
if (typeof obj["name"] !== "string" || obj["name"].length === 0) {
throw new Error(`manifest at ${manifestPath} is missing required field 'name' (string)`);
}
if (obj["version"] === undefined || obj["version"] === null) {
throw new Error(`manifest at ${manifestPath} is missing required field 'version'`);
}
const versionStr = String(obj["version"]);
if (!SAFE_INSTALL_VERSION.test(versionStr)) {
throw new Error(
`manifest at ${manifestPath} has an invalid version '${versionStr}' — a version must be a single bounded ` +
`token [A-Za-z0-9][A-Za-z0-9._+-]{0,31} (no ':' or separator, no whitespace, ≤32 chars).`,
);
}
const files = obj["files"];
if (!Array.isArray(files)) {
throw new Error(`manifest at ${manifestPath} must declare 'files: [...]'`);
}
for (let i = 0; i < files.length; i++) {
const entry = files[i];
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
throw new Error(`manifest at ${manifestPath} has malformed entry at files[${i}]`);
if (!res.ok) throw new Error(`Could not fetch ${what} at ${url}: HTTP ${res.status} ${res.statusText}`.trim());
// res.url is the FINAL url after any redirects — declared files must resolve
// relative to it, not the caller's original spelling (r2 MEDIUM-1).
return { text: await res.text(), finalUrl: res.url || url };
}
// OPR.0.5.3.7 R4 — install a context pack from a URL. <url> points at the pack's
// manifest.yaml (a trailing '/' is treated as '<url>manifest.yaml'); every
// files[].path is fetched relative to that manifest. ATOMIC BY CONSTRUCTION:
// everything stages into a temp SIBLING of the target and is published by one
// renameSync, so a malformed manifest, an unreachable URL, or a missing declared
// file leaves NO partial pack behind. Deliberately dumb: no registry, no cache.
async function installPackFromUrl(
url: string,
overrideName: string | undefined,
targetRoot: string,
): Promise<{ targetDir: string; installName: string }> {
const manifestUrl = url.endsWith("/") ? `${url}manifest.yaml` : url;
mkdirSync(targetRoot, { recursive: true });
const staging = mkdtempSync(join(targetRoot, ".tmp-add-"));
try {
// Fetch + validate the manifest before touching the target namespace.
const { text: manifestText, finalUrl: finalManifestUrl } = await fetchTextOrThrow(manifestUrl, "manifest");
writeFileSync(join(staging, "manifest.yaml"), manifestText);
validateContextPackManifestForInstall(join(staging, "manifest.yaml"));
const manifest = parseYaml(manifestText) as { name: string; files: Array<{ path: string }> };
const installName = overrideName ?? manifest.name;
assertSafeInstallRef(installName);
assertDestinationNamespaceContained(targetRoot, installName);
const targetDir = join(targetRoot, installName);
if (existsSync(targetDir)) {
throw new Error(`A context pack named '${installName}' already exists at ${targetDir}. Remove it first or use --name to install under a different name.`);
}
const file = entry as Record<string, unknown>;
const relPath = file["path"];
if (typeof relPath !== "string" || relPath.length === 0) {
throw new Error(`manifest at ${manifestPath} files[${i}] missing 'path' (string)`);
}
if (relPath.includes("..") || isAbsolute(relPath) || relPath.startsWith("\\")) {
throw new Error(`manifest at ${manifestPath} files[${i}].path '${relPath}' must be a relative path inside the pack (no '..' segments, no leading '/')`);
}
if (!ALLOWED_CONTEXT_PACK_SUFFIXES.has(extname(relPath))) {
throw new Error(`manifest at ${manifestPath} files[${i}].path '${relPath}' has an unsupported suffix; allowed: ${Array.from(ALLOWED_CONTEXT_PACK_SUFFIXES).join(", ")}`);
}
if (typeof file["role"] !== "string" || file["role"].length === 0) {
throw new Error(`manifest at ${manifestPath} files[${i}] missing 'role' (string)`);
// Fetch every declared file relative to the manifest's FINAL url (after
// redirects), via the platform URL resolver — never the caller's original
// spelling (r2 MEDIUM-1: a redirected manifest must not resolve files against
// the stale request base).
//
// BOUNDARY (r2 HIGH-1): new URL() also honors an ABSOLUTE f.path, and the
// manifest validator accepts a URL-shaped value as a filesystem-relative
// path. Require every resolved file URL to stay under the manifest's own
// directory (same origin + path prefix) so a stranger-supplied manifest can
// never make add fetch cross-origin or climb out of its pack. The trailing
// slash on the base defeats prefix-sibling ('/pack' vs '/pack-evil') tricks.
const manifestDirUrl = new URL("./", finalManifestUrl).href;
for (const f of manifest.files) {
const fileUrl = new URL(f.path, finalManifestUrl).href;
if (!fileUrl.startsWith(manifestDirUrl)) {
throw new Error(
`manifest file '${f.path}' resolves to ${fileUrl}, outside the pack directory ${manifestDirUrl}. ` +
`Declared files must be relative to the manifest (no absolute URLs, no escaping the pack).`,
);
}
const { text: fileText } = await fetchTextOrThrow(fileUrl, `file '${f.path}'`);
const dest = join(staging, f.path);
mkdirSync(dirname(dest), { recursive: true });
writeFileSync(dest, fileText);
}
// Re-validate the on-disk staged pack, then publish atomically.
validateContextPackManifestForInstall(join(staging, "manifest.yaml"));
assertTreeHasNoSymlinks(staging);
renameSync(staging, targetDir);
return { targetDir, installName };
} catch (err) {
rmSync(staging, { recursive: true, force: true });
throw err;
}
}
@@ -222,6 +235,10 @@ Examples:
rig context add ./my-pack
rig context rm packs/compaction-restore
rig context sync
rig context profile world-public --situation fresh --runtime claude-code
rig context work-install --runtime claude-code
rig context trace --rig product-team --seat orch1-lead --name LEARNED.md
rig context trace --rig product-team --pod delivery --seat dev1-qa --name LEARNED.md
`);
const getDeps = (): StatusDeps => depsOverride ?? {
@@ -229,6 +246,120 @@ Examples:
clientFactory: (url: string) => new DaemonClient(url),
};
cmd.command("work-install")
.description("Resolve System World plus project work context and the managed skill loadout")
.option("--project <id>", "Exact project id from workspace.yaml")
.option("--mission <id>", "Exact mission id under the selected project")
.option("--slice <id>", "Exact slice id under the selected mission")
.option("--deliver", "Include the exact content of each extant planned file")
.option("--runtime <runtime>", "Inspect skills for claude-code (alias: claude) or codex", contextRuntimeArg)
.option("--cwd <path>", "Agent working directory that receives skill projections (default: current directory)")
.option("--topology <ids>", "Comma-separated topology/profile skill identities")
.option("--apply-skills", "Reconcile selected skills into the runtime harness directory")
.option("--json", "JSON output")
.action((opts: { project?: string; mission?: string; slice?: string; deliver?: boolean; runtime?: string; cwd?: string; topology?: string; applySkills?: boolean; json?: boolean }) => {
if (opts.applySkills && opts.runtime === undefined) {
console.error("invalid_runtime: --apply-skills requires --runtime claude-code (alias: claude) or codex");
process.exitCode = 1;
return;
}
const store = new ConfigStore();
const workspaceRoot = String(store.resolveWithSource("workspace.root").value);
const catalogPath = String(store.resolveWithSource("workspace.catalog_path").value);
const contextRoot = String(store.resolveWithSource("context.root").value);
const systemWorldSetting = store.resolveWithSource("context.system_world");
const result = resolveWorkPosition({
workspaceRoot,
catalogPath,
contextRoot,
systemWorldSelection: String(systemWorldSetting.value),
systemWorldSource: systemWorldSetting.source,
...(opts.project !== undefined ? { project: opts.project } : {}),
...(opts.mission !== undefined ? { mission: opts.mission } : {}),
...(opts.slice !== undefined ? { slice: opts.slice } : {}),
});
if ("error" in result) {
if (opts.json) console.log(JSON.stringify({ ok: false, ...result }));
else console.error(`${result.error.code}: ${result.error.message}`);
process.exitCode = 1;
return;
}
let skillLoadout: SkillLoadout | undefined;
let skillProjection: ReconcileSkillLoadoutResult | undefined;
if (opts.runtime) {
const topologySkills = (opts.topology ?? "").split(",").map((id) => id.trim()).filter(Boolean);
const resolvedSkills = resolveSkillLoadout({
catalogRoot: String(store.resolveWithSource("skills.root").value),
systemSkills: result.systemWorld.skills,
topologySkills,
projectRoot: result.position.projectRoot,
projectSkills: result.skills,
});
if (!resolvedSkills.ok) {
if (opts.json) console.log(JSON.stringify({ ok: false, errors: resolvedSkills.errors }, null, 2));
else for (const error of resolvedSkills.errors) console.error(`${error.code}: ${error.message}`);
process.exitCode = 1;
return;
}
skillLoadout = resolvedSkills.loadout;
skillProjection = reconcileSkillLoadout({
loadout: skillLoadout,
runtime: opts.runtime === "codex" ? "codex" : "claude-code",
cwd: resolve(opts.cwd ?? process.cwd()),
apply: opts.applySkills === true,
});
if (!skillProjection.ok) process.exitCode = 1;
}
if (opts.json) {
const output = opts.deliver
? {
...result,
pieces: result.pieces.map((piece) => piece.exists
? { ...piece, content: readFileSync(piece.path, "utf8") }
: piece),
...(skillLoadout ? { skillLoadout, skillProjection } : {}),
}
: { ...result, ...(skillLoadout ? { skillLoadout, skillProjection } : {}) };
console.log(JSON.stringify(output, null, 2));
return;
}
if (opts.deliver) {
for (const planned of result.pieces) {
if (!planned.exists) {
console.log(`=== ${planned.address} (absent: ${planned.path}) ===`);
continue;
}
console.log(`=== ${planned.altitude} ${planned.address} ===`);
console.log(readFileSync(planned.path, "utf8"));
}
printWorkInstallSelectors(result, (opts.topology ?? "").split(",").map((id) => id.trim()).filter(Boolean));
if (skillProjection) {
for (const receipt of skillProjection.receipts) {
console.log(`${receipt.status.padEnd(7)} ${receipt.id} [${receipt.selectedBy.join("+")}] ${receipt.target}`);
}
for (const id of skillProjection.removed) console.log(`removed ${id}`);
if (skillProjection.freshLaunchRequired) console.log("skills fresh seat process required to observe changed ambient skills");
if (!opts.applySkills) console.log("skills read-only; add --apply-skills to reconcile");
}
for (const warning of result.warnings) console.error(`Warning: ${warning}`);
return;
}
console.log(`project ${result.position.projectId ?? "(unmanifested)"}: ${result.position.projectRoot}`);
printWorkInstallSelectors(result, (opts.topology ?? "").split(",").map((id) => id.trim()).filter(Boolean));
for (const planned of result.pieces) {
console.log(`${planned.altitude.padEnd(7)} ${planned.address} [${planned.source}] ${planned.exists ? planned.path : `(absent: ${planned.path})`}`);
}
if (skillProjection) {
for (const receipt of skillProjection.receipts) {
console.log(`${receipt.status.padEnd(7)} ${receipt.id} [${receipt.selectedBy.join("+")}] ${receipt.target}`);
}
for (const id of skillProjection.removed) console.log(`removed ${id}`);
if (skillProjection.freshLaunchRequired) console.log("skills fresh seat process required to observe changed ambient skills");
if (!opts.applySkills) console.log("skills read-only; add --apply-skills to reconcile");
}
for (const warning of result.warnings) console.error(`Warning: ${warning}`);
});
async function getClient(): Promise<DaemonClient> {
const deps = getDeps();
const status = await getDaemonStatus(deps.lifecycleDeps);
@@ -239,6 +370,57 @@ Examples:
return deps.clientFactory(getDaemonUrl(status));
}
// OPR.0.5.3.6 — the productized chain-file trace. Daemon-independent by
// design: the walk is a config read + filesystem reads, so it works on a
// box whose daemon is down (orientation is exactly when that happens).
cmd.command("trace")
.description("Walk the topology tree for one chain filename (instance -> rig -> optional pod -> optional seat), keyed off topology.root")
.requiredOption("--rig <rig>", "Rig name (the rigs/<rig> altitude)")
.option("--pod <pod>", "Pod id (the pods/<pod> altitude); omit when no pod context is selected")
.option("--seat <seat>", "Seat id (the seats/<seat> altitude); omit for a rig-level trace")
.requiredOption("--name <file>", "Chain filename, identical at every altitude (e.g. LEARNED.md, CULTURE.md)")
.option("--json", "JSON output for agents")
.action(async (opts: { rig: string; pod?: string; seat?: string; name: string; json?: boolean }) => {
const { ConfigStore } = await import("../config-store.js");
const { traceTopologyChain } = await import("../lib/topology-trace.js");
const store = new ConfigStore();
const resolved = store.resolveWithSource("topology.root");
let result;
try {
result = traceTopologyChain({
topologyRoot: String(resolved.value),
name: opts.name,
rig: opts.rig,
pod: opts.pod ?? null,
seat: opts.seat ?? null,
});
} catch (err) {
// r2-B3: traversal-shaped input is a clean refusal, never a stack trace.
console.error(err instanceof Error ? err.message : String(err));
process.exitCode = 1;
return;
}
// Advisories go to stderr on BOTH output modes — a legacy read must
// never pass silently, and stdout stays clean for piping.
for (const level of result.levels) {
if (level.advisory) console.error(`ADVISORY ${level.advisory}`);
}
if (opts.json) {
console.log(JSON.stringify({ topologyRootSource: resolved.source, ...result }, null, 2));
return;
}
console.log(`chain "${result.name}" under topology.root=${result.topologyRoot} (source: ${resolved.source})`);
for (const level of result.levels) {
if (level.source === "absent") {
console.log(`\n== ${level.altitude} — absent (${level.path})`);
continue;
}
const origin = level.source === "legacy" ? ` [LEGACY: ${level.resolvedPath}]` : "";
console.log(`\n== ${level.altitude} — ${level.path}${origin}`);
console.log(level.content?.trimEnd() ?? "");
}
});
cmd.command("compose")
.description("Compose ordered files into a durable context-pack ref (never delivers)")
.requiredOption("--out <ref>", "Path-like durable output ref")
@@ -290,11 +472,11 @@ Examples:
return;
}
if (entries.length === 0) {
console.log("No context packs in library. Author one at ~/.openrig/context-packs/<name>/ then run: rig context sync");
console.log("No context packs in library. Author one under `rig config get context.root`, then run: rig context sync");
return;
}
for (const e of entries) {
console.log(`${e.relativePath.padEnd(36)} ${e.name.padEnd(24)} v${String(e.version).padEnd(6)} ${String(e.files.length).padStart(2)} files ~${String(e.derivedEstimatedTokens).padStart(6)} tokens ${e.sourceType} ${e.sourcePath}`);
console.log(`${e.relativePath.padEnd(36)} ${e.name.padEnd(24)} v${String(e.version).padEnd(6)} ${(e.taxonomy ?? "—").padEnd(8)} ${String(e.files.length).padStart(2)} files ~${String(e.derivedEstimatedTokens).padStart(6)} tokens ${e.sourceType} ${e.sourcePath}`);
}
} catch (err) {
console.error((err as Error).message);
@@ -368,6 +550,214 @@ Examples:
}
});
// OPR.0.5.3.7 R1 — the PULL verb: an agent-facing serving verb over the EXISTING assembler
// path (the same by-ref/preview machinery `preview` uses — never a parallel assembler).
// `preview` is the operator's pre-send check; `get` is what a seat runs on demand to LOAD a
// library entry. Output is the assembled bundle itself (so the agent consumes exactly those
// bytes), warnings to stderr; `--json` for programmatic use. Naming ruled: `rig context get`
// (one library, one verb — NOT `rig skills get`; "skills" is an org category in the library).
cmd.command("get")
.argument("<name-or-ref>", "Context library entry name, path-like ref, or address (<pack-ref>/<file>#H2-slug/H3-slug)")
.description("Serve the assembled bundle for an agent to load on demand (the pull verb)")
.option("--json", "JSON output")
.action(async (nameOrRef: string, opts: { json?: boolean }) => {
try {
const client = await getClient();
// OPR.0.5.3.5 Atom 4c: fragment addresses route directly to the
// daemon's resolver. A bare slash-containing value may be either a
// pack ref or a whole-file address, so exact-pack lookup wins and only
// its 404 falls through to the same resolver.
if (nameOrRef.includes("#")) {
const res = await client.get<{ text?: string; message?: string; error?: string }>(
`/api/context-packs/library/resolve-address?address=${encodeURIComponent(nameOrRef)}`,
);
if (res.status !== 200) {
throw new Error(res.data?.message ?? res.data?.error ?? `Daemon returned HTTP ${res.status} for resolve-address`);
}
if (opts.json) console.log(JSON.stringify(res.data, null, 2));
else console.log(res.data.text);
return;
}
let entry: ContextPackEntryWire;
if (nameOrRef.includes("/")) {
const exact = await client.get<ContextPackEntryWire & { error?: string; message?: string }>(
`/api/context-packs/library/by-ref?ref=${encodeURIComponent(nameOrRef)}`,
);
if (exact.status === 404) {
const res = await client.get<{ text?: string; message?: string; error?: string }>(
`/api/context-packs/library/resolve-address?address=${encodeURIComponent(nameOrRef)}`,
);
if (res.status !== 200) {
throw new Error(res.data?.message ?? res.data?.error ?? `Daemon returned HTTP ${res.status} for resolve-address`);
}
if (opts.json) console.log(JSON.stringify(res.data, null, 2));
else process.stdout.write(res.data.text ?? "");
return;
}
if (exact.status === 400) throw new Error(exact.data?.message ?? `Unsafe context pack ref '${nameOrRef}'.`);
if (exact.status !== 200) throw new Error(`Daemon returned HTTP ${exact.status} for /api/context-packs/library/by-ref`);
entry = exact.data;
} else {
entry = await resolvePack(client, nameOrRef);
}
const res = await client.get<PreviewWire>(`/api/context-packs/library/by-ref/preview?ref=${encodeURIComponent(entry.relativePath)}`);
if (res.status !== 200) throw new Error(`Daemon returned HTTP ${res.status}`);
const bundle = res.data;
if (opts.json) {
console.log(JSON.stringify(bundle, null, 2));
return;
}
// Warnings go to stderr so stdout is exactly the served bundle bytes.
if (bundle.missingFiles.length > 0) {
console.error(`Warning: ${bundle.missingFiles.length} file(s) referenced by manifest are missing on disk.`);
for (const m of bundle.missingFiles) console.error(` - ${m.path} (role: ${m.role})`);
}
console.log(bundle.bundleText);
} catch (err) {
console.error((err as Error).message);
process.exitCode = 1;
}
});
// OPR.0.5.3.5 Atom 4d — situation-composed delivery (the profile verb).
// Serving only: pieces to stdout with their source labels (Q2-Amendment 1),
// budget report + provenance warnings to stderr. Delivery-free like every
// library verb — nothing here sends to a seat. Naming rig/seat (or mission)
// is the caller's explicit grant of read access to that directory subtree.
cmd.command("profile")
.argument("<name-or-ref>", "Context pack name or path-like ref (its manifest must declare atoms)")
.requiredOption("--situation <situation>", "fresh | handover | post-compaction")
// r1 4d obs 2: default from the seat's own environment — a codex seat that
// forgets the flag must not silently get a claude profile (mini-req 3 is
// the rule that the runtimes compose DIFFERENT profiles). Flag beats env;
// an unrecognized env value falls back to claude rather than erroring a
// surface the env owner may not control.
.option("--runtime <runtime>", "claude-code (alias: claude) or codex (default: $OPENRIG_RUNTIME, else claude-code)", contextRuntimeArg)
.option("--profile <profile>", "Named install profile declared by the pack (selection + ordered phases)")
.option("--budget <tokens>", "Situation token budget — overage is REPORTED, never truncated")
.option("--rig <rig>", "With --seat: grant read access to that seat's tree (seat: atoms)")
.option("--seat <seat>", "With --rig: the seat whose tree seat: atoms may read")
.option("--mission <mission>", "Grant read access to that mission's tree (mission: atoms)")
.option("--slice <slice>", "With --mission: compose the legacy default project/mission/slice SPEC walk")
.option("--json", "JSON output (the full composed profile)")
.action(async (nameOrRef: string, opts: { situation: string; runtime?: string; profile?: string; budget?: string; rig?: string; seat?: string; mission?: string; slice?: string; json?: boolean }) => {
try {
const client = await getClient();
const entry = await resolvePack(client, nameOrRef);
// r1 F2: the product's runtime vocabulary is "claude-code" / "codex"
// (the adapters' values, live on real seats) — map it EXPLICITLY. A
// genuinely unknown value falls back to claude WITH A VOICE: a future
// third runtime must not silently get a claude profile (the exact
// mini-req 3 hazard this default exists to close).
const envRuntime = process.env["OPENRIG_RUNTIME"];
let runtime = opts.runtime;
if (runtime === undefined) {
if (envRuntime === "codex") runtime = "codex";
else if (envRuntime === "claude-code" || envRuntime === "claude") runtime = "claude";
else {
if (envRuntime) console.error(`Warning: unrecognized OPENRIG_RUNTIME '${envRuntime}' — composing the claude profile; pass --runtime to override.`);
runtime = "claude";
}
}
// Keep the composer/manifest key and returned metadata compatible.
if (runtime === "claude-code") runtime = "claude";
const params = new URLSearchParams({ ref: entry.relativePath, situation: opts.situation, runtime });
if (opts.profile !== undefined) params.set("profile", opts.profile);
if (opts.budget !== undefined) params.set("budget", opts.budget);
if (opts.rig !== undefined) params.set("rig", opts.rig);
if (opts.seat !== undefined) params.set("seat", opts.seat);
if (opts.mission !== undefined) params.set("mission", opts.mission);
if (opts.slice !== undefined) params.set("slice", opts.slice);
const res = await client.get<{
profileId?: string;
phases?: Array<{ id: string; kind: string; sources?: string[]; estimatedTokens: number }>;
pieces?: Array<{ atomId: string; address: string; sourceKind: string; text: string; estimatedTokens: number }>;
totalEstimatedTokens?: number;
budget?: { limitTokens: number; overageTokens: number; dropCandidates: Array<{ atomId: string; priority: string; estimatedTokens: number }> };
provenanceWarnings?: string[];
message?: string;
error?: string;
}>(`/api/context-packs/library/by-ref/profile?${params.toString()}`);
if (res.status !== 200) {
throw new Error(res.data?.message ?? res.data?.error ?? `Daemon returned HTTP ${res.status} for by-ref/profile`);
}
const profile = res.data;
if (opts.json) {
console.log(JSON.stringify(profile, null, 2));
return;
}
if (profile.profileId) {
console.error(`PROFILE ${profile.profileId}`);
for (const phase of profile.phases ?? []) {
const sources = phase.sources ? ` [${phase.sources.join(", ")}]` : "";
console.error(`PHASE ${phase.id} (${phase.kind}${sources}, ~${phase.estimatedTokens} tokens)`);
}
}
// Warnings and the budget report ride stderr so stdout is exactly the
// composed walk an agent consumes.
for (const w of profile.provenanceWarnings ?? []) console.error(`PROVENANCE ${w}`);
if (profile.budget) {
console.error(
`BUDGET: over by ~${profile.budget.overageTokens} tokens (limit ${profile.budget.limitTokens}); ` +
`drop candidates in order: ${profile.budget.dropCandidates.map((d) => `${d.atomId} (${d.priority}, ~${d.estimatedTokens})`).join(", ")}`,
);
}
for (const p of profile.pieces ?? []) {
// r1 4d obs 1: the escape marker rides the FRAMING header, so an
// agent that discards stderr still learns a piece's bytes came from
// outside its root — self-describing payload, zero composed bytes
// touched.
const escaped = (p as { provenance?: { escapesRoot?: boolean } }).provenance?.escapesRoot ? " !ESCAPED-ROOT" : "";
console.log(`=== ${p.atomId} [${p.sourceKind}${escaped}] ${p.address} (~${p.estimatedTokens} tokens)`);
console.log(p.text);
console.log("");
}
} catch (err) {
console.error((err as Error).message);
process.exitCode = 1;
}
});
// OPR.0.5.3.5 mini-req 7 — the outgoing occupant's boundary write (the Q2
// requirement the store alone does not satisfy). Daemon-independent like
// trace: the seat dir resolves from topology.root CONFIG; the write flows
// through the ONE store (supersession + addressability gate); advisory
// contract findings ride stderr while the write still lands — the boundary
// is never blocked on prose shape.
cmd.command("recap-write")
.description("Write this seat's authored RECAP (decisions-with-rationale) at the handover boundary; supersedes the previous recap into the seat's chain")
.requiredOption("--rig <rig>", "Rig name (the rigs/<rig> altitude)")
.requiredOption("--seat <seat>", "Seat id (the seats/<seat> altitude)")
.requiredOption("--file <path>", "Markdown file containing the recap content")
.action(async (opts: { rig: string; seat: string; file: string }) => {
try {
const { writeSeatRecap, validateRecapContract, listRecapChain } = await import("@openrig/daemon/seat-recap-store");
const content = readFileSync(opts.file, "utf-8");
const store = new ConfigStore();
const topologyRoot = String(store.resolveWithSource("topology.root").value);
assertSafeTopologySegment("rig", opts.rig);
assertSafeTopologySegment("seat", opts.seat);
const rigDir = join(topologyRoot, "rigs", opts.rig);
if (!existsSync(rigDir)) {
throw new Error(`rig directory ${rigDir} does not exist — check --rig against the topology tree (topology.root=${topologyRoot}).`);
}
const seatDir = join(topologyRoot, "rigs", opts.rig, "seats", opts.seat);
assertDestinationNamespaceContained(join(topologyRoot, "rigs"), `${opts.rig}/seats/${opts.seat}/RECAP.md`);
mkdirSync(seatDir, { recursive: true });
for (const f of validateRecapContract(content)) {
console.error(f.kind === "no-decisions-section"
? "ADVISORY no-decisions-section: the authoring contract asks for decisions WITH rationale — conclusions alone are the lossy handoff shape."
: `ADVISORY nonstandard-unverified-marker (line ${f.line}): use the canonical 'UNVERIFIED:' form so uncertain facts stay findable.`);
}
writeSeatRecap({ seatDir, content });
const chain = listRecapChain(seatDir);
console.log(`Recap written: ${join(seatDir, "RECAP.md")}${chain.length > 0 ? ` (${chain.length} superseded predecessor${chain.length === 1 ? "" : "s"} retained)` : ""}`);
} catch (err) {
console.error((err as Error).message);
process.exitCode = 1;
}
});
cmd.command("sync")
.description("Re-walk discovery roots and refresh the library index")
.option("--json", "JSON output")
@@ -395,50 +785,65 @@ Examples:
});
cmd.command("add")
.argument("<source-dir>", "Directory containing manifest.yaml + included files")
.description("Install a context pack from a local directory into ~/.openrig/context-packs/")
.option("--name <name>", "Override the install name (defaults to source directory basename)")
.argument("<source>", "Pack directory/manifest URL, or Git repository path/URL with --git")
.description("Install a pack; --git discovers a pack in a Git repository and retains its update relationship")
.option("--name <name>", "Override the install name (defaults to the manifest name / source basename)")
.option("--git", "Clone a Git repository path/URL with existing Git credentials; select a pack snapshot")
.option("--checkout", "With --git, select an existing checkout instead of cloning; updates may merge in it")
.option("--pack <path>", "With --git, select a repository-relative pack; default discovers manifest.yaml or .openrig/context-packs")
.option("--json", "JSON output")
.action(async (sourceDir: string, opts: { name?: string; json?: boolean }) => {
.action(async (source: string, opts: { name?: string; json?: boolean; git?: boolean; checkout?: boolean; pack?: string }) => {
try {
if (!existsSync(sourceDir)) throw new Error(`Source directory not found: ${sourceDir}`);
const stat = lstatSync(sourceDir);
if (stat.isSymbolicLink()) throw new Error(`Source must not be a symlink: ${sourceDir}`);
if (!stat.isDirectory()) throw new Error(`Source must be a directory containing manifest.yaml: ${sourceDir}`);
const manifestPath = join(sourceDir, "manifest.yaml");
if (!existsSync(manifestPath)) {
throw new Error(`Source directory must contain manifest.yaml: ${sourceDir}`);
}
validateContextPackManifestForInstall(manifestPath);
// Read the manifest's name as the canonical install name when
// --name not given. Cheap parse: trust the daemon to validate
// on next sync; here we just need the name.
const installName = opts.name ?? (() => {
try {
const raw = readFileSync(manifestPath, "utf-8");
const m = raw.match(/^name:\s*['"]?([^'"\n]+)['"]?\s*$/m);
return m?.[1]?.trim() || basename(sourceDir);
} catch {
return basename(sourceDir);
// OPR.0.5.9.5 Wave B — config-resolved context library,
// never a hardcoded ~/.openrig literal; the daemon resolves the same key.
const targetRoot = new ConfigStore().resolve().context.root;
let targetDir: string;
let gitSelection: ReturnType<typeof addGitContext>["selected"] | undefined;
if ((opts.pack || opts.checkout) && !opts.git) throw new Error("--pack and --checkout require --git.");
if (opts.git) {
const gitClient = await getClient();
assertLocalGitClient(gitClient);
({ installedAt: targetDir, selected: gitSelection } = addGitContext(source, opts, targetRoot));
} else if (isHttpUrl(source)) {
// R4 — URL install: fetch → validate → atomic stage+rename (no partial pack).
({ targetDir } = await installPackFromUrl(source, opts.name, targetRoot));
} else {
// Local directory install.
if (!existsSync(source)) throw new Error(`Source directory not found: ${source}`);
const stat = lstatSync(source);
if (stat.isSymbolicLink()) throw new Error(`Source must not be a symlink: ${source}`);
if (!stat.isDirectory()) throw new Error(`Source must be a directory containing manifest.yaml: ${source}`);
const manifestPath = join(source, "manifest.yaml");
if (!existsSync(manifestPath)) {
throw new Error(`Source directory must contain manifest.yaml: ${source}`);
}
})();
assertSafeInstallRef(installName);
assertTreeHasNoSymlinks(sourceDir);
const targetRoot = getDefaultOpenRigPath("context-packs");
mkdirSync(targetRoot, { recursive: true });
assertDestinationNamespaceContained(targetRoot, installName);
const targetDir = join(targetRoot, installName);
let targetExists = false;
try {
lstatSync(targetDir);
targetExists = true;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
validateContextPackManifestForInstall(manifestPath);
const installName = opts.name ?? (() => {
try {
const raw = readFileSync(manifestPath, "utf-8");
const m = raw.match(/^name:\s*['"]?([^'"\n]+)['"]?\s*$/m);
return m?.[1]?.trim() || basename(source);
} catch {
return basename(source);
}
})();
assertSafeInstallRef(installName);
assertTreeHasNoSymlinks(source);
mkdirSync(targetRoot, { recursive: true });
assertDestinationNamespaceContained(targetRoot, installName);
targetDir = join(targetRoot, installName);
let targetExists = false;
try {
lstatSync(targetDir);
targetExists = true;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
}
if (targetExists) {
throw new Error(`A context pack named '${installName}' already exists at ${targetDir}. Remove it first or use --name to install under a different name.`);
}
cpSync(source, targetDir, { recursive: true });
}
if (targetExists) {
throw new Error(`A context pack named '${installName}' already exists at ${targetDir}. Remove it first or use --name to install under a different name.`);
}
cpSync(sourceDir, targetDir, { recursive: true });
// Sync the daemon library so the new pack appears immediately.
const client = await getClient();
const syncRes = await client.post<{ count: number; errors?: Array<{ source: string; error: string }>; entries: ContextPackEntryWire[] }>("/api/context-packs/library/sync");
@@ -452,10 +857,14 @@ Examples:
if (syncError) {
throw new Error(`Installed at ${targetDir}, but daemon rejected the pack during sync: ${syncError.error}`);
}
if (gitSelection && !syncRes.data.entries.some((entry) => resolve(entry.sourcePath) === resolve(targetDir))) {
throw new Error(`Git selection retained at ${targetDir}, but this daemon does not serve it. Check context.root and workspace ref precedence before using it.`);
}
if (opts.json) {
console.log(JSON.stringify({ installedAt: targetDir, count: syncRes.data.count }, null, 2));
console.log(JSON.stringify({ installedAt: targetDir, count: syncRes.data.count, ...(gitSelection ? { gitSource: gitSelection } : {}) }, null, 2));
} else {
console.log(`Installed at ${targetDir}. Library now has ${syncRes.data.count} context pack(s).`);
if (gitSelection) console.log(`Git ${gitSelection.revision} from ${gitSelection.checkout}; inspect/update with rig context source.`);
}
} catch (err) {
console.error((err as Error).message);
@@ -463,6 +872,41 @@ Examples:
}
});
const source = cmd.command("source")
.description("Inspect Git checkout vs served context, or explicitly fetch/merge and select an update")
.addHelpText("after", "\nStart: rig context add <repository-path-or-URL> --git [--pack path]\nEdit/commit in the reported checkout with ordinary Git. Updates never push or reset.\nConflicts retain the old selection; resolve/commit or abort in the checkout before retrying.\nSelection does not prove agent consumption; use context get and check the actual consumer.\n");
for (const operation of ["inspect", "update"] as const) {
source.command(operation)
.argument("<ref>", "Selected Git-backed context pack ref")
.description(operation === "inspect"
? "Read local revision, edits, conflicts and selection; no fetch or consumption claim"
: "Explicitly fetch/merge the upstream, then select the clean pack; refuse local selection edits")
.option("--json", "JSON output")
.action(async (ref: string, opts: { json?: boolean }) => {
try {
const client = await getClient();
assertLocalGitClient(client);
const entry = await resolvePack(client, ref);
const localRoot = new ConfigStore().resolve().context.root;
if (resolve(entry.sourcePath) !== resolve(localRoot, entry.relativePath)) throw new Error("This pack is not in the locally configured context library. Run Git source commands on its owning instance.");
if (entry.sourceType === "builtin") throw new Error("Builtin context is not a writable Git selection.");
const result = operation === "inspect" ? inspectGitContext(entry.sourcePath) : updateGitContext(entry.sourcePath);
if (operation === "update") {
const sync = await client.post<{ errors?: Array<{ source: string; error: string }>; entries: ContextPackEntryWire[] }>("/api/context-packs/library/sync");
if (sync.status !== 200) throw new Error(`Selection updated, but library sync failed (HTTP ${sync.status}); run rig context sync.`);
if (!sync.data.entries.some((candidate) => resolve(candidate.sourcePath) === resolve(entry.sourcePath))) throw new Error(`Selection retained, but the daemon cannot serve it: ${sync.data.errors?.map((error) => error.error).join("; ") || "check context root and ref precedence"}`);
}
// Structured output keeps checkout/selected bytes/consumption distinct
// in both terminal and machine use, without a second status renderer.
console.log(JSON.stringify(result, null, 2));
} catch (err) {
const message = (err as Error).message;
console.error(opts.json ? JSON.stringify({ error: message }) : message);
process.exitCode = 1;
}
});
}
cmd.command("rm")
.argument("<ref>", "Path-like ref of the context pack to remove (e.g. packs/compaction-restore)")
.description("Remove a context pack from the library by its path-like ref")
+18 -8
View File
@@ -1,7 +1,8 @@
import { Command } from "commander";
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { copyFileSync, lstatSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { ConfigStore } from "../config-store.js";
// `rig crash-cart --json` — the daemon-DOWN recovery verdict emit (plan c015d9ed §C3, coupling ruling
// option A). Prints ONE JSON = the 3-state detector verdict + (on DOWN) the discovery — READ-ONLY, a
@@ -9,8 +10,9 @@ import { join } from "node:path";
// (never exit-code-only). The `emit` is injected: the real wiring imports emitCrashCartState from the
// scoped @openrig/daemon/crash-cart subpath (reuse VERBATIM) — set up alongside this command.
//
// NOTE: this CLI-local CrashCartEmit is the documented parse/print contract; it matches the daemon's
// emit shape (the JSON contract). When the subpath export lands, the real emit is wired as the default.
// SCOPE FENCE (plan R10 / Boundaries): "no new CLI verb v1; bare `rig` is the cart." There are NO
// `restore-fleet` / `cancel-fleet` public verbs — the TUI drives the conductor through the daemon client
// directly against the R2-sanctioned daemon-side batch route. This command emits the read-only verdict only.
export type DaemonState = "up" | "down" | "unverified";
@@ -36,18 +38,24 @@ function openrigHome(): string {
* the detector's readDaemonJson omits it). */
function readDaemonJsonWithDb(home: string): { pid: number; port: number; host?: string; db: string } | undefined {
const p = join(home, "daemon.json");
if (!existsSync(p)) return undefined;
if (!existsOrThrow(p)) return undefined;
try {
const j = JSON.parse(readFileSync(p, "utf8")) as { pid?: unknown; port?: unknown; host?: unknown; db?: unknown };
if (typeof j.pid === "number" && typeof j.port === "number" && typeof j.db === "string") {
return { pid: j.pid, port: j.port, host: typeof j.host === "string" ? j.host : undefined, db: j.db };
}
return undefined;
} catch {
return undefined;
throw new Error("daemon.json has no usable PID, port and database path");
} catch (error) {
throw new Error(`Cannot read recorded daemon state at ${p}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/** Only ENOENT means absent; permissions and dangling links must fail visibly. */
function existsOrThrow(path: string): boolean {
try { lstatSync(path); return true; }
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return false; throw error; }
}
/**
* The real verdict: LAZY-imports the narrow @openrig/daemon/crash-cart subpath at invocation (dep
* rail 2 — rig startup + other verbs never load the daemon module) and composes the shipped detector
@@ -90,7 +98,8 @@ async function realEmit(): Promise<CrashCartEmit> {
isProcessAlive: cc.isProcessAlive,
probeHealthz: (url) => probeClassified(url).then((r) => r === "answered"),
copyFile: copyFileSync,
exists: existsSync,
exists: existsOrThrow,
configuredDbPath: new ConfigStore().resolve().db.path,
makeScratchDir: () => mkdtempSync(join(tmpdir(), "crash-cart-")),
removeScratchDir: (d) => rmSync(d, { recursive: true, force: true }),
openDb: cc.openDaemonDbReadonly,
@@ -115,5 +124,6 @@ export function crashCartCommand(deps?: Partial<CrashCartCommandDeps>): Command
// A non-zero exit is a HINT only (not-cleanly-up); the JSON remains the contract.
process.exitCode = result.state === "up" ? 0 : 1;
});
return cmd;
}
+108
View File
@@ -0,0 +1,108 @@
import { resolve as resolvePath } from "node:path";
import { stringify as stringifyYaml } from "yaml";
import { Command } from "commander";
import { DaemonClient } from "../client.js";
import { getDaemonStatus, getDaemonUrl, daemonStatusGuard } from "../daemon-lifecycle.js";
import { realDeps } from "./daemon.js";
import type { StatusDeps } from "./status.js";
import { resolveDefaultAgentRef } from "./topology-default-agent.js";
interface CreateResult {
rigId: string;
specName: string;
specVersion: string;
nodes: Array<{
logicalId: string;
status: "launched" | "failed" | "attention_required";
sessionName?: string;
error?: string;
}>;
errors?: string[];
message?: string;
error?: string;
}
export function createCommand(depsOverride?: StatusDeps): Command {
const cmd = new Command("create").description("Create a one-seat rig without writing a spec");
const getDeps = () => depsOverride ?? {
lifecycleDeps: realDeps(),
clientFactory: (url: string) => new DaemonClient(url),
};
cmd
.argument("<name>", "Name of the new rig")
.option("--runtime <runtime>", "Agent runtime", "claude-code")
.option("--cwd <path>", "Working directory for the seat", process.cwd())
.option("--json", "JSON output for agents")
.action(async (name: string, opts: { runtime: string; cwd: string; json?: boolean }) => {
const deps = getDeps();
const status = await getDaemonStatus(deps.lifecycleDeps);
if (!daemonStatusGuard(status)) return;
const client = deps.clientFactory(getDaemonUrl(status));
let agentRef: string;
try {
agentRef = await resolveDefaultAgentRef(client);
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exitCode = 1;
return;
}
const cwd = resolvePath(opts.cwd);
const logicalId = "main.lead";
const yaml = stringifyYaml({
version: "0.2",
name,
pods: [{
id: "main",
label: "Main",
members: [{
id: "lead",
agent_ref: agentRef,
runtime: opts.runtime,
profile: "default",
cwd,
}],
edges: [],
}],
edges: [],
});
const res = await client.postText<CreateResult>(
"/api/rigs/import",
yaml,
"text/yaml",
{ "X-Rig-Root": cwd, "X-Cwd-Override": cwd },
{ timeoutMs: 120_000 },
);
const node = res.data.nodes?.find((candidate) => candidate.logicalId === logicalId);
const ok = res.status < 400 && node?.status === "launched";
if (opts.json) {
console.log(JSON.stringify({
ok,
rigId: res.data.rigId,
rigName: name,
pod: "main",
seat: logicalId,
source: agentRef,
node,
}, null, 2));
} else if (ok) {
console.log(`Created rig ${name} (${res.data.rigId})`);
console.log(` Seat: ${logicalId}${node?.sessionName ? ` (${node.sessionName})` : ""}`);
} else {
const detail = res.data.errors?.join("; ")
?? res.data.message
?? res.data.error
?? node?.error
?? `Create failed (HTTP ${res.status})`;
console.error(detail);
}
if (!ok) process.exitCode = 1;
});
return cmd;
}
+65 -15
View File
@@ -1,5 +1,7 @@
import { Command } from "commander";
import fs from "node:fs";
import { randomUUID } from "node:crypto";
import { acquireDaemonStartLock } from "../daemon-start-lock.js";
import { execFileSync, spawn } from "node:child_process";
import { fetchWithTimeout } from "../fetch-with-timeout.js";
import {
@@ -10,6 +12,8 @@ import {
tailLogs,
type LifecycleDeps,
OPENRIG_DIR,
STATE_FILE,
resolveBindIntent,
} from "../daemon-lifecycle.js";
interface ProcessAliveDeps {
@@ -28,6 +32,22 @@ export function createIsProcessAlive(deps: ProcessAliveDeps): (pid: number) => b
};
}
type ExecFile = (file: string, args: string[], options: { encoding: "utf-8" }) => string;
// Windows has no ps(1) and no zombie state, so the signal probe alone decides liveness there.
export function readProcessState(
pid: number,
platform: NodeJS.Platform = process.platform,
run: ExecFile = execFileSync,
): string | null {
if (platform === "win32") return "R";
try {
return run("ps", ["-o", "state=", "-p", String(pid)], { encoding: "utf-8" });
} catch {
return null;
}
}
export function realDeps(): LifecycleDeps {
const isProcessAlive = createIsProcessAlive({
signalCheck: (pid) => {
@@ -38,16 +58,11 @@ export function realDeps(): LifecycleDeps {
return false;
}
},
readProcessState: (pid) => {
try {
return execFileSync("ps", ["-o", "state=", "-p", String(pid)], { encoding: "utf-8" });
} catch {
return null;
}
},
readProcessState: (pid) => readProcessState(pid),
});
return {
acquireStartLock: () => acquireDaemonStartLock(OPENRIG_DIR),
spawn: (cmd, args, opts) => spawn(cmd, args, opts as Parameters<typeof spawn>[2]),
fetch: async (url) => {
const res = await fetchWithTimeout(globalThis.fetch, url, {}, {
@@ -60,11 +75,31 @@ export function realDeps(): LifecycleDeps {
},
kill: (pid, signal) => { process.kill(pid, signal as NodeJS.Signals); return true; },
readFile: (p) => { try { return fs.readFileSync(p, "utf-8"); } catch { return null; } },
writeFile: (p, content) => fs.writeFileSync(p, content, "utf-8"),
writeFile: (p, content) => {
if (p !== STATE_FILE) { fs.writeFileSync(p, content, "utf-8"); return; }
const temporary = `${p}.${randomUUID()}.tmp`;
try {
fs.writeFileSync(temporary, content, { encoding: "utf-8", flag: "wx" });
fs.renameSync(temporary, p);
} finally {
try { fs.unlinkSync(temporary); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
}
},
removeFile: (p) => { try { fs.unlinkSync(p); } catch { /* ignore */ } },
exists: (p) => fs.existsSync(p),
mkdirp: (p) => fs.mkdirSync(p, { recursive: true }),
pathKind: (p) => {
try {
const value = fs.lstatSync(p);
if (value.isDirectory()) return "directory";
if (value.isFile()) return "file";
return "other";
} catch {
return "missing";
}
},
openForAppend: (p) => fs.openSync(p, "a"),
closeFile: (fd) => fs.closeSync(fd),
isProcessAlive,
// RULING 1ae863d2 — sibling-home scan (home-resolution honesty).
listDir: (p) => { try { return fs.readdirSync(p); } catch { return []; } },
@@ -78,6 +113,7 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
cmd
.command("start")
.description("Start the daemon")
.addHelpText("after", "\nStartup reserves this local instance before initialization and verifies the spawned child PID on every required listener.\nMissing/mismatched identity or child exit fails startup; failed publication withdraws only this launch's matching state. Use a matching CLI/daemon installation.\nA concurrent start fails without spawning another child. Inspect daemon-start.lock for launcher/child PIDs after an interrupted start;\nonly archive an abandoned reservation after proving both processes absent. A failed cleanup retains it and reports the child PID.\n")
.option("--port <port>", "Port to listen on")
.option("--host <host>", "Host to bind on")
.option("--db <path>", "Database path")
@@ -108,9 +144,19 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
// multi-bind (loopback + tailscale auto-detect) when the operator
// never opted in to a specific host.
const hostResolution = configStore.resolveWithSource("daemon.host");
const hostUserExplicit = opts.host !== undefined || hostResolution.source !== "default";
const effectiveHost = opts.host ?? config.daemon.host;
const hostForDaemon = hostUserExplicit ? effectiveHost : undefined;
// S20 — bind intent comes ONLY from the dedicated surfaces: the --host flag,
// a FILE-sourced daemon.host, or OPENRIG_BIND_HOST. An env-sourced daemon.host
// is the overloaded routing channel (ENV_MAP maps it from OPENRIG_HOST — the
// exact injected state a managed environment carries) and never creates intent.
const intent = resolveBindIntent({
flagHost: opts.host,
envBindHost: process.env["OPENRIG_BIND_HOST"],
configSource: hostResolution.source,
configHost: config.daemon.host,
});
const hostUserExplicit = intent.explicit;
const effectiveHost = intent.host ?? "127.0.0.1";
const hostForDaemon = intent.host;
// Run preflight before starting
const preflight = new SystemPreflight({
@@ -140,6 +186,9 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
transcriptsEnabled: config.transcripts.enabled,
transcriptsPath: config.transcripts.path,
workspaceRoot: config.workspace.root,
contextRoot: config.context.root,
skillsRoot: config.skills.root,
topologyRoot: config.topology.root,
// V1 pre-release CLI/daemon Item 1 — project the
// ConfigStore-resolved rotation tunables into the daemon
// process env so file-stored values
@@ -190,11 +239,12 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
cmd
.command("stop")
.description("Stop the daemon")
.description("Stop the daemon (10s shutdown budget; 12s process wait; incomplete drain exits nonzero)")
.addHelpText("after", "\nSends at most one SIGTERM to a live target and verifies the original PID and listener. Repeated signals join shutdown.\nFor a recorded target, missing/stale receipts and incomplete drains exit nonzero, including retries.\nTarget state is retained until a matching clean receipt; status reads preserve unverified state.\nNo target is a distinct no-op, never clean-drain certification; unbound incomplete evidence stays unverified.\nInspect OPENRIG_HOME/daemon-shutdown.json and daemon.log for the phase and outcome.\nThe bound covers asynchronous shutdown; an event-loop wedge still requires operator recovery.\n")
.action(async () => {
try {
await stopDaemon(getDeps());
console.log("Daemon stopped");
const outcome = await stopDaemon(getDeps());
console.log(outcome === "stopped" ? "Daemon stopped" : "No daemon target recorded; listener refused. Nothing to stop; prior drain not certified.");
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exitCode = 1;
@@ -235,7 +285,7 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
console.log("Daemon stopped");
break;
case "stale":
console.log("Daemon stale (cleaned up)");
console.log("Daemon PID is absent (stale state)");
break;
case "unverified":
// 1ae863d2 — C3 semantics: we could NOT confirm up or down; never claim stopped.
+1 -1
View File
@@ -70,7 +70,7 @@ function resolveRuntimeConfig(configStore: Pick<ConfigStore, "resolve">): Resolv
function realDestroyDeps(): DestroyDeps {
const lifecycleDeps = realDeps();
return {
stopDaemon: async () => stopDaemon(lifecycleDeps),
stopDaemon: async () => { await stopDaemon(lifecycleDeps); },
inspectListener: async (host: string, port: number): Promise<ListenerInspection> => {
try {
const res = await fetch(`http://${host}:${port}/healthz`);
+8 -6
View File
@@ -15,6 +15,7 @@ import {
import { buildTmuxControlFailure, probeTmuxControl } from "../tmux-health.js";
import { parse as parseYaml } from "yaml";
import { compareSpecToLive, topologyFromRigSpec, topologyFromLiveLogicalIds } from "@openrig/daemon/spec-conformance";
import { classifyNodeVersion } from "../node-support.js";
interface DoctorCheck {
name: string;
@@ -48,7 +49,6 @@ export interface DoctorDeps {
fetchLiveLogicalIds?: (rigName: string) => Promise<string[] | null>;
}
const MIN_NODE_MAJOR = 20;
const DEFAULT_PORT = 7433;
function defaultCheckPort(port: number, host: string): Promise<boolean> {
@@ -96,16 +96,18 @@ export function runDoctorChecks(deps: DoctorDeps): { checks: DoctorCheck[]; port
}
// 3. Node version
const major = parseInt(process.version.replace(/^v/, ""), 10);
if (major >= MIN_NODE_MAJOR) {
const nodeSupport = classifyNodeVersion(process.version);
if (nodeSupport.kind === "supported") {
checks.push({ name: "node_version", status: "pass", message: `Node ${process.version}` });
} else if (nodeSupport.kind === "untested") {
checks.push({ name: "node_version", status: "warn", message: nodeSupport.message! });
} else {
checks.push({
name: "node_version",
status: "fail",
message: `Node ${process.version} is below minimum (v${MIN_NODE_MAJOR}).`,
reason: "OpenRig requires Node 20+ for built-in fetch, ESM, and stable API support.",
fix: "Install Node 20+ via nvm, fnm, or your package manager.",
message: nodeSupport.message!,
reason: nodeSupport.reason,
fix: nodeSupport.fix,
});
}
+6
View File
@@ -58,6 +58,12 @@ Examples:
}
const receipt = res.data["receipt"] as Record<string, unknown> | null;
const probeStatus = res.data["probeStatus"];
if (probeStatus === "stale" || probeStatus === "no_orchestrator") {
const probeError = res.data["probeError"];
console.error(`Service status is ${probeStatus === "stale" ? "stale" : "unverified"} (probe: ${probeStatus}); ${receipt ? "showing the saved receipt" : "no saved receipt is available"}.${typeof probeError === "string" && probeError ? ` ${probeError}` : ""}`);
}
if (!receipt) {
console.log("Services configured but no receipt available yet.");
return;
+9 -12
View File
@@ -98,22 +98,19 @@ export function expandCommand(depsOverride?: StatusDeps): Command {
for (const w of data.warnings) console.log(` Warning: ${w}`);
}
// Honest retry guidance for failed nodes. The pod-aware /launch route
// returns pod_aware_launch_unsupported for any node `expand` creates
// (every expand-created node has podId set), so the recoverable path is
// shrink-the-failed-pod + fix + re-expand, not per-node relaunch.
// Honest retry guidance for failed nodes. The /launch route runs every
// pod-bearing node (which every expand-created node is) through the
// managed subset launch with full startup orchestration, so per-node
// relaunch is the recovery path once the underlying cause is fixed.
if (data.retryTargets && data.retryTargets.length > 0) {
console.log("");
console.log(" Failed nodes cannot be relaunched directly because pod-aware launch");
console.log(" bypasses startup orchestration. Recover by:");
console.log(" Failed nodes can be relaunched individually. Recover by:");
console.log(` 1. Verify other seats are healthy: rig ps --nodes --rig ${rigId}`);
if (data.podNamespace) {
console.log(` 2. Shrink the failed new pod: rig shrink ${rigId} ${data.podNamespace}`);
} else {
console.log(` 2. Shrink the failed new pod: rig shrink ${rigId} <pod-namespace>`);
console.log(` 2. Fix the spec/config issue behind each failure.`);
console.log(` 3. Relaunch each failed node:`);
for (const target of data.retryTargets) {
console.log(` rig launch ${rigId} ${target}`);
}
console.log(` 3. Fix the spec/config issue.`);
console.log(` 4. Re-run: rig expand ${rigId} <pod-fragment-path>`);
console.log(` Failed targets: ${data.retryTargets.join(", ")}`);
}
+261 -44
View File
@@ -1,4 +1,5 @@
import { Command } from "commander";
import { resolveSenderSession, SENDER_FALLBACK } from "../sender-identity.js";
// The human registry is home-state owned by the daemon; the verb LAZY-imports the narrow
// @openrig/daemon/gateway-human-registry surface at invocation (the C3/crash-cart dep rail —
// one source, no twin). Type-only import keeps the surface off the eager cli graph.
@@ -9,45 +10,115 @@ import type { addHumanFragment as AddHumanFragment } from "@openrig/daemon/gatew
// write -> re-project). address is DERIVED as <entityId>@external (the registered
// convention, pinned by the schema); no-clobber unless --replace is passed explicitly.
interface BindingSpec {
kind: string;
connectorRef: string;
secretsRef: string;
role: string;
handle?: string;
// The --binding spec parse (kind:connectorRef:secretsRef:role[:handle=<id>], ':'-bearing
// vault-pointer secretsRef, optional handle= token) lives in the registry surface as
// parseBindingSpec — ONE source for add (here) and `set binding.<n>` (S12), lazy-imported
// with the rest of the surface so the eager CLI graph stays daemon-free.
// ── S12 (OPR.0.5.5.12): the queue-row half of the remove guard, injectable so verb tests
// never need a live daemon. ok:false = the source COULD NOT BE CHECKED (unreachable daemon)
// — an indeterminate, never evidence of an empty board.
export interface HumanRowRef {
id: string;
state: string;
summary?: string;
}
export type HumanRowsLookup = (address: string) => Promise<
| { ok: true; rows: HumanRowRef[] }
| { ok: false; error: string }
>;
export interface GatewayCommandDeps {
queueRows?: HumanRowsLookup;
humanReadiness?: HumanReadinessLookup;
}
/** --binding kind:connectorRef:secretsRef:role[:handle=<id>]. secretsRef is a vault POINTER and
* may itself contain ':' (e.g. "vault://slack/mike"), so kind/connectorRef are the first two
* fields, role is the LAST positional, and secretsRef is everything between (colons intact).
* A6 v3: an OPTIONAL `handle=<id>` token (the human's platform id, e.g. Slack user id) may appear
* anywhere; it is extracted first so it never disturbs the positional parse. A handle-less binding
* is outbound-only (not inbound-resolvable). */
function parseBinding(spec: string): BindingSpec | { error: string } {
const all = spec.split(":");
const handleTokens = all.filter((p) => p.startsWith("handle="));
if (handleTokens.length > 1) {
return { error: `--binding must carry at most one handle= token (got "${spec}")` };
}
const handle = handleTokens.length === 1 ? handleTokens[0]!.slice("handle=".length) : undefined;
if (handle !== undefined && handle.length === 0) {
return { error: `--binding handle= must be non-empty (got "${spec}")` };
}
const parts = all.filter((p) => !p.startsWith("handle="));
if (parts.length < 4) {
return { error: `--binding must be kind:connectorRef:secretsRef:role[:handle=<id>] (got "${spec}")` };
}
const kind = parts[0]!;
const connectorRef = parts[1]!;
const role = parts[parts.length - 1]!;
const secretsRef = parts.slice(2, -1).join(":");
if (!kind || !connectorRef || !secretsRef || !role) {
return { error: `--binding fields must be non-empty: kind:connectorRef:secretsRef:role[:handle=<id>] (got "${spec}")` };
}
return handle !== undefined ? { kind, connectorRef, secretsRef, role, handle } : { kind, connectorRef, secretsRef, role };
export interface HumanReadinessView {
state: "ready" | "not-ready" | "indeterminate";
configured: boolean | null;
enabled: boolean | null;
active: boolean | null;
ready: boolean;
connector?: { kind: string; ref: string };
reason: string;
nextAction: string | null;
checkedAt?: string;
}
export function gatewayCommand(): Command {
export type HumanReadinessLookup = (entityId: string) => Promise<HumanReadinessView>;
export async function daemonHumanReadiness(entityId: string): Promise<HumanReadinessView> {
try {
const { DaemonClient } = await import("../client.js");
const response = await new DaemonClient().get<{ ok?: boolean; readiness?: HumanReadinessView; error?: unknown; message?: unknown }>(
`/api/gateway/human/${encodeURIComponent(entityId)}/readiness`,
);
if (response.status !== 200) {
const detail = [response.data?.error, response.data?.message].filter((value) => typeof value === "string").join(": ");
throw new Error(`HTTP ${response.status}: ${detail || "readiness request refused"}`);
}
const readiness = response.data?.readiness;
if (response.data?.ok !== true || !readiness
|| !["ready", "not-ready", "indeterminate"].includes(readiness.state)
|| [readiness.configured, readiness.enabled, readiness.active].some((value) => value !== null && typeof value !== "boolean")
|| readiness.ready !== (readiness.state === "ready")
|| typeof readiness.reason !== "string" || !readiness.reason.trim()
|| (readiness.nextAction !== null && typeof readiness.nextAction !== "string")
|| (readiness.connector !== undefined && (!readiness.connector || typeof readiness.connector.kind !== "string" || typeof readiness.connector.ref !== "string"))
|| (readiness.checkedAt !== undefined && typeof readiness.checkedAt !== "string")) {
throw new Error("HTTP 200: malformed delivery readiness response");
}
return readiness;
} catch (error) {
return {
state: "indeterminate",
configured: null,
enabled: null,
active: null,
ready: false,
reason: `delivery readiness could not be read from the daemon: ${(error as Error).message}`,
nextAction: "rig status",
};
}
}
/** Default queue-row half of the remove guard: non-terminal rows addressed to the human,
* via the daemon. ok:false carries the reason the board COULD NOT BE CHECKED. */
export async function daemonQueueRows(address: string): ReturnType<HumanRowsLookup> {
try {
const { DaemonClient } = await import("../client.js");
const client = new DaemonClient();
// Fix-r1 F2 (R2 blocking): enumerate to EXHAUSTION. A bounded read that comes back
// FULL is truncation-suspect — completeness is proven only by a window strictly larger
// than the result. Grow the window until that holds; a board that outgrows every
// window is an honest completeness refusal, never a silently-shortened orphan list.
let limit = 1000;
for (let attempt = 0; attempt < 6; attempt++) {
const res = await client.get<Array<{ qitemId?: string; state?: string; summary?: string | null }>>(
`/api/queue/list?destinationSession=${encodeURIComponent(address)}&state=pending,in-progress,blocked&limit=${limit}&compact=1`,
);
const data = Array.isArray(res.data) ? res.data : [];
if (data.length < limit) {
return {
ok: true,
rows: data.map((r) => ({
id: String(r.qitemId ?? "(unknown-id)"),
state: String(r.state ?? "(unknown-state)"),
...(r.summary ? { summary: String(r.summary) } : {}),
})),
};
}
limit *= 4;
}
return { ok: false, error: `queue enumeration for ${address} saturated every window up to ${limit / 4} rows — completeness unproven` };
} catch (err) {
return { ok: false, error: (err as Error).message };
}
}
export function gatewayCommand(deps: GatewayCommandDeps = {}): Command {
const queueRows: HumanRowsLookup = deps.queueRows ?? daemonQueueRows;
const humanReadiness: HumanReadinessLookup = deps.humanReadiness ?? daemonHumanReadiness;
const cmd = new Command("gateway").description("Gateway: the human registry + connector surfaces");
const human = cmd.command("human").description("Manage human specs (file-per-human fragments under gateway/humans/)");
@@ -63,12 +134,39 @@ export function gatewayCommand(): Command {
.requiredOption("--delivery-class <A|B|C|D>", "Notification loudness class (the notifications register selection)")
.option("--away", "Set the AWAY preset")
.option("--replace", "Explicitly replace an existing human (no silent overwrite)")
.action(async (entityId: string, opts: { displayName: string; binding: string[]; deliveryClass: string; away?: boolean; replace?: boolean }) => {
const bindings: BindingSpec[] = [];
.option("--reason <reason>", "Reason recorded with connector binding changes", "register human delivery")
.option("--actor <actor>", "Named operator when outside a managed seat")
.action(async (entityId: string, opts: { displayName: string; binding: string[]; deliveryClass: string; away?: boolean; replace?: boolean; reason: string; actor?: string }) => {
// LAZY import the narrow daemon surface at invocation (dep rail 2).
const registry = await import("@openrig/daemon/gateway-human-registry");
const { addHumanFragment, parseBindingSpec } = registry as unknown as {
addHumanFragment: typeof AddHumanFragment;
parseBindingSpec: (spec: string) => { ok: true; binding: Record<string, unknown> } | { ok: false; error: string };
};
// Fix-r1 F1 (R2 blocking, A1/R5): the add VERB is the single-human boundary. A second
// DISTINCT human is refused BEFORE any parse or write — the several-fragment state A1
// describes as hand-authored must never be produced by the product's own surface.
// Fail closed: an unverifiable registry refuses too (never add into an unknown state).
const existing = registry.listHumans();
if (!existing.ok) {
console.error(`refused: cannot verify the single-human boundary — ${existing.error}`);
process.exitCode = 1;
return;
}
if (existing.humans.length > 0 && !existing.humans.some((h) => h.entityId === entityId)) {
const ids = existing.humans.map((h) => h.entityId).join(", ");
console.error(
`refused: a human is already configured (${ids}) — 0.5.5 ships the SIMPLE SINGLE-HUMAN surface (amendment A1, founder R5), so \`rig gateway human add\` manages one human. ` +
`If you truly need several, hand-author a fragment YAML under ${registry.humansDir()} (several fragments are displayed honestly, with an advisory); multi-human MANAGEMENT arrives in 0.5.7.`,
);
process.exitCode = 1;
return;
}
const bindings: Record<string, unknown>[] = [];
for (const spec of opts.binding) {
const b = parseBinding(spec);
if ("error" in b) { console.error(b.error); process.exitCode = 1; return; }
bindings.push(b);
const b = parseBindingSpec(spec);
if (!b.ok) { console.error(`--binding: ${b.error}`); process.exitCode = 1; return; }
bindings.push(b.binding);
}
const fragment: Record<string, unknown> = {
entityId,
@@ -78,11 +176,130 @@ export function gatewayCommand(): Command {
connectorBindings: bindings,
prefs: { deliveryClass: opts.deliveryClass, ...(opts.away ? { away: true } : {}) },
};
// LAZY import the narrow daemon surface at invocation (dep rail 2).
const { addHumanFragment } = (await import("@openrig/daemon/gateway-human-registry")) as { addHumanFragment: typeof AddHumanFragment };
const res = addHumanFragment(fragment, undefined, { replace: !!opts.replace });
const before = registry.showHuman(entityId);
try {
const result = await registry.runChannelOperation({
actor: resolveSenderSession() ?? opts.actor ?? SENDER_FALLBACK, provenance: "claimed:v1", reason: opts.reason,
action: "binding", subject: `${entityId}@external`,
before: { digest: registry.channelStateDigest(before.ok ? before.record.connectorBindings.map(({ inboundResolvable: _derived, ...binding }) => binding) : null) },
run: async () => {
const res = addHumanFragment(fragment, undefined, { replace: !!opts.replace });
if (!res.ok) throw new Error(res.error);
const digest = registry.channelStateDigest(res.fragment.connectorBindings);
return { value: res, after: { digest }, effect: before.ok && registry.channelStateDigest(before.record.connectorBindings.map(({ inboundResolvable: _derived, ...binding }) => binding)) === digest ? "no-op" : "applied" };
},
});
console.log(JSON.stringify({ ok: true, entityId, path: result.value.path, receipt: result.receipt }));
} catch (error) { console.error(`refused: ${(error as Error).message}`); process.exitCode = 1; }
});
// ── S12 (OPR.0.5.5.12): the fragment lifecycle beyond add. Every verb operates through
// fragments + regeneration (the registry surface); none writes the generated projection.
human
.command("list")
.description("Show the configured human (single-human surface per A1/R5; several fragments render honestly with a 0.5.7 advisory)")
.option("--json", "Complete record(s) as JSON")
.action(async (opts: { json?: boolean }) => {
const { listHumans } = await import("@openrig/daemon/gateway-human-registry");
const res = listHumans();
if (!res.ok) { console.error(`refused: ${res.error}`); process.exitCode = 1; return; }
console.log(JSON.stringify({ ok: true, entityId: res.fragment.entityId, path: res.path }));
const humans = await Promise.all(res.humans.map(async (record) => ({ ...record, deliveryReadiness: await humanReadiness(record.entityId) })));
if (opts.json) { console.log(JSON.stringify({ ok: true, humans, ...(res.advisory ? { advisory: res.advisory } : {}) })); return; }
if (res.humans.length === 0) { console.log("no human configured yet — register one: rig gateway human add <entityId> --display-name … --binding … --delivery-class …"); return; }
for (const h of humans) {
const inbound = h.bindings.inboundResolvable ? "" : " [outbound-only]";
console.log(`${h.entityId} "${h.displayName}" class=${h.deliveryClass} ${h.away ? "away" : "available"} bindings=${h.bindings.count} (primary ${h.bindings.primary.kind}:${h.bindings.primary.connectorRef}) delivery=${h.deliveryReadiness.state}${inbound}`);
if (!h.deliveryReadiness.ready) console.log(` delivery: ${h.deliveryReadiness.reason}${h.deliveryReadiness.nextAction ? `; next: ${h.deliveryReadiness.nextAction}` : ""}`);
}
if (res.advisory) console.log(`advisory: ${res.advisory}`);
});
human
.command("show <entityId>")
.description("Show the effective record: fragment values + which defaults filled the rest, with provenance")
.option("--json", "Complete record as JSON")
.action(async (entityId: string, opts: { json?: boolean }) => {
const { showHuman } = await import("@openrig/daemon/gateway-human-registry");
const res = showHuman(entityId);
if (!res.ok) { console.error(`refused: ${res.error}`); process.exitCode = 1; return; }
const r = { ...res.record, deliveryReadiness: await humanReadiness(entityId) };
if (opts.json) { console.log(JSON.stringify({ ok: true, record: r })); return; }
console.log(`${r.entityId} (${r.address}) — "${r.displayName}"`);
console.log(` fragment: ${r.fragmentPath}`);
console.log(` delivery-class: ${r.prefs.deliveryClass.value} (${r.prefs.deliveryClass.source})`);
console.log(` away: ${r.prefs.away.value} (${r.prefs.away.source})`);
r.connectorBindings.forEach((b, i) => {
console.log(` binding.${i}: ${b.kind}:${b.connectorRef} role=${b.role}${b.handle ? ` handle=${b.handle}` : " [outbound-only]"}`);
});
console.log(` delivery-readiness: ${r.deliveryReadiness.state} (${r.deliveryReadiness.reason})`);
if (r.deliveryReadiness.nextAction) console.log(` next: ${r.deliveryReadiness.nextAction}`);
});
human
.command("set <entityId> <field> <value>")
.description("Edit one field through the verb (same validation as add; re-projection immediate). Fields: display-name, delivery-class, away, binding.<n>")
.option("--reason <reason>", "Reason recorded with connector binding changes", "update human binding")
.option("--actor <actor>", "Named operator when outside a managed seat")
.action(async (entityId: string, field: string, value: string, opts: { reason: string; actor?: string }) => {
const registry = await import("@openrig/daemon/gateway-human-registry");
if (field.startsWith("binding.")) {
const before = registry.showHuman(entityId);
if (!before.ok) { console.error(`refused: ${before.error}`); process.exitCode = 1; return; }
try {
const prior = registry.channelStateDigest(before.record.connectorBindings.map(({ inboundResolvable: _derived, ...binding }) => binding));
const result = await registry.runChannelOperation({
actor: resolveSenderSession() ?? opts.actor ?? SENDER_FALLBACK, provenance: "claimed:v1", reason: opts.reason,
action: "binding", subject: `${entityId}@external`, before: { digest: prior },
run: async () => {
const res = registry.setHumanField(entityId, field, value);
if (!res.ok) throw new Error(res.error);
const digest = registry.channelStateDigest(res.fragment.connectorBindings);
return { value: res, after: { digest }, effect: digest === prior ? "no-op" : "applied" };
},
});
console.log(JSON.stringify({ ok: true, entityId, field, path: result.value.path, receipt: result.receipt }));
} catch (error) { console.error(`refused: ${(error as Error).message}`); process.exitCode = 1; }
return;
}
const res = registry.setHumanField(entityId, field, value);
if (!res.ok) { console.error(`refused: ${res.error}`); process.exitCode = 1; return; }
console.log(JSON.stringify({ ok: true, entityId: res.fragment.entityId, field, path: res.path }));
});
human
.command("remove <entityId>")
.description("Remove a human: refuses while open conversations or non-terminal rows exist (--force archives and records what was orphaned; the fragment is archived, never deleted)")
.option("--force", "Archive despite KNOWN in-flight items (each is recorded as orphaned)")
.action(async (entityId: string, opts: { force?: boolean }) => {
const { removeHumanFragment, pendingConversationsFor, ADDRESS_DOMAIN } =
await import("@openrig/daemon/gateway-human-registry");
const address = `${entityId}@${ADDRESS_DOMAIN}`;
// Guard input 1 (fs): un-Acked outbound decisions = open conversations.
const conversations = pendingConversationsFor(entityId);
// Guard input 2 (daemon): non-terminal rows addressed to the human. An unreachable
// board is INDETERMINATE — remove refuses rather than fabricating an absence, and
// --force does not apply (it overrides KNOWN in-flight work, not ignorance of it).
const rowsRes = await queueRows(address);
if (!rowsRes.ok) {
console.error(
`refused: the queue could not be checked for non-terminal rows addressed to ${address} (${rowsRes.error}) — ` +
`remove needs a reachable daemon to prove nothing would be orphaned; --force does not override an unchecked board.`,
);
process.exitCode = 1;
return;
}
const inflight = [
...conversations,
...rowsRes.rows.map((r) => ({
kind: "queue-row" as const,
id: r.id,
detail: `${r.state} row ${r.id}${r.summary ? ` — ${r.summary}` : ""}`,
})),
];
const res = removeHumanFragment(entityId, { force: !!opts.force, inflight });
if (!res.ok) { console.error(`refused: ${res.error}`); process.exitCode = 1; return; }
console.log(JSON.stringify({ ok: true, removed: res.removed, archivedPath: res.archivedPath, ...(res.orphanRecordPath ? { orphanRecordPath: res.orphanRecordPath } : {}) }));
});
return cmd;
+231
View File
@@ -0,0 +1,231 @@
import { resolve as resolvePath } from "node:path";
import { Command } from "commander";
import { DaemonClient } from "../client.js";
import { getDaemonStatus, getDaemonUrl, daemonStatusGuard } from "../daemon-lifecycle.js";
import { realDeps } from "./daemon.js";
import type { StatusDeps } from "./status.js";
import { resolveDefaultAgentRef } from "./topology-default-agent.js";
interface RigNode {
rigName: string;
podNamespace: string | null;
}
interface GrowNode {
logicalId: string;
status: "launched" | "failed" | "attention_required";
sessionName?: string;
error?: string;
}
interface AddMemberResult {
ok: boolean;
result?: {
podNamespace: string;
node: GrowNode;
};
errors?: string[];
message?: string;
error?: string;
}
interface ExpandResult {
ok: boolean;
status?: "ok" | "partial" | "failed";
podNamespace?: string;
nodes?: GrowNode[];
warnings?: string[];
retryTargets?: string[];
errors?: string[];
message?: string;
error?: string;
}
export function growCommand(depsOverride?: StatusDeps): Command {
const cmd = new Command("grow").description("Add one or more seats to a running rig");
const getDeps = () => depsOverride ?? {
lifecycleDeps: realDeps(),
clientFactory: (url: string) => new DaemonClient(url),
};
cmd
.argument("<rig-id>", "Target rig ID")
.argument("<members...>", "Names for the new seats")
.option("--pod <pod>", "Target pod; inferred when the rig has one pod")
.option("--new-pod <pod>", "Create a new pod for the seats")
.option("--runtime <runtime>", "Agent runtime", "claude-code")
.option("--cwd <path>", "Working directory for the seat", process.cwd())
.option("--json", "JSON output for agents")
.action(async (
rigId: string,
members: string[],
opts: { pod?: string; newPod?: string; runtime: string; cwd: string; json?: boolean },
) => {
if (opts.pod && opts.newPod) {
console.error("Choose either --pod or --new-pod, not both.");
process.exitCode = 1;
return;
}
const deps = getDeps();
const status = await getDaemonStatus(deps.lifecycleDeps);
if (!daemonStatusGuard(status)) return;
const client = deps.clientFactory(getDaemonUrl(status));
const rigRes = await client.get<RigNode[]>(`/api/rigs/${encodeURIComponent(rigId)}/nodes`);
if (rigRes.status >= 400) {
console.error(`Rig ${rigId} was not found.`);
process.exitCode = 1;
return;
}
const rigName = rigRes.data[0]?.rigName;
const pods = [...new Set(
rigRes.data
.map((node) => node.podNamespace)
.filter((namespace): namespace is string => typeof namespace === "string"),
)];
if (opts.newPod && pods.includes(opts.newPod)) {
console.error(`Pod ${opts.newPod} already exists. Use --pod ${opts.newPod} to grow it.`);
process.exitCode = 1;
return;
}
const pod = opts.newPod
?? (opts.pod
? pods.find((candidate) => candidate === opts.pod)
: pods.length === 1
? pods[0]
: undefined);
if (!pod) {
const available = pods.join(", ") || "none";
console.error(opts.pod
? `Pod ${opts.pod} was not found. Available pods: ${available}.`
: `Choose a pod with --pod. Available pods: ${available}.`);
process.exitCode = 1;
return;
}
let agentRef: string;
try {
agentRef = await resolveDefaultAgentRef(client);
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exitCode = 1;
return;
}
const cwd = resolvePath(opts.cwd);
const memberSpecs = members.map((member) => ({
id: member,
agent_ref: agentRef,
runtime: opts.runtime,
profile: "default",
cwd,
}));
let nodes: GrowNode[] = [];
let ok = false;
let detail: string | undefined;
let warnings: string[] = [];
let retryTargets: string[] = [];
if (opts.newPod) {
const res = await client.post<ExpandResult>(
`/api/rigs/${encodeURIComponent(rigId)}/expand`,
{
pod: { id: pod, label: pod, members: memberSpecs, edges: [] },
rigRoot: cwd,
},
{ timeoutMs: 120_000 },
);
nodes = res.data.nodes ?? [];
warnings = res.data.warnings ?? [];
retryTargets = res.data.retryTargets ?? [];
ok = res.status < 400
&& res.data.ok
&& res.data.status === "ok"
&& nodes.length === members.length
&& nodes.every((node) => node.status === "launched");
detail = res.data.errors?.join("; ")
?? res.data.message
?? res.data.error
?? nodes.find((node) => node.error)?.error;
const returned = new Set(nodes.map((node) => node.logicalId));
for (const member of memberSpecs) {
const logicalId = `${pod}.${member.id}`;
if (!returned.has(logicalId)) {
nodes.push({ logicalId, status: "failed", error: detail ?? "No node outcome returned." });
}
}
} else {
const failures: string[] = [];
for (const member of memberSpecs) {
const res = await client.post<AddMemberResult>(
`/api/rigs/${encodeURIComponent(rigId)}/pods/${encodeURIComponent(pod)}/members`,
{ member, rigRoot: cwd },
{ timeoutMs: 120_000 },
);
const responseDetail = res.data.errors?.join("; ")
?? res.data.message
?? res.data.error
?? res.data.result?.node.error
?? `Grow failed (HTTP ${res.status})`;
const node = res.data.result?.node ?? {
logicalId: `${pod}.${member.id}`,
status: "failed" as const,
error: responseDetail,
};
nodes.push(node);
if (res.status >= 400 || !res.data.ok || node.status !== "launched") {
failures.push(`${member.id}: ${responseDetail}`);
}
}
ok = failures.length === 0;
detail = failures.join("; ") || undefined;
}
if (opts.json) {
console.log(JSON.stringify({
ok,
rigId,
rigName,
pod,
seats: nodes.map((node) => node.logicalId),
source: agentRef,
...(nodes.length === 1 ? { seat: nodes[0]?.logicalId, node: nodes[0] } : {}),
nodes,
...(detail ? { detail } : {}),
warnings,
retryTargets,
}, null, 2));
} else if (ok) {
console.log(`Grew rig ${rigName ?? rigId}`);
if (nodes.length === 1) {
const node = nodes[0]!;
console.log(` Seat: ${node.logicalId}${node.sessionName ? ` (${node.sessionName})` : ""}`);
} else {
console.log(" Seats:");
for (const node of nodes) {
console.log(` ${node.logicalId}${node.sessionName ? ` (${node.sessionName})` : ""}`);
}
}
} else {
console.error(`Grow did not fully succeed for ${rigName ?? rigId}`);
for (const node of nodes) {
const icon = node.status === "launched" ? "OK" : node.status === "attention_required" ? "ATTENTION" : "FAIL";
const session = node.sessionName ? ` (${node.sessionName})` : "";
const error = node.error ? ` - ${node.error}` : "";
console.error(` [${icon}] ${node.logicalId}: ${node.status}${session}${error}`);
}
if (detail && !nodes.some((node) => node.error && detail.includes(node.error))) {
console.error(` ${detail}`);
}
for (const warning of warnings) console.error(` Warning: ${warning}`);
if (retryTargets.length > 0) {
console.error(" Failed nodes can be relaunched individually. Recover by:");
for (const target of retryTargets) console.error(` rig launch ${rigId} ${target}`);
}
}
if (!ok) process.exitCode = 1;
});
return cmd;
}
+467
View File
@@ -0,0 +1,467 @@
import { readFileSync } from "node:fs";
import { Command, InvalidArgumentError, Option } from "commander";
import {
HEALTH_SEVERITIES,
HEALTH_STATUSES,
type HealthRecord,
type HealthScope,
} from "@openrig/daemon/health-projection";
import type { HealthListProjection } from "@openrig/daemon/health-detectors";
import {
DaemonClient,
DaemonConnectionError,
DaemonResponseError,
DaemonTimeoutError,
} from "../client.js";
import {
getDaemonStatus,
getDaemonUrl,
statusGuardMessage,
} from "../daemon-lifecycle.js";
import { realDeps } from "./daemon.js";
import { resolveIdentitySource } from "./whoami.js";
import type { StatusDeps } from "./status.js";
import { shellQuote } from "../cross-host-executor.js";
import { omittedReadField, readView } from "../read-view.js";
interface DiagnosisEntry {
row: Record<string, unknown> & { qitemId: string };
finding: HealthRecord;
packet: Record<string, unknown> & { instructions: string };
receipts: unknown[];
authority: Array<{ path: string; state: string; content?: string; role?: string; reason?: string; selectedBy?: string }>;
disposition: { verdict: string; causalStart: string | null; steering: string; uncertainty: string; evidenceRefs: string[];
correction?: { applicability: string; causalJudgment: string; action: { state: string; summary: string }; effect: { state: string; summary: string } } } | null;
assessment?: { actor?: string; at: string } | null;
behavioralEffect?: string;
guidance?: string;
humanDelivery: { qitemId: string; outcome: string } | null;
notificationReadiness?: { ready: boolean; reason: string } | null;
}
function diagnosisPreview(entry: DiagnosisEntry) {
const omitted: ReturnType<typeof omittedReadField>[] = [];
// ponytail: omit only known evidence payloads; keep current decisions and unknowns intact.
function without<T extends object>(record: T, keys: string[], prefix = ""): T {
const copy = { ...record } as Record<string, unknown>;
for (const key of keys) {
const value = copy[key];
if (value === undefined || value === null || value === "" || (Array.isArray(value) && !value.length)) continue;
omitted.push(omittedReadField(`${prefix}${key}`, value));
delete copy[key];
}
return copy as T;
}
const result = without(entry, ["receipts", "guidance"]);
result.row = without(entry.row, ["body", "chainOfRecord"], "row.");
result.packet = without(entry.packet, ["finding", "authority", "instructions"], "packet.");
result.finding = without(entry.finding, ["evidence"], "finding.");
if (entry.finding.ceremony) {
result.finding.ceremony = { ...entry.finding.ceremony,
workflowReceipts: entry.finding.ceremony.workflowReceipts.map((receipt, index) => {
const envelope = without(receipt, ["evidence"], `finding.ceremony.workflowReceipts[${index}].`);
const evidence = receipt.evidence;
// These are the existing cut/acceptance identity spellings, not a verdict on the receipt.
const identity = evidence && typeof evidence === "object" && !Array.isArray(evidence)
? Object.fromEntries(Object.entries(evidence).filter(([key, value]) =>
["candidate", "candidateSha", "candidate_sha", "cutSha", "tree", "verdict", "evidenceRef", "evidence_ref"].includes(key)
&& typeof value === "string"))
: {};
return { ...envelope, evidenceIdentity: identity };
}),
};
}
result.authority = entry.authority.map((ref, index) => without(ref, ["content"], `authority[${index}].`));
return { ...result, readView: readView(entry, `rig health diagnosis show ${shellQuote(entry.row.qitemId)} --full --json`, omitted) };
}
const HEALTH_ERROR_SCHEMA = "openrig.health-error/v0alpha1" as const;
type IdentitySource = { nodeId?: string; sessionName?: string };
export interface HealthDeps extends StatusDeps {
resolveIdentity: () => IdentitySource | null;
}
interface HealthListOptions {
self?: boolean;
seat?: string;
rig?: string;
instance?: string | boolean;
severity?: string;
status?: string;
limit?: number;
json?: boolean;
}
interface HealthCliError {
schema: typeof HEALTH_ERROR_SCHEMA;
error: string;
message: string;
nextInspection: string;
details?: unknown;
}
function defaultDeps(): HealthDeps {
return {
lifecycleDeps: realDeps(),
clientFactory: (baseUrl: string) => new DaemonClient(baseUrl),
resolveIdentity: () => resolveIdentitySource({}),
};
}
function parseLimit(value: string): number {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 200) {
throw new InvalidArgumentError("must be an integer from 1 to 200");
}
return parsed;
}
function emitError(error: HealthCliError, json: boolean): void {
if (json) {
console.log(JSON.stringify(error));
} else {
console.error(`Error: ${error.message}`);
console.error(` Next inspection: ${error.nextInspection}`);
}
process.exitCode = 1;
}
function daemonError(details: unknown): HealthCliError {
return {
schema: HEALTH_ERROR_SCHEMA,
error: "health_daemon_unavailable",
message: "The health projection could not be read because the daemon did not respond.",
nextInspection: "rig daemon status",
details,
};
}
function responseError(status: number, data: unknown): HealthCliError {
const daemonCode = typeof data === "object" && data !== null && typeof (data as { error?: unknown }).error === "string"
? (data as { error: string }).error
: null;
if (status === 503 && daemonCode === "health_projection_unavailable") {
return {
schema: HEALTH_ERROR_SCHEMA,
error: daemonCode,
message: "The daemon is reachable, but its health projection is unavailable.",
nextInspection: "rig daemon status",
};
}
if (status === 404 && daemonCode === "health_finding_not_found") {
return {
schema: HEALTH_ERROR_SCHEMA,
error: daemonCode,
message: "No health finding exists with that ID in the current projection.",
nextInspection: "rig health --instance --json",
};
}
if (status === 404 && daemonCode === "not_found") {
return {
schema: HEALTH_ERROR_SCHEMA,
error: "health_projection_unavailable",
message: "The daemon is reachable, but it does not expose the health projection.",
nextInspection: "rig --version",
details: data,
};
}
return {
schema: HEALTH_ERROR_SCHEMA,
error: "health_query_failed",
message: `The daemon rejected the health query (HTTP ${status}).`,
nextInspection: "rig health --help",
details: data,
};
}
async function readyClient(deps: HealthDeps, json: boolean): Promise<DaemonClient | null> {
const status = await getDaemonStatus(deps.lifecycleDeps, { cleanupStaleState: false });
if (status.state !== "running" || status.healthy === false) {
emitError(daemonError(statusGuardMessage(status)), json);
return null;
}
return deps.clientFactory(getDaemonUrl(status));
}
async function readSelfSeatId(client: DaemonClient, deps: HealthDeps, json: boolean): Promise<string | null> {
const source = deps.resolveIdentity();
if (!source) {
emitError({
schema: HEALTH_ERROR_SCHEMA,
error: "health_identity_unavailable",
message: "The current seat identity is unavailable, so self health cannot be scoped safely.",
nextInspection: "rig whoami --json",
}, json);
return null;
}
const params = new URLSearchParams();
if (source.nodeId) params.set("nodeId", source.nodeId);
else if (source.sessionName) params.set("sessionName", source.sessionName);
params.set("compact", "1");
const response = await client.get<Record<string, unknown>>(`/api/whoami?${params.toString()}`);
if (response.status === 409) {
emitError({
schema: HEALTH_ERROR_SCHEMA,
error: "health_identity_ambiguous",
message: "More than one managed seat matches the current identity.",
nextInspection: "rig ps --nodes -A",
details: response.data,
}, json);
return null;
}
if (response.status >= 400) {
emitError({
schema: HEALTH_ERROR_SCHEMA,
error: "health_identity_unavailable",
message: "The daemon could not resolve the current seat identity.",
nextInspection: "rig whoami --json",
details: response.data,
}, json);
return null;
}
const identity = response.data["identity"];
const nodeId = typeof identity === "object" && identity !== null
? (identity as Record<string, unknown>)["nodeId"]
: null;
if (typeof nodeId !== "string" || nodeId.length === 0) {
emitError({
schema: HEALTH_ERROR_SCHEMA,
error: "health_identity_indeterminate",
message: "The identity response did not carry a stable node ID, so self health is indeterminate.",
nextInspection: "rig whoami --full --json",
details: response.data,
}, json);
return null;
}
return nodeId;
}
function scopeLabel(scope: HealthScope): string {
switch (scope.type) {
case "instance": return `instance:${scope.instanceId}`;
case "rig": return `rig:${scope.rigId}`;
case "seat": return `seat:${scope.seatId}`;
case "mission": return `mission:${scope.missionId}`;
case "slice": return `slice:${scope.sliceId}`;
}
}
function renderList(projection: HealthListProjection): void {
const evaluated = projection.evaluatedAt ?? "unavailable";
console.log(`Fleet health — evaluated=${evaluated} findings=${projection.total} limit=${projection.limit}`);
if (projection.records.length === 0) {
console.log("No health findings match this bounded query. This is not a healthy assertion.");
console.log("Next inspection: widen only as needed with `rig health --instance --json`.");
return;
}
for (const record of projection.records) {
console.log(`${record.id} ${record.severity} ${record.status} ${record.detector}`);
console.log(` ${scopeLabel(record.scope)} ${record.confidence} confidence freshness=${record.freshness.state}${record.indeterminateReason ? ` indeterminate=${record.indeterminateReason}` : ""}`);
console.log(` ${record.summary}`);
if (record.operatingPosture) console.log(` Operating posture: ${record.operatingPosture.posture} (${record.operatingPosture.source}); phase=${record.operatingPosture.context?.phase.value ?? "unknown"}`);
}
if (projection.truncated) {
console.log(`Truncated at ${projection.limit} of ${projection.total}; narrow the scope or raise --limit (maximum 200).`);
}
}
function renderExplanation(record: HealthRecord): void {
console.log(`Health finding ${record.id}`);
console.log(` Detector: ${record.detector} (${record.category})`);
console.log(` Scope: ${scopeLabel(record.scope)}`);
console.log(` Outcome: ${record.severity} / ${record.status} / ${record.confidence} confidence`);
console.log(` Observed: ${record.startedAt ?? "unavailable"} → ${record.lastObservedAt ?? "unavailable"}`);
console.log(` Window: ${record.window.startedAt} → ${record.window.endedAt}; source=${record.window.source}; limit=${record.window.limit}; retention=${record.window.retentionSeconds}s`);
console.log(` Freshness: ${record.freshness.state}; evaluated=${record.freshness.evaluatedAt}; newest=${record.freshness.newestSourceAt ?? "unavailable"}; max-age=${record.freshness.maxAgeSeconds}s; age=${record.freshness.ageSeconds ?? "unavailable"}s`);
if (record.indeterminateReason) console.log(` Indeterminate: ${record.indeterminateReason}`);
console.log(` Rule: ${record.threshold}`);
console.log(` Policy: ${record.policyVersion ?? "not reported by source"}`);
if (record.operatingPosture) console.log(` Posture: ${JSON.stringify(record.operatingPosture)}`);
console.log(` Explanation: ${record.explanation}`);
console.log(` Evidence: ${JSON.stringify(record.evidence)}`);
if (record.ceremony) console.log(` Diagnosis stage: ${record.ceremony.stage}\n Normal context: ${JSON.stringify(record.ceremony)}`);
console.log(` Inspect: ${record.suggestedInspection}`);
}
async function guardedRequest<T>(
request: () => Promise<{ status: number; data: T }>,
json: boolean,
): Promise<{ status: number; data: T } | null> {
try {
return await request();
} catch (error) {
const details = error instanceof DaemonTimeoutError
? { kind: "timeout", message: error.message }
: error instanceof DaemonResponseError
? { kind: "unreadable-response", status: error.status, bodySnippet: error.bodySnippet }
: error instanceof DaemonConnectionError
? { kind: "connection", message: error.message }
: { kind: "unexpected", message: error instanceof Error ? error.message : String(error) };
emitError(daemonError(details), json);
return null;
}
}
export function healthCommand(depsOverride?: HealthDeps): Command {
const deps = depsOverride ?? defaultDeps();
const command = new Command("health")
.description("Inspect read-only, explainable system health records")
.addOption(new Option("--self", "Inspect the current seat (default)").conflicts(["seat", "rig", "instance"]))
.addOption(new Option("--seat <node-id>", "Inspect one seat by stable node ID").conflicts(["self", "rig", "instance"]))
.addOption(new Option("--rig <rig-id>", "Inspect findings whose canonical scope is this rig").conflicts(["self", "seat", "instance"]))
.addOption(new Option("--instance [instance-id]", "Inspect the whole local instance, or one canonical instance scope by ID").conflicts(["self", "seat", "rig"]))
.addOption(new Option("--severity <severity>", "Filter by severity").choices([...HEALTH_SEVERITIES]))
.addOption(new Option("--status <status>", "Filter by finding status").choices([...HEALTH_STATUSES]))
.option("--limit <count>", "Maximum findings (1-200)", parseLimit, 100)
.option("--json", "JSON output; diagnosis list/show need --full for complete evidence")
.option("--actor <name>", "Attribute explicit diagnosis writes when operating outside a managed seat")
.addHelpText("after", `
The default scope is the current seat. --instance without an ID is the bounded
instance-wide projection. Empty output is not a healthy assertion. This command
list/explain never mutate. Diagnosis mutations use explicit subcommands; automatic diagnostic presentation is opt-in policy.`);
command.action(async (options: HealthListOptions) => {
const json = Boolean(options.json);
const client = await readyClient(deps, json);
if (!client) return;
let scopeType: HealthScope["type"] | undefined;
let scopeId: string | undefined;
if (options.seat) {
scopeType = "seat";
scopeId = options.seat;
} else if (options.rig) {
scopeType = "rig";
scopeId = options.rig;
} else if (typeof options.instance === "string") {
scopeType = "instance";
scopeId = options.instance;
} else if (!options.instance) {
const seatId = await guardedRequest(
() => readSelfSeatId(client, deps, json).then((data) => ({ status: data === null ? 1 : 0, data })),
json,
);
if (!seatId || seatId.data === null) return;
scopeType = "seat";
scopeId = seatId.data;
}
const params = new URLSearchParams();
if (scopeType && scopeId) {
params.set("scope_type", scopeType);
params.set("scope_id", scopeId);
}
params.set("limit", String(options.limit ?? 100));
if (options.severity) params.set("severity", options.severity);
if (options.status) params.set("status", options.status);
const response = await guardedRequest(
() => client.get<HealthListProjection>(`/api/health?${params.toString()}`),
json,
);
if (!response) return;
if (response.status >= 400) {
emitError(responseError(response.status, response.data), json);
return;
}
if (json) console.log(JSON.stringify(response.data));
else renderList(response.data);
});
command
.command("explain <finding-id>")
.description("Explain one health finding from its canonical bounded record")
.option("--json", "Emit the canonical daemon health record as JSON")
.action(async (findingId: string, options: { json?: boolean }) => {
const json = Boolean(options.json || command.opts().json);
const client = await readyClient(deps, json);
if (!client) return;
const response = await guardedRequest(
() => client.get<HealthRecord>(`/api/health/${encodeURIComponent(findingId)}`),
json,
);
if (!response) return;
if (response.status >= 400) {
emitError(responseError(response.status, response.data), json);
return;
}
if (json) console.log(JSON.stringify(response.data));
else renderExplanation(response.data);
});
async function diagnosisRequest(path: string, options: { json?: boolean; full?: boolean }, payload?: unknown) {
const json = Boolean(options.json || command.opts().json);
const client = await readyClient(deps, json);
if (!client) return;
const response = await guardedRequest(() => payload === undefined ? client.get(`/api/health-diagnosis${path}`) : client.post(`/api/health-diagnosis${path}`, { ...(payload as object), actor: command.opts().actor }), json);
if (!response) return;
if (response.status >= 400) { console.error(JSON.stringify(response.data)); process.exitCode = 1; return; }
const occurrenceRead = payload === undefined && path !== "/policy" && path !== "/checkpoints";
if (options.full || json || path === "/policy" || path === "/checkpoints" || path === "/evaluate" || path.endsWith("/notify")) {
const data = occurrenceRead && !options.full
? (Array.isArray(response.data) ? response.data.map((entry) => diagnosisPreview(entry as DiagnosisEntry)) : diagnosisPreview(response.data as DiagnosisEntry))
: response.data;
console.log(JSON.stringify(data, null, json ? undefined : 2));
} else {
const entries = Array.isArray(response.data) ? response.data : [response.data];
if (!entries.length) console.log("No diagnostic occurrences. This is not a healthy assertion.");
for (const entry of entries as DiagnosisEntry[]) {
console.log(`${entry.row.qitemId} ${entry.finding.status} ${entry.finding.detector}`);
console.log(` Disposition: ${entry.disposition?.verdict ?? "awaiting agent investigation"}`);
if (path) {
console.log(` Queue: ${entry.row.state ?? "unknown"} Owner: ${entry.row.destinationSession ?? "unknown"} Blocker: ${entry.row.blockedOn ?? "none recorded"}`);
if (entry.notificationReadiness) console.log(` Human readiness: ${entry.notificationReadiness.ready ? "ready" : "unavailable"} — ${entry.notificationReadiness.reason}`);
if (entry.humanDelivery) console.log(` Human delivery: ${entry.humanDelivery.outcome} (${entry.humanDelivery.qitemId})`);
console.log(` Finding: ${entry.finding.id} Policy: ${entry.finding.policyVersion ?? "unreported"}`);
console.log(` ${entry.finding.explanation}`);
if (entry.finding.operatingPosture) console.log(` Posture: ${entry.finding.operatingPosture.posture} (${entry.finding.operatingPosture.source}); phase=${entry.finding.operatingPosture.context?.phase.value ?? "unknown"}; ${entry.finding.operatingPosture.reason}`);
console.log(` Start: ${entry.disposition?.causalStart ?? "unknown"}`);
console.log(` Steering: ${entry.disposition?.steering ?? "not yet recorded"}`);
console.log(` Uncertainty: ${entry.disposition?.uncertainty ?? entry.finding.indeterminateReason ?? "diagnosis pending"}`);
if (entry.assessment) console.log(` Attributed assessment: ${entry.assessment.actor ?? "unknown"} at ${entry.assessment.at}`);
const correction = entry.disposition?.correction;
if (correction) {
console.log(` Applicability: ${correction.applicability}`);
console.log(` Causal judgment: ${correction.causalJudgment}`);
console.log(` Action (${correction.action.state}): ${correction.action.summary}`);
}
console.log(` Later behavioral effect (owner report): ${correction?.effect.state ?? "unobserved"}${correction ? " — " + correction.effect.summary : " — a disposition or clearance alone is not changed behavior"}`);
for (const ref of entry.authority) console.log(` ${ref.role ?? "Authority"} (${ref.state}): ${ref.path}${ref.selectedBy ? " selected by " + ref.selectedBy : ""}${ref.reason ? " — " + ref.reason : ""}`);
if (occurrenceRead) {
const view = diagnosisPreview(entry).readView;
console.log(` Evidence preview; full record ${view.fullJsonBytes} JSON bytes: ${view.fullCommand}`);
} else console.log(` ${entry.packet.instructions}`);
}
}
}
}
function fromFile(file: string): unknown {
const text = readFileSync(file, "utf8");
if (Buffer.byteLength(text) > 1048576) throw new Error("Health input exceeds 1 MiB");
return JSON.parse(text);
}
command.command("policy").description("Inspect effective policy and engine state; apply edited JSON with --file")
.option("--file <path>", "Apply policy JSON with an audit record").option("--json")
.action(async (o: { file?: string; json?: boolean }) => diagnosisRequest("/policy", o, o.file ? { value: fromFile(o.file) } : undefined));
command.command("checkpoint").description("Inspect or submit an outcome-boundary lineage census (not a per-edit ritual)")
.option("--file <path>", "Submit checkpoint JSON with exact queue and product evidence").option("--json")
.action(async (o: { file?: string; json?: boolean }) => diagnosisRequest("/checkpoints", o, o.file ? { value: fromFile(o.file) } : undefined));
command.command("diagnose").description("Preview policy admission; --apply creates or re-presents bounded diagnostic context")
.option("--apply").option("--json").action(async (o: { apply?: boolean; json?: boolean }) => diagnosisRequest("/evaluate", o, { apply: Boolean(o.apply) }));
const diagnosis = command.command("diagnosis").description("Read occurrences and record agent-owned dispositions");
diagnosis.command("list").description("List occurrence summaries; evidence payloads require --full")
.option("--json", "Summary array with explicit omitted fields and per-occurrence expansion commands")
.option("--full", "Complete records including all evidence and receipts; may be large")
.action(async (o: { json?: boolean; full?: boolean }) => diagnosisRequest("", o));
diagnosis.command("show <qitem-id>").description("Inspect current state and decisions; expand retained evidence deliberately")
.option("--json", "Summary JSON with omitted fields, original byte size and exact full command")
.option("--full", "Complete original record; use --full --json for lossless JSON (may be large)")
.action(async (id: string, o: { json?: boolean; full?: boolean }) => diagnosisRequest(`/${encodeURIComponent(id)}`, o));
diagnosis.command("record <qitem-id>").requiredOption("--file <path>", "Disposition JSON with verdict, causalStart, steering, uncertainty, evidenceRefs").option("--json")
.action(async (id: string, o: { file: string; json?: boolean }) => diagnosisRequest(`/${encodeURIComponent(id)}/disposition`, o, { value: fromFile(o.file) }));
diagnosis.command("notify <qitem-id>").description("Explicitly request human delivery under policy and verified connector readiness").option("--json")
.action(async (id: string, o: { json?: boolean }) => diagnosisRequest(`/${encodeURIComponent(id)}/notify`, o, {}));
return command;
}
Loaded 100 of 1508 files, more files were not shown because too many files have changed in this diff. Show more